🔒 锁机制
全局锁/表锁/行锁 · 共享锁/排他锁/意向锁 · 间隙锁与临键锁 · 死锁排查
1. MySQL 的锁有哪些分类?
按粒度:
- 全局锁:FLUSH TABLES WITH READ LOCK,整个库只读。用于全库备份(一致性快照)。代价:业务停摆
- 表级锁:
- 表锁(LOCK TABLES):整表加锁,InnoDB 下少用
- 元数据锁(MDL):DDL/DML 自动加,防止 DDL 改表时 DML 出错。注意:长事务持有 MDL 读锁会阻塞 DDL(DDL 需要写锁);若此时有 DDL 正在等待,后续 DML 也会被排队堵住
- 意向锁(IS/IX):加行锁前自动加表级意向锁,让"表锁与行锁共存"的判断无需扫描行
- 自增锁(AUTO-INC):自增主键的并发分配
- 行级锁(InnoDB 才有):
- 共享锁 S:可并发读,排斥写(SELECT ... LOCK IN SHARE MODE)
- 排他锁 X:读写都排斥(UPDATE/DELETE/INSERT 自动加,SELECT FOR UPDATE 显式加)
- 记录锁(Record Lock):锁单条记录
- 间隙锁(Gap Lock):锁记录之间的间隙,防插入(防幻读)
- 临键锁(Next-Key Lock):记录锁 + 前向间隙锁,默认 RR 下的行锁形态
🎯 面试要点
- 锁的兼容:S 与 S 兼容,S 与 X 互斥,X 与 X 互斥
- 意向锁的意义:判断"能否加表锁"不用遍历所有行
- 行锁是基于索引实现的:条件没走索引 → 行锁退化成表锁(全表加锁)——高频考点
2. 悲观锁和乐观锁?各自实现?
- 悲观锁:默认会冲突,操作前先加锁。数据库实现:SELECT ... FOR UPDATE(事务内持锁直到提交)。适用:写冲突频繁、库存扣减
- 乐观锁:默认不冲突,提交时校验。实现:
version字段(或更新时间戳)——UPDATE ... WHERE version = #{old},影响行数为 0 则重试。CAS 思想
乐观锁扣库存(经典)
-- 表 stock(id, goods_id, count, version)
-- 1. 读版本号
SELECT count, version FROM stock WHERE goods_id = 100; -- count=10, version=3
-- 2. 条件更新,version 校验
UPDATE stock
SET count = count - 1, version = version + 1
WHERE goods_id = 100 AND version = 3; -- 影响 0 行 → 冲突,重试
🎯 面试要点
- 乐观锁在高竞争下重试成本高;悲观锁在低竞争下锁开销浪费
- update count = count - 1 WHERE count > 0 也是无锁扣减技巧(数据库行锁保证原子)
- FOR UPDATE 要配合索引 + 事务;悲观锁在分布式下要换分布式锁(Redis)
3. 死锁如何产生?怎么排查和避免?
死锁:两个事务互相持有对方需要的锁,互不相让。InnoDB 自动检测死锁(等待图),选择回滚代价小的事务(undo 少者),并抛 Deadlock found when trying to get lock(1213 错误)。
排查:
SHOW ENGINE INNODB STATUS看 LATEST DETECTED DEADLOCK 段(两事务持锁/等锁的 SQL)- 开启死锁日志:innodb_print_all_deadlocks=ON(记录所有死锁到错误日志)
避免:
- 所有事务按固定顺序访问资源(如先锁 id 小者)
- 尽量缩小事务范围(短事务少持锁)
- 避免锁升级为表锁(保证条件走索引)
- 业务层重试机制(死锁是常态,重试 2-3 次)
🎯 面试要点
- MySQL 死锁会主动检测并牺牲一个事务,所以应用要能容忍 1213 错误重试
- 死锁四条件(互斥/持有等待/不可剥夺/循环等待)——与 JVM 死锁同一套理论
- 批量更新注意排序(ORDER BY id),避免不同事务更新顺序不一致