脏读、不可重复读、幻读到底怎么触发?
这三个问题不是理论概念,而是真实 SQL 执行顺序下必然出现的现象。关键在于:**事务 A 读取时,事务 B 正在做什么、是否已提交、操作的是同一行还是新插入的行**。
脏读:事务 A 执行SELECT时,事务 B 刚
UPDATE了一行但还没
COMMIT;A 读到了这行“未定稿”,B 后续
ROLLBACK→ A 的读取结果就失效了。 不可重复读:事务 A 第一次
SELECT id=100得到
name='Alice';事务 B 提交了
UPDATE users SET name='Bob' WHERE id=100;A 再次
SELECT id=100,得到
name='Bob'—— 同一行值变了。 幻读:事务 A 执行
SELECT * FROM orders WHERE status='pending'返回 3 条;事务 B 插入一条新
status='pending'记录并
COMMIT;A 再次执行相同查询,返回 4 条 —— 行数变多了,像“幻影”。注意:这不是更新同一行,而是满足条件的新行被插入。
MySQL 默认的 REPEATABLE READ 真的能防住所有问题吗?
不能。MySQL InnoDB 的默认隔离级别是
REPEATABLE READ,它靠 MVCC + Next-Key Lock(临键锁)实现,能防脏读和不可重复读,但对幻读只做“部分防护”——仅针对普通
SELECT(快照读)有效;一旦用了当前读(如
SELECT ... FOR UPDATE、
UPDATE、
DELETE),幻读仍可能发生,尤其在范围条件上。 例如:事务 A 执行
SELECT * FROM t WHERE c > 10 FOR UPDATE锁住满足条件的记录和间隙;事务 B 尝试插入
c=15会被阻塞 → 这是幻读被锁挡住。 但如果事务 B 插入的是
c=5(不在 A 的查询范围内),或 A 没加锁直接
SELECT(快照读),那 B 的插入就能成功,A 再查就会“看到幻影”。 真正彻底解决幻读,只有
SERIALIZABLE隔离级别,但它会让所有并发
SELECT变成串行,线上基本不用。
写-写冲突:为什么两个 UPDATE 会互相卡住?
当两个事务同时想改同一行,InnoDB 必须用排他锁(X 锁)互斥。谁先拿到锁谁先改,后到的只能等 —— 这就是锁等待。如果等待超时(默认
innodb_lock_wait_timeout = 50秒),会报错:
Lock wait timeout exceeded; try restarting transaction。 典型场景:秒杀扣库存,多个请求同时执行
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock > 0。 陷阱:即使 SQL 带了
AND stock > 0条件,InnoDB 仍会对匹配的索引记录(甚至间隙)加 X 锁,后续请求必须排队。 优化方向不是“去掉锁”,而是缩短锁持有时间:确保该语句走索引、避免大事务、减少其他无关 SQL 在同一事务中。
读-写并发下,MVCC 是怎么悄悄帮你躲开锁的?
MVCC 不是魔法,它是通过给每行数据维护多个版本(由
DB_TRX_ID标记),配合事务启动时生成的
ReadView,让普通
SELECT读取“快照”,而不是最新行 —— 所以读不加锁,也不阻塞写。 关键点:
REPEATABLE READ下,事务第一次
SELECT生成
ReadView,之后所有快照读都复用它;
READ COMMITTED则每次
SELECT都新建
ReadView,所以能看到其他事务已提交的修改(即允许不可重复读)。 注意:
SELECT ... LOCK IN SHARE MODE或
FOR UPDATE是当前读,绕过 MVCC,直接加 S/X 锁,会阻塞其他写操作。 一个易忽略的事实:MVCC 只解决读-写冲突,对写-写冲突完全不管 —— 两个
UPDATE依然要抢锁,哪怕它们读的是不同快照。
最常被低估的一点:并发问题从来不是孤立存在的。比如你调高了隔离级别防幻读,却没意识到它会让更多语句升级为当前读,从而增加锁竞争;又比如你依赖 MVCC 实现无锁读,却在事务里混进了
SELECT ... FOR UPDATE,瞬间打破快照一致性。真正的并发控制,是隔离级别、锁策略、SQL 写法、应用重试逻辑四者咬合的结果,缺一不可。
