MySQL 锁
锁加在索引记录上
并发事务访问同一资源时,锁用来排队,避免写写互相覆盖。InnoDB 的行锁挂在索引记录上,不是挂在物理行号上。WHERE 没有走索引,可能锁住大量记录,看起来就像锁表。
快照读(普通 SELECT)几乎不加行锁;当前读才加记录锁、间隙锁或 Next-Key。不要背「RR = 共享锁拿到事务结束」——那不是 InnoDB 默认一致性读的行为。
隔离级别决定锁的范围和持有时间:RR 的间隙锁更强,用来堵幻读;RC 对间隙更宽松,并发插入更好,幻读风险也更高。
粒度、模式和意向锁
| 表锁 | 行锁 | 页锁(少见) | |
|---|---|---|---|
| 开销 | 小 | 大 | 中 |
| 加锁速度 | 快 | 慢 | 中 |
| 死锁 | 基本不会 | 会 | 可能 |
| 并发 | 低 | 高 | 中 |
InnoDB 默认行锁。DDL、显式 LOCK TABLES 仍会用表锁。MyISAM 基本是表锁。
按模式:
- 共享锁 S:读。兼容其他 S,不兼容 X
- 排他锁 X:写。和其他 S / X 都不兼容
给整张表加 X 锁时,如果要扫每一行有没有行锁,代价过高。InnoDB 用表级意向锁先打标:
- IS:打算给某些行加 S,先拿表 IS
- IX:打算给某些行加 X,先拿表 IX
要锁整张表时,看表上有没有冲突的意向锁即可,不必遍历行。
Record、Gap 与 Next-Key
三种行锁:
- Record Lock:锁住已有索引记录
- Gap Lock:锁住记录之间的间隙,阻止别人往这个间隙插入
- Next-Key Lock:Record + 前面的 Gap,RR 当前读的默认形态
插入意向锁让多个事务往不同间隙插入可以并行;往同一被 gap 锁住的间隙插入则等待。
RR 下:
- 快照读靠 MVCC,不靠间隙锁
- 当前读靠 Next-Key 减少幻读
- 等值查询走唯一索引时,会优化成 Record Lock,不必锁间隙
排查用 SHOW ENGINE INNODB STATUS、performance_schema.data_locks。等锁超时由 innodb_lock_wait_timeout 控制,默认 50 秒。
乐观锁和悲观锁
悲观锁假定会发生冲突:SELECT ... FOR UPDATE,或把隔离级别提到 SERIALIZABLE。冲突多、不能接受覆盖写时用。
乐观锁先改,提交时再检测。常见是表上加 version:
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 3;影响行数为 0,说明被别人改过,业务重试。CAS 是同一思路。读多写少、冲突少时更合适。
死锁:检测后回滚,不是把超时调大
两个以上事务互相等对方持有的锁,就是死锁。InnoDB 用等待图检测,牺牲其中一个(通常 undo 更少的)回滚,错误号 1213。MyISAM 一次性拿齐表锁,几乎没有死锁。
减少死锁:
- 多表、多行都按固定顺序访问
- 事务尽量短,不要在事务里做 RPC
- 要改就直接加 X 锁,不要先 S 再升级成 X
- 条件走索引,锁范围更准
- 能接受的话降低隔离级别,减少 gap lock
- 批量更新先在应用侧按主键排好序
死锁发生后看最新死锁日志,调整 SQL 顺序。把 innodb_lock_wait_timeout 调大只是让会话等得更久,解决不了循环等待。