4.2 锁机制:锁解决事务问题
了解了有哪些锁之后,我们需要知道在极其复杂的并发争抢场景下,InnoDB 是如何通过精准的锁算法来解决事务错乱甚至幻读的。
一、 并发事务引发的四大问题
不管锁设计得多么花里胡哨,都是为了消灭下述“四大毒瘤”的一种或几种:
- 脏读 (Dirty Read):
- 发生什么:你读到了别人事务刚刚修改但还未提交决议的数据。万一他回滚了,你读到的就是根本不存在的假象。
- 解药:隔离级别提升。
- 不可重复读 (Non-repeatable Read):
- 发生什么:你在一个事务内前后两次读同一个东西,结果发现值变了(中间被别人 Update 掉并提交了)。
- 解药:引入 MVCC,锁定视图快照版本。
- 幻读 (Phantom Read):
- 发生什么:你根据某个条件查出一批数据,结果再次查询时,仿佛见鬼一样,多出了几行或者少了几行(被别人 Insert/Delete 掉并提交了)。
- 解药:今天的主角——间隙锁与 Next-Key Lock 机制。
- 丢失修改 (Lost Update):
- 发生什么:经典并发冲突,两个人同时读取存款余额 100 块,都扣了 50,最后一个人写回去 50 把前一人的操作抹杀了。
- 解药:依靠悲观锁(强制排他)或乐观锁(应用层版本号比对)。
二、 InnoDB 行锁的三大算法门派
不要以为行级锁就真的是只拿个链子把某一行锁死。基于处理精度的要求,它演化出了三种算法:
1. Record Lock (记录锁)
- 含义:原教旨主义的行锁,稳稳当当仅仅锁住单条索引记录。
- 场景:当你使用主键或唯一索引等极高区分度的条件进行精准等值查询(如
WHERE id = 1)时生效。
2. Gap Lock (间隙锁)
- 含义:它非常特殊,不锁定任何实际数据记录,而是锁定两条索引记录之间的虚拟“间隙”。
- 目的:它是彻底根治幻读的终极武器。锁住了间隙,意味着这之间完全变成了一座无法跨越的墙,任何事务都别想往这个空挡里面
INSERT新的幻影数据。
3. Next-Key Lock (临键锁)
- 含义:它是行锁的默认终极形态,也就是 Record Lock + Gap Lock 的防弹装甲。
- 机制:不仅要把那条索引记录本身锁死,还要把它之前的空隙全部封禁。它构成了“左开右闭”的严密封锁区间,使得其他事务既不能修改这条记录,也不能尝试在它附近安插眼线。
三、 对抗“丢失修改”的实战心法
其实在任何隔离级别下,都不会导致数据库理论上的丢失修改问题,因为有行级锁:
- 事务 A 执行 UPDATE t SET age = 20 WHERE id = 1。InnoDB 立刻给 id=1 这行数据加上行级 X 锁。
- 这时,不管你的数据库是在 RU、RC 还是 RR 隔离级别,只要 事务 B 也来执行 UPDATE t SET age = 30 WHERE id = 1 或 DELETE 这行数据。
- 事务 B 尝试获取 id=1 的 X锁时,会发现锁正被事务所 A 拿着,于是事务 B 必须卡住。
- 直到事务 A 提交(Commit)或回滚(Rollback)释放了 X 锁,事务 B 才能拿到锁去执行属于它的修改。
而实际上,当用户执行查询再修改时,可能会导致这个问题:
- 事务A:SELECT balance 查出是 100;(快照读,不加锁)
- 事务B:SELECT balance 查出是 100;(快照读,不加锁)
- 事务A在代码里计算 100 - 10 = 90,然后 UPDATE set balance = 90(加X锁并快速执行成功);
- 事务B在代码里计算 100 - 20 = 80,然后 UPDATE set balance = 80(事务A刚完事,B也拿到X锁执行成功)。 结果:A扣款10块钱这一事实,被B覆盖了!
如何解决:
-
悲观策略 (SELECT ... FOR UPDATE) 从查询的那一刻起,直接对这行数据挂上排他锁(X锁)。虽然这会让所有后续也想读写的兄弟事务立刻进入阻塞死等状态(影响吞吐量),但防范成功率 100%。
-
乐观策略 (版本控制 / CAS) 相信这个世界没那么糟糕,不去数据库层面抢锁。而在业务表里自己多加一列
version_int(或时间戳)。 每次读取带走这个version,更新时附带条件:UPDATE table SET balance = balance - 50, version = 2 WHERE id = 1 AND version = 1。如果受影响行数为 0,说明在你犹豫的时候有人捷足先登了,你在业务层处理回滚重试即可。极强的高并发解法。