一、为什么写这篇文章

表结构

-- code_shop.t_test_lock definition
CREATE TABLE `t_test_lock` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称',
  `age` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '年龄',
  `trans_id` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '可重复交易流水号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `t_test_lock_id_IDX` (`id`) USING BTREE,
  KEY `t_test_lock_trans_id_IDX` (`trans_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=45 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

示例数据

下面种类型的插入SQL为什么会在并发执行的出现死锁?

INSERT INTO t_test_lock (name, age, trans_id) 
select '1', '2', 'trans_id_0010'  from dual
where not exists (select 1 from t_test_lock  where trans_id = "trans_id_0010")

读完本文章你可以:

1. 把 ACID、隔离级别、锁类型、加锁算法四张知识网织成一张;
2. 预判一条 SQL 在不同隔离级别下到底会加什么锁、会不会死锁;
3. 遇到“幻读”“死锁”报警时,不再只会 `kill thread`,而是能定位、能复现、能改 SQL。

二、热身:ACID 与隔离级别

特性英文全称中文含义一句话说明MySQL/InnoDB 典型实现
AAtomicity原子性事务要么全部成功,要么全部回滚,不存在中间状态Redo/Undo Log:崩溃恢复时利用日志回滚或重做(后面再出文章详解)
CConsistency一致性事务执行前后,数据库始终满足所有定义好的约束与业务规则外键、触发器、唯一索引、MVCC 可见性判断等共同保证
IIsolation隔离性并发事务之间互不干扰,如同串行执行锁(行锁、间隙锁、Next-Key)+ MVCC 快照读
DDurability持久性事务一旦提交,数据永久保存,即使系统崩溃也不丢失Redo Log 刷盘 + 双写缓冲区(Doublewrite Buffer)(后面再出文章详解)
隔离级别脏读不可重复读幻读实现简述
读未提交 RU✅✅✅只加行级 X 锁,读完立即释放
读已提交 RC❌✅✅行级 X/S 锁,事务提交后释放;无 Gap Lock
可重复读 RR(InnoDB 默认)❌❌部分场景 ❌Next-Key Lock 解决当前读幻读
序列化 Serializable❌❌❌所有 SELECT 隐式 LOCK IN SHARE MODE,退化成表锁

幻读 vs 不可重复读:
不可重复读强调“同一行被修改两次读结果不同”;
幻读强调“同一范围被插入/删除两次读结果不同”。


可重复读 RR(InnoDB 默认)为什么部分解决幻读:

场景是否幻读原因
普通 SELECT(快照读)可能读历史版本,不锁间隙
SELECT … FOR UPDATE / LOCK IN SHARE MODE(当前读)不会Next-Key Lock 锁住区间,挡插入
UPDATE / DELETE 范围语句(当前写)不会同上
唯一索引等值查询不会唯一性保证,无需间隙锁也安全

三、锁家族一览

  1. 共享锁 S:允许其他事务再读,不允许写。
  2. 排他锁 X:读写都不允许。
  3. 意向共享锁 IS:事务想在某些行加 S 锁,先在表上加 IS。
  4. 意向排他锁 IX:同理,想在某些行加 X 锁。
  5. 表级锁:LOCK TABLES … READ/WRITE 才会真正锁整张表;日常 UPDATE/DELETE 只锁行。
  6. 意向锁目的:让表锁与行锁快速互斥判断,避免逐行扫描。

四、加锁算法三维图

算法锁定范围是否解决幻读典型 SQL
Record Lock(RL)单行索引记录❌RC 下 WHERE id=1 FOR UPDATE
Gap Lock(GL)开区间间隙✅ 挡住插入RR 下 WHERE num=250(值不存在)
Next-Key Lock(NK)行 + 左间隙✅ 当前读完全防幻读RR 下 WHERE num>200

锁最终落在索引上;
无索引时退化成全表 Next-Key Lock,看起来就像表锁,但概念不同。


五、实验表与数据

CREATE TABLE t(
  id INT PRIMARY KEY,
  name VARCHAR(10),
  num INT,
  KEY idx_num(num)      -- 普通索引
);
INSERT INTO t VALUES
(1,'aaa',100),
(2,'bbb',200),
(3,'bbb',300),
(7,'ccc',200);

间隙划分(按 num 索引排序):
(∞,100) (100,200) (200,300) (300,+∞)


六、RC / RU 隔离级别加锁实测

场景是否加锁锁类型锁住的索引项备注
SELECT * FROM t WHERE num=200否——快照读
SELECT … WHERE num=200 LOCK IN SHARE MODE是S 行锁num=200 的两条记录(pId=2,7)当前读
SELECT … WHERE num=200 FOR UPDATE是X 行锁同上同上
SELECT … WHERE num>200 FOR UPDATE是X 行锁num=300(pId=3)无 Gap Lock

RC 只要命中行就加单行 Record Lock,无间隙;
若条件列无索引,会先锁全表再逐行释放不符合条件的锁,成本极高——线上禁止。


七、RR / Serializable 隔离级别加锁实测

场景锁类型锁住的索引项Gap Lock 范围备注
SELECT … WHERE num=200 LOCK IN SHARE MODES + NKnum=200 两条(100,200] (200,300]挡插入 num∈(100,300)
SELECT … WHERE num=250 FOR UPDATE(值不存在)X Gap Lock—(200,300)只锁间隙,挡插入 250
SELECT … WHERE num>400 FOR UPDATE(无结果)X Gap Lock—(400,+∞)挡超大值插入
唯一索引等值查询S/X Record Lock唯一索引 + 聚簇索引无 Gap自动降级为行锁

Serializable 下所有普通 SELECT 也会隐式加 LOCK IN SHARE MODE,快照读消失,直接退化成当前读 + Next-Key Lock,并发最低。


八、死锁案例与排查

经典场景:
A 事务:UPDATE t SET num=250 WHERE id=2;
B 事务:UPDATE t SET num=250 WHERE id=7;
A 接着:SELECT * FROM t WHERE num=250 FOR UPDATE;
B 接着:同样的 SQL
→ 双方各自持有 id=2、7 的 X 行锁,又等待对方在 (200,300) 间隙的插入锁,循环等待→死锁。

排查三板斧:

  1. SHOW ENGINE INNODB STATUS\G 看 LATEST DETECTED DEADLOCK;
  2. 开 innodb_print_all_deadlocks=1 持续打印;
  3. 用 performance_schema.data_locks 看事务持有/等待的锁模式。

规避原则:

  • 更新必须走索引,减少锁范围;
  • 统一事务顺序(先 A 表再 B 表);
  • 避免长事务,尽快提交/回滚;
  • 不允许更新唯一索引列(业务层幂等校验)。

九、MVCC:快照读的魔法

InnoDB 每行隐藏两个字段:

  • trx_id:最后一次修改该行的事务 ID;
  • roll_pointer:指向 undo 段,形成版本链。

每个事务启动时拿到一份活跃事务数组,用于判断“对我可见的版本”。
因此快照读无需加锁,读写互不阻塞,大幅提升并发。
但当前读(FOR UPDATE/LOCK IN SHARE MODE)必须加锁,以保证写入安全。


十、一张脑图总结(文字版)

ACID
└─ 隔离级别
   ├─ RU(无Gap)
   ├─ RC(无Gap)
   ├─ RR(Next-Key)
   └─ Serializable(全表NK)
锁类型
├─ S / X
├─ IS / IX(表级意向)
└─ 表锁(LOCK TABLES)
加锁算法
├─ Record Lock
├─ Gap Lock
└─ Next-Key Lock
MVCC
└─ 快照读(无锁)
   └─ 当前读(S/X + NK)

十一、快问快答

Q1:RC 下 WHERE num=200 FOR UPDATE 锁几行?
A:num 索引上两行,聚簇索引对应两行,共** 4 个 Record Lock**,无 Gap。

Q2:RR 下 WHERE num=250 FOR UPDATE 锁几行?
A:250 不存在,只在 (200,300) 加 Gap Lock,锁** 0 行**但挡插入。

Q3:为什么唯一索引等值查询不加 Gap Lock?
A:唯一性保证不可能插入重复值,无需挡间隙,自动退化为 Record Lock。


十二、写在最后:思维导图

附思维导图一张:
【腾讯文档】MYSQL锁
https://docs.qq.com/mind/DU1ViV3FuWVNRWnpo?mode=mind

更多推荐