第 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;
特点:
- 防止脏读(只读取已提交的数据)
- 允许不可重复读
- 允许幻影读
- 许多数据库的默认值(PostgreSQL、SQL Server)
- 读取后释放行锁
- 很好地平衡并发性和一致性
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;
特点:
- 使用一致读取视图(快照)
- 从事务开始点读取
- 防止不可重复读
- 主要防止幻影读(但自我INSERT被允许)
- 使用索引上的间隙锁进行写谓词
- MySQL中的默认隔离级别
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:现在插入成功
-- 结果:插入完成
-- 所有操作串行执行,无冲突
特点:
- 防止所有异常(脏、不可重复、幻影读)
- 实现真正的可序列化
- 由于锁定导致的显著性能成本
- 多用户系统中的高竞争
- 很少使用 - 通常REPEATABLE READ就足够了
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. 最佳实践
- 使用MySQL的默认值(REPEATABLE READ) —— 设计良好且性能出色
- 使用显式锁定(FOR UPDATE)进行关键部分 —— 当您需要确保特定行为时
- 保持事务简短 —— 减少锁定保持时间
- 以一致的顺序锁定资源 —— 防止死锁
- 为死锁使用应用程序级重试 —— 暂时冲突是正常的
- 监控长时间运行的事务 —— 它们阻塞其他
- 避免SERIALIZABLE,除非绝对必要 —— 严重的性能影响
- 测试并发场景 —— 隔离问题在负载下出现
- 设置适当的innodb_lock_wait_timeout —— 平衡等待和快速失败
- 对于读密集型分析使用READ COMMITTED —— 良好的性能,隔离损失不多
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事务隔离级别提供了一致性与性能的谱系:
- **READ UNCOMMITTED:**避免 —— 风险太大
- **READ COMMITTED:**用于高并发分析
- **REPEATABLE READ:**MySQL的默认值 —— 用于大多数应用
- **SERIALIZABLE:**很少需要 —— 极端的性能成本
大多数应用程序应该使用REPEATABLE READ,对关键部分使用显式锁定(FOR UPDATE)。死锁是正常的 —— 使用重试优雅地处理它们。如果竞争成为问题,监控锁等待并调整策略。