第 7 章

事务隔离级别实验

MySQL 事务隔离级别完全指南

理解事务隔离对于构建正确的并发应用程序至关重要。本指南探索所有四个隔离级别,包括异常演示、性能影响和何时使用每个级别。

1. ACID属性和隔离

1.1 事务基础

事务是一系列操作,要么全部成功(提交),要么全部失败(回滚)。


ACID属性:

A - 原子性
    所有操作都成功或全部失败
    中间状态永远不可见
    示例:银行转账(借记+贷记同时发生或都不发生)

C - 一致性
    数据库从一个有效状态移动到另一个有效状态
    所有约束都得到维护
    完整性规则得到强制执行

I - 隔离
    事务不相互干扰
    一个事务的更改对其他事务隐藏
    直到事务提交

D - 持久性
    一旦提交,数据在故障中存活
    持久化到磁盘
    即使服务器崩溃也不会丢失

事务生命周期:


开始事务
  ↓
执行语句
  ├─ SELECT(读取数据)
  ├─ INSERT/UPDATE/DELETE(修改数据)
  └─ ...(更多操作)
  ↓
决策点
  ├─ COMMIT(更改永久)
  └─ ROLLBACK(更改被丢弃)

2. 四个隔离级别

2.1 READ UNCOMMITTED (RU) —— 最宽松

允许读取由未提交事务编写的数据(脏读)。

脏读演示:


-- 会话1:更新而不提交
BEGIN;
UPDATE accounts SET balance = 50.00 WHERE id = 1;
-- 还没有提交

-- 会话2:读取未提交的更改
SELECT balance FROM accounts WHERE id = 1;
-- 结果:50.00(脏读 - 读取未提交的数据!)

-- 会话1:回滚事务
ROLLBACK;

-- 会话2:再次检查余额
SELECT balance FROM accounts WHERE id = 1;
-- 结果:100.00(值改变了 - 会话2读取了脏数据)

特点:

2.2 READ COMMITTED (RC) —— 中等隔离

只读取已提交的数据,但允许不可重复读和幻影读。

不可重复读演示:


-- 会话1:设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 结果:100.00

-- 会话2:修改并提交
BEGIN;
UPDATE accounts SET balance = 75.00 WHERE id = 1;
COMMIT;

-- 会话1:再次读取同一行
SELECT balance FROM accounts WHERE id = 1;
-- 结果:75.00(不同的值! - 不可重复读)

-- 会话1:回滚
ROLLBACK;

幻影读演示:


-- 会话1:计数账户
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT COUNT(*) FROM accounts;
-- 结果:1

-- 会话2:插入新行
BEGIN;
INSERT INTO accounts VALUES (2, 'Bob', 200.00);
COMMIT;

-- 会话1:在同一事务中再次计数
SELECT COUNT(*) FROM accounts;
-- 结果:2(幻影读 - 新行出现了!)

-- 会话1:回滚
ROLLBACK;

特点:

2.3 REPEATABLE READ (RR) —— MySQL默认

使用一致快照保证事务内的可重复读。

可重复读保证:


-- 会话1:启动事务
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 结果:100.00

-- 会话2:修改并提交
BEGIN;
UPDATE accounts SET balance = 75.00 WHERE id = 1;
COMMIT;

-- 会话1:再次读取同一行
SELECT balance FROM accounts WHERE id = 1;
-- 结果:100.00(一致快照 - 值不变!)

-- 会话1:回滚
ROLLBACK;

特点:

2.4 SERIALIZABLE —— 最严格

事务串行执行,就像一个接一个。

序列化事务:


-- 会话1:启动序列化事务
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT COUNT(*) FROM accounts;
-- 结果:1

-- 会话2:尝试插入
BEGIN;
INSERT INTO accounts VALUES (2, 'Bob', 200.00);
-- 阻塞!等待会话1释放锁

-- 会话1:提交
COMMIT;

-- 会话2:现在插入成功
-- 结果:插入完成

-- 所有操作串行执行,无冲突

特点:

3. 隔离级别比较表

隔离级别 脏读 不可重复读 幻影读 性能 使用场景
READ UNCOMMITTED 可能 可能 可能 最快 很少使用的分析
READ COMMITTED 防止 可能 可能 很好 通用(PostgreSQL默认)
REPEATABLE READ 防止 防止 大多防止* 很好 MySQL默认,大多数应用
SERIALIZABLE 防止 防止 防止 最慢 关键/财务系统很少

4. 实际应用场景

4.1 电子商务:订单处理


-- 高并发:每分钟数千个订单
-- 需要防止库存超售

-- 建议:REPEATABLE READ(MySQL默认)
-- 原因:防止不可重复读,但允许合理的并发

-- 事务:
BEGIN;  -- REPEATABLE READ

-- 检查库存
SELECT stock_quantity FROM inventory WHERE product_id = 1;
-- 假设结果是10

-- ...客户确认订单...

-- 扣减库存
UPDATE inventory SET stock_quantity = stock_quantity - 1
WHERE product_id = 1;

COMMIT;

4.2 财务转账:账户余额


-- 关键:必须确保没有丢失的更新
-- 使用 REPEATABLE READ + 显式锁定

BEGIN;

-- 锁定两个账户以安全转账
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
SELECT balance FROM accounts WHERE id = 2 FOR UPDATE;

-- 现在两行都被锁定,读取值
SET @from_balance = (SELECT balance FROM accounts WHERE id = 1);
SET @to_balance = (SELECT balance FROM accounts WHERE id = 2);

-- 检查资金是否充足
IF @from_balance >= 25.00 THEN
  UPDATE accounts SET balance = balance - 25.00 WHERE id = 1;
  UPDATE accounts SET balance = balance + 25.00 WHERE id = 2;
  COMMIT;
ELSE
  ROLLBACK;
END IF;

5. InnoDB锁类型


SHARED (S) LOCK
├─ 多个事务可以持有同一行的共享锁
├─ 没有事务可以在共享锁存在时持有排他锁
└─ 用于:SELECT ... LOCK IN SHARE MODE

EXCLUSIVE (X) LOCK
├─ 只有一个事务可以持有行的排他锁
├─ 其他锁(共享/排他)不能持有
└─ 用于:INSERT, UPDATE, DELETE, SELECT ... FOR UPDATE

GAP LOCKS
├─ 锁定索引记录之间的间隙(不是记录本身)
├─ 防止在间隙中插入
└─ 在REPEATABLE READ中防止幻影

NEXT-KEY LOCKS
├─ 记录锁+间隙锁的组合
├─ 锁定记录及其前面的间隙
└─ REPEATABLE READ中的默认值

查看当前锁:
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

SHOW ENGINE INNODB STATUS;
-- 查看LOCK WAIT部分

6. 死锁处理


死锁场景:

-- 事务A:
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
-- (持有账户1的锁,等待账户2的锁)
UPDATE accounts SET balance = balance + 50 WHERE id = 2;

-- 同时...
-- 事务B:
BEGIN;
UPDATE accounts SET balance = balance - 30 WHERE id = 2;
-- (持有账户2的锁,等待账户1的锁)
UPDATE accounts SET balance = balance + 30 WHERE id = 1;

-- 死锁!两个事务互相等待
-- ERROR 1213 (40001): Deadlock found when trying to get lock

死锁预防策略:

策略1:一致的锁定顺序
所有事务按同一顺序加锁:id_min先,id_max后

策略2:缩短事务
减少持锁时间

策略3:超时和重试
SET innodb_lock_wait_timeout = 5;  -- 5秒超时

应用代码:
try {
  START TRANSACTION;
  UPDATE orders SET status='shipped' WHERE id = order_id;
  COMMIT;
} catch (DeadlockException e) {
  // 重试最多3次
  sleep(random(1,3) seconds);
  retry();
}

7. 性能与一致性权衡

级别 并发性 一致性 锁等待可能性 最适合
READ UNCOMMITTED 优秀 近似分析
READ COMMITTED 非常好 大多数通用应用
REPEATABLE READ 非常好 中等 MySQL默认(推荐)
SERIALIZABLE 完美 非常高 很少需要

8. 最佳实践

9. 检查当前设置


-- 检查当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';

-- 或对于会话
SELECT @@transaction_isolation;
SELECT @@SESSION.transaction_isolation;

-- 更改当前会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 全局更改(影响新连接)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 检查其他相关设置
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';  -- 默认50秒
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';   -- 0/1/2
SHOW VARIABLES LIKE 'max_connections';             -- 影响并发

总结

MySQL事务隔离级别提供了一致性与性能的谱系:

大多数应用程序应该使用REPEATABLE READ,对关键部分使用显式锁定(FOR UPDATE)。死锁是正常的 —— 使用重试优雅地处理它们。如果竞争成为问题,监控锁等待并调整策略。

本章评分
4.6  / 5  (60 评分)

💬 留言讨论