)
在前面的章节中我们提到了MVCC多版本并发控制它巧妙地通过“版本快照”解决了“读-写”冲突实现了非阻塞读。但如果两个事务同时执行 UPDATE 操作修改同一行数据即写-写Write-Write场景快照就没用了。这时候MySQL 必须亮出它的铁腕手段——锁机制。一、写-写冲突的核心问题更新丢失想象一下这个场景初始状态账户余额 100 元。事务 A取出 100 元准备扣款。它先读到 100然后执行 100 - 100 0。事务 B同一时刻取出 50 元也先读到 100执行 100 - 50 50。结果如果事务 A 先提交事务 B 后提交B 的结果50元会覆盖 A 的结果0元。银行莫名其妙亏了 50 元。这种现象就叫“更新丢失”。二、MySQL 的解决方案排他锁X锁为了防止更新丢失InnoDB 存储引擎采用了行级锁Row-level Locking。当一个事务准备修改UPDATE/DELETE一条记录时它会先尝试获取该行的排他锁Exclusive Lock简称 X 锁。如果事务 A 拿到了锁它就可以进行修改。如果事务 B 也想修改发现 X 锁已被 A 占用事务 B 必须进入阻塞等待状态直到事务 A 提交或回滚释放了锁。结论在“写-写”场景下事务是串行化执行的。只有拿到锁的事务才能操作。三、更新丢失的两个分类在数据库理论中更新丢失分为两类。1.第一类更新丢失回滚丢失一个事务的回滚把另一个已经提交的事务更新的数据给覆盖了。在 InnoDB 中这种情况绝对不会发生。因为UPDATE会加排他锁X 锁事务 B 在事务 A 回滚之前根本无法修改该行数据。2.第二类更新丢失覆盖丢失一个事务基于旧数据计算新值并提交覆盖了另一个事务已经提交的更新。这是最常见的更新丢失。虽然数据库有锁但如果程序逻辑是“先读出来在内存计算再写回去”锁也救不了。四、四大隔离级别下的写-写场景在 InnoDB 中为了防止“脏写Dirty Write”所有的隔离级别在修改数据时都会加锁。也就是说如果事务 A 正在修改某行事务 B 想改同一行必须得等 A 提交或回滚。1. 读未提交RU 读提交RC在这两个级别下写-写冲突的表现最直接锁定单行只要事务 A 执行了 UPDATE该行就会被加上记录锁Record Lock。事务 B 的表现必须阻塞等待直到 A 释放锁。区别在这两个级别下MySQL 基本只锁住被修改的那些行。2. 可重复读RR/串行化Serializable作为 MySQL 的默认级别RR 在写操作上比 RC “霸道”得多。间隙锁Gap Lock与 Next-Key LockRR 不仅锁住存在的记录还会锁住记录之间的“间隙”。写-写冲突升级在 RC 下如果事务 A 修改了 ID10 的行事务 B 还可以插入 ID11 的新行。在 RR 下如果事务 A 的写操作涉及范围比如 WHERE id 5它会把整个范围都锁住。此时事务 B 想插入 ID11 的记录也会被阻塞。目的这是为了从根本上解决“幻读”问题确保写操作的区间安全。而串行化全线加锁读也不让读。以上两种的区别在于RU/RC无法解决幻读而RR/Serializable可以。比如select * from Roles whereid 5;RU/RC下仍可以插入而RR/Serializable禁止插入杜绝了两次搜索不一样的情况。五、写-写死锁的情况1.典型死锁场景转账假设有用户 1 和用户 2两人同时互相转账。死锁产生。2.解决方案在代码层进行“ID 排序”无论谁给谁转账我们的业务逻辑都强制要求先锁 ID 小的再锁 ID 大的。这样当事务 A 和事务 B 同时发生时它们都会先去争抢 id1 的锁。谁抢到了谁先走没抢到的就在第一行等着而不会去占着第二行的锁。这就变“环路等待”为“顺序排队”了。修改后的逻辑接收到转账请求 (id11, id22)。排序发现 1 2。执行 UPDATE ... WHERE id 1;执行 UPDATE ... WHERE id 2;