在 MySQL 的默认事务隔离级别(可重复读,REPEAT READ)下,事务 A 和事务 B 对同一行数据的操作时会产生什么呢?
一、场景还原
时间线
事务 A
事务 B
1
BEGIN;
2
SELECT * FROM t WHERE id=1;
3
UPDATE t SET col=100 WHERE id=1;
4
BEGIN;
5
SELECT * FROM t WHERE id=1;
6
UPDATE t SET col=200 WHERE id=1;
7
COMMIT; 或 ROLLBACK;
8
COMMIT;
二、关键分析
1. 事务 A 未提交时
事务 B 的 SELECT 行为:
在 可重复读 隔离级别下,事务 B 的 SELECT 会读取到事务开始时的快照数据(事务 A 的修改不可见)。
如果事务 B 使用 SELECT ... FOR UPDATE,则会尝试获取 行锁,此时会被阻塞,直到事务 A 提交或回滚。
事务 B 的 UPDATE 行为:
无论事务 B 的 SELECT 是否使用 FOR UPDATE,执行 UPDATE 时都会尝试获取 行锁。
如果事务 A 未提交,事务 B 的 UPDATE 会被阻塞,直到事务 A 释放锁(提交或回滚)。
2. 事务 A 提交后
事务 B 的 UPDATE:
如果事务 A 已提交,事务 B 的 UPDATE 将基于 事务 A 提交后的最新数据 进行修改。
示例:
事务 A 将 col 从 100 改为 200 并提交。
事务 B 的 UPDATE 会覆盖为 200 → 新值。
3. 事务 A 回滚后
事务 B 的 UPDATE:
如果事务 A 回滚,事务 B 的 UPDATE 将基于 原始数据 进行修改。
示例:
事务 A 的修改被撤销(col 恢复为原值)。
事务 B 的 UPDATE 基于原值修改。
三、可能结果
场景 1:事务 A 提交前,事务 B 尝试更新
事务 A 状态
事务 B 结果
未提交
事务 B 的 UPDATE 被阻塞,直到事务 A 提交或回滚(或等待锁超时)
提交
事务 B 获取锁,基于事务 A 提交后的数据更新,最终提交成功
回滚
事务 B 获取锁,基于原始数据更新,最终提交成功
场景 2:事务 A 提交后,事务 B 更新
事务 B 的 UPDATE 总能成功,但可能覆盖事务 A 的修改(需注意 丢失更新 问题)。
四、锁机制详解
锁类型
事务 A
事务 B
共享锁 (S)
SELECT ... LOCK IN SHARE MODE
允许其他事务读,但禁止写
排他锁 (X)
UPDATE/DELETE
禁止其他事务读写
事务 A 的 UPDATE 会持有排他锁,直到事务结束。
事务 B 的 UPDATE 必须等待排他锁释放。
五、隔离级别影响
隔离级别
事务 B 的 SELECT 结果
事务 B 的 UPDATE 行为
读未提交
看到事务 A 未提交的修改
可能基于脏数据更新
读已提交
看不到事务 A 未提交的修改
等待锁释放后更新最新数据
可重复读
看不到事务 A 未提交的修改
等待锁释放后更新最新数据
串行化
事务 B 的 SELECT 会直接被阻塞
严格串行执行
六、解决方案
1. 避免丢失更新
乐观锁:
UPDATE t SET col =200, version = version +1WHERE id =1AND version =<查询时的版本号>;
悲观锁:
SELECT*FROM t WHERE id =1FORUPDATE;-- 在事务中锁定行,再执行更新
2. 设置锁等待超时
SET innodb_lock_wait_timeout =30;-- 单位:秒
七、验证实验
步骤 1:开启两个 MySQL 客户端
-- 客户端 ABEGIN;SELECT*FROM t WHERE id =1;UPDATE t SET col =100WHERE id =1;-- 客户端 BBEGIN;SELECT*FROM t WHERE id =1;-- 可重复读下看不到 A 的修改UPDATE t SET col =200WHERE id =1;-- 被阻塞