跳转至

413_数据库事务与并发控制


一句话说明

事务就是"要么全做,要么全不做"的操作包,并发控制防止多人同时操作时数据乱套。


核心知识点

ACID四大特性

特性英文含义类比
原子性Atomicity全做或全不做转账:钱出了必须进账,不能半途而废
一致性Consistency事务前后数据合法账户总额不变
隔离性Isolation事务间互不干扰两人同时操作互不影响
持久性Durability提交后永久保存提交后断电也不丢失

事务隔离级别(从低到高)

隔离级别脏读不可重复读幻读性能
READ UNCOMMITTED可能可能可能最高
READ COMMITTED不会可能可能
REPEATABLE READ不会不会可能
SERIALIZABLE不会不会不会最低
  • MySQL默认:REPEATABLE READ
  • PostgreSQL默认:READ COMMITTED

实战SQL示例

-- ========== PostgreSQL环境示例 ==========

-- ========== 1. 基本事务操作 ==========
-- 场景:批量插入基因表达数据,失败则全部回滚
BEGIN;  -- 开始事务(或用 START TRANSACTION)

-- 插入样本信息
INSERT INTO samples (sample_id, patient_id, collection_date)
VALUES ('S100', 'P001', '2024-01-15');

-- 插入该样本的基因表达数据
INSERT INTO gene_expression (gene_id, sample_id, expression, condition)
VALUES 
    ('BRCA1', 'S100', 245.7, 'tumor'),
    ('TP53',  'S100', 89.3,  'tumor'),
    ('KRAS',  'S100', 412.1, 'tumor');

-- 更新样本处理状态
UPDATE samples SET status = 'processed' WHERE sample_id = 'S100';

COMMIT;  -- 全部成功则提交(永久生效)
-- 如果中间任何步骤失败,执行 ROLLBACK; 回滚所有操作

-- ========== 2. 保存点(SAVEPOINT):部分回滚 ==========
BEGIN;

INSERT INTO samples VALUES ('S101', 'P002', '2024-01-16');
SAVEPOINT after_sample;  -- 设置保存点

-- 尝试插入表达数据
INSERT INTO gene_expression VALUES ('INVALID_GENE', 'S101', -999, 'tumor');
-- 假设这步失败(基因ID不合法)

ROLLBACK TO SAVEPOINT after_sample;  -- 回滚到保存点(只撤销表达数据插入)
-- 样本信息插入仍然保留!

-- 插入正确的表达数据
INSERT INTO gene_expression VALUES ('EGFR', 'S101', 156.2, 'tumor');

COMMIT;  -- 提交(样本 + 正确的表达数据都保存)

-- ========== 3. 隔离级别设置 ==========
-- 查看当前隔离级别
SHOW transaction_isolation;  -- PostgreSQL
SELECT @@transaction_isolation;  -- MySQL

-- 设置本次事务的隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- ... 查询操作
COMMIT;

-- ========== 4. 锁机制示例 ==========
-- 行级锁:SELECT FOR UPDATE(悲观锁)
-- 场景:多个分析进程同时要处理样本,防止重复处理
BEGIN;

-- 锁定一个待处理的样本(其他进程遇到这行会等待)
SELECT sample_id, status 
FROM samples 
WHERE status = 'pending'
LIMIT 1
FOR UPDATE SKIP LOCKED;  -- SKIP LOCKED:跳过被锁的行,直接拿下一个未锁的
-- 这样多个工作进程可以并行处理不同样本,不会冲突

-- 更新状态
UPDATE samples SET status = 'processing' WHERE sample_id = 'S100';

COMMIT;  -- 释放锁

-- ========== 5. 乐观锁(用版本号实现)==========
-- 适合读多写少的场景,不加数据库锁
-- 表结构:analysis_jobs(job_id, config, version INT)

-- 第一步:读取数据和版本号
SELECT job_id, config, version FROM analysis_jobs WHERE job_id = 42;
-- 假设读到 version = 5

-- 第二步:更新时检查版本号(如果别人已修改,version就变了,更新失败)
UPDATE analysis_jobs
SET config = '{"threads": 8}',
    version = version + 1   -- 版本号+1
WHERE job_id = 42
  AND version = 5;          -- ★ 关键:只有version还是5才更新

-- 检查影响行数
-- 如果影响0行 → 说明version已被修改(其他人抢先了),需要重试
-- 如果影响1行 → 更新成功

-- ========== 6. 死锁处理 ==========
-- 死锁:进程A锁了表1等表2,进程B锁了表2等表1,互相等待
-- 数据库会自动检测死锁并回滚其中一个事务

-- 避免死锁的原则:所有事务按固定顺序加锁
-- ❌ 容易死锁:
-- 事务A: 锁samples → 锁gene_expression
-- 事务B: 锁gene_expression → 锁samples

-- ✅ 避免死锁:统一顺序
-- 所有事务: 先锁samples → 再锁gene_expression

-- PostgreSQL查看死锁日志
-- log_lock_waits = on (在postgresql.conf中配置)

-- ========== 7. 事务在Python中的使用 ==========
python_example = """
import psycopg2  # PostgreSQL Python驱动

conn = psycopg2.connect("dbname=biodb user=admin")
conn.autocommit = False  # 关闭自动提交(手动控制事务)

try:
    cur = conn.cursor()

    # 插入样本
    cur.execute(
        "INSERT INTO samples VALUES (%s, %s, %s)",
        ('S200', 'P003', '2024-01-17')
    )

    # 插入表达数据
    expression_data = [('BRCA1', 'S200', 312.5), ('TP53', 'S200', 78.9)]
    cur.executemany(
        "INSERT INTO gene_expression (gene_id, sample_id, expression) VALUES (%s, %s, %s)",
        expression_data
    )

    conn.commit()  # 提交事务
    print("数据插入成功!")

except Exception as e:
    conn.rollback()  # 任何错误都回滚
    print(f"错误,已回滚: {e}")

finally:
    conn.close()
"""
print(python_example)

面试常问点

  1. Q: 什么是脏读、不可重复读、幻读? A: 脏读=读到未提交的数据;不可重复读=同一事务两次读同一行得到不同值;幻读=同一事务两次查询返回不同行数(有新行被插入)。

  2. Q: MySQL的REPEATABLE READ怎么避免幻读? A: 通过MVCC(多版本并发控制)+间隙锁(Gap Lock)。读时用快照,写时用间隙锁防止其他事务在范围内插入新行。

  3. Q: 乐观锁和悲观锁的选择原则? A: 读多写少用乐观锁(版本号);写冲突频繁用悲观锁(SELECT FOR UPDATE);短事务用悲观锁,长事务用乐观锁。

  4. Q: 事务提交后数据是如何持久化的? A: 通过WAL(预写日志,Write-Ahead Logging):先写日志到磁盘,再修改数据文件。崩溃恢复时重放日志。


速查表

-- 事务控制
BEGIN / START TRANSACTION;  -- 开始
COMMIT;                      -- 提交
ROLLBACK;                    -- 回滚
SAVEPOINT sp_name;           -- 设置保存点
ROLLBACK TO SAVEPOINT sp_name; -- 回滚到保存点

-- 锁
SELECT ... FOR UPDATE;        -- 排他锁(写锁)
SELECT ... FOR SHARE;         -- 共享锁(读锁)
SELECT ... FOR UPDATE SKIP LOCKED; -- 跳过已锁行

-- 隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;