前言
MySQL 锁是并发场景下数据库控制资源竞争的核心机制。很多开发写 SQL 的时候,经常遇到事务卡住、更新超时、死锁报错,但是不知道根源是锁。 很多人混淆 MyISAM 和 InnoDB 锁差异,分不清行锁、表锁、意向锁,尤其对临键锁(Next-Key Lock)和死锁理解模糊。
核心前提:
- MyISAM:只支持表级锁,不支持事务,不支持行锁;读锁共享,写锁互斥。
- InnoDB:支持事务,支持行级锁 + 表级意向锁,是我们生产环境主流引擎。 InnoDB 锁是基于索引实现!不走索引,行锁会升级为表锁(坑点)。
一、锁粒度:表锁 vs 行锁
1.1 MyISAM 表锁(表级锁)
对整张表加锁。分为共享读锁(S 锁)、排他写锁(X 锁)
- 读锁(S):多个会话可以同时加读锁;读的时候阻塞写
- 写锁(X):写锁独占;写阻塞读、写阻塞写
特点:开销小,加锁快;无死锁 锁粒度大,并发差。只要一条记录写操作,整张表被锁住。
适用:只读报表、静态数据,生产业务几乎不用 MyISAM。
1.2 InnoDB 行锁(行级锁)
锁粒度是单行记录,只锁定访问的那一行。 同样分为:
共享锁 S(读锁):多个事务可以同时加 S 锁;S 和 S 兼容,S 与 X 互斥
select * from user where id = 1 lock in share mode; -- 手动加共享锁排他锁 X(写锁):事务修改数据自动加 X 锁;X 锁和任何锁都互斥
select * from user where id = 1 for update; -- 手动加排他锁 update user set name='xxx' where id=1; -- update/delete自动加X锁优点:粒度小,并发高,只锁目标行
缺点:加锁开销大,会产生死锁
重点坑:行锁是基于索引实现!如果 where 条件没有命中索引,InnoDB 无法定位行,会退化成表锁!
1.3 InnoDB 意向锁(Intention Lock)【高频】
意向锁属于表级锁,意向锁之间互相兼容;意向锁和真实 S/X 锁互斥。 分为:
- 意向共享锁 IS:事务打算给表里面某行加 S 锁,先给表加 IS 锁
- 意向排他锁 IX:事务打算给表里面某行加 X 锁,先给表加 IX 锁
意向锁作用:提升表锁判断效率如果没有意向锁,当事务想要加表锁时,需要遍历表中所有行,判断是否有行锁; 有了意向锁:只需要判断表上有没有意向锁,快速判断能不能加表锁。
意向锁兼容矩阵:
| S(表共享) | X(表排他) | IS | IX | |
|---|---|---|---|---|
| S | 兼容 | 互斥 | 兼容 | 互斥 |
| X | 互斥 | 互斥 | 互斥 | 互斥 |
| IS | 兼容 | 互斥 | 兼容 | 兼容 |
| IX | 互斥 | 互斥 | 兼容 | 兼容 |
意向锁不需要手动操作,InnoDB 自动维护。
二、InnoDB 行锁细分:记录锁、间隙锁、临键锁(Next-Key Lock)
InnoDB 默认隔离级别:RR(可重复读),为了解决幻读问题,使用临键锁。
RC(读已提交)隔离级别,没有间隙锁,只有记录锁,但会出现幻读。
2.1 记录锁 Record Lock
锁定索引中具体的某一行记录。主键 / 唯一索引等值查询,命中存在数据时,加记录锁。where id = 10,id 为主键且存在这条记录,锁定 id=10 这一行。
2.2 间隙锁 Gap Lock
锁定索引记录之间的间隙,不包含记录本身,防止其他事务在间隙插入数据,解决幻读。
等值查询,但是这条记录不存在,会加间隙锁。 例:id 主键,表中有 1,5,10;查询 where id=7,不存在,则锁住 (5,10) 这个间隙,其他事务无法插入 id=7。
2.3 临键锁 Next-Key Lock(RR 默认行锁算法)
临键锁 =记录锁 + 间隙锁,左开右闭区间。是 RR 隔离级别默认加锁算法。 索引:1,5,10 Next-Key 区间:(-∞,1], (1,5], (5,10], (10,+∞) 查询 id=5,临键锁区间 (1,5],锁住 5 这条记录 + 1~5 之间的间隙。
等值查询,命中唯一索引存在数据,临键锁退化成记录锁; 等值查询,记录不存在,临键锁退化成间隙锁。
幻读:同一个事务内,相同查询,前后读到行数不一样(别的事务插入新数据)。RR 依靠临键锁解决幻读。
三、死锁
3.1 死锁四大必要条件,缺一不可
- 互斥条件:资源排他,一个资源同一时间只能被一个事务持有(锁互斥特性)
- 请求与保持条件:事务已经持有锁,同时请求获取新锁,不释放已有锁
- 不可剥夺条件:已经获取的锁,不能被其他事务强行抢占,只能自己释放
- 循环等待条件:多个事务形成锁等待循环链,事务 A 等 B 的锁,B 等 C,C 等 A
四个条件同时满足,死锁产生;破坏任意一个条件,就可以解除死锁。
3.2 死锁示例
事务 1:
begin; update user set name='a' where id=1; -- 获取id=1行X锁 update user set name='b' where id=2; -- 请求id=2锁事务 2:
begin; update user set name='b' where id=2; -- 获取id=2行X锁 update user set name='a' where id=1; -- 请求id=1锁事务 1 持有 id1,等 id2;事务 2 持有 id2,等 id1 → 循环等待,死锁。
InnoDB 会自动检测死锁,回滚代价较小的事务,抛出Deadlock found when trying to get lock。
3.3 如何定位死锁
开启死锁日志,查看show engine innodb status;,找到LATEST DETECTED DEADLOCK,查看死锁事务、锁、SQL。
3.4 死锁规避方案(生产实践)
- 统一资源访问顺序:所有事务更新多行,都按固定顺序更新(先 id1 再 id2),破坏循环等待条件。最有效!
- 事务尽量简短,尽早提交,减少锁持有时间;
- 避免大事务,不要在事务中做耗时操作;
- 尽量使用主键 / 唯一索引更新,缩小锁范围;不走索引会锁整张表;
- 避免一次性锁多条记录;
- 业务层增加重试机制,捕获死锁异常重试。
四、锁等待超时
死锁是循环等待,InnoDB 自动检测回滚; 锁等待超时:单纯拿不到锁,没有循环等待。
参数:innodb_lock_wait_timeout默认 50 秒。超过时间直接报错。
五、RC 和 RR 隔离级别锁差异总结
| 隔离级别 | 锁机制 | 幻读 |
|---|---|---|
| RC 读已提交 | 只有记录锁,无间隙锁 | 存在幻读 |
| RR 可重复读 | 默认 Next-Key 临键锁,包含记录 + 间隙锁 | 解决幻读 |
RC 并发性能略高,但是存在幻读;MySQL 默认 RR。
六、生产环境锁优化最佳实践
- 更新语句一定要走索引,避免行锁升级表锁;update 条件字段必须建索引;
- 事务尽量小而短,不要长事务,长事务会长时间占用锁,增加锁冲突;
- select 普通查询不加锁(快照读);for update/lock in share mode 是当前读,会加锁;
- 尽量减少事务内锁的行数;
- 统一更新顺序,规避死锁;
- 监控:锁等待、死锁次数、长事务告警。
快照读 vs 当前读: 普通 select 是快照读,读取 undo 日志版本,不加行锁; update、delete、for update、lock in share mode 是当前读,读取最新数据,加行锁。
七、面试高频问答
Q1:InnoDB 行锁是锁行还是锁索引?
A:锁索引!行锁是加在索引上,如果 SQL 没有索引,无法定位行,退化成表锁。
Q2:意向锁作用?意向锁之间互斥吗
A:意向锁是表级锁,标记表内存在行锁;意向锁之间互相兼容;意向锁和表 S/X 锁互斥。
Q3:临键锁是什么,解决什么问题?
A:Next-Key 临键锁 = 记录锁 + 间隙锁,RR 隔离级别默认锁算法,用来解决幻读。
Q4:死锁四个必要条件,怎么避免死锁?
A:互斥、请求保持、不可剥夺、循环等待;破坏任意一个就不会死锁。最常用:统一资源访问顺序,破坏循环等待。
Q5:RC 和 RR 在锁上最大区别?
A:RC 没有间隙锁,只有记录锁,会出现幻读;RR 有间隙锁 + 临键锁,防止幻读。
Q6:select * from table where id=1 会加行锁吗?
A:普通 select 快照读不加锁;select ... for update /lock in share mode 当前读,才加锁。
结语
MySQL 锁机制是并发数据库核心难点,非常喜欢深挖。
重点记住:MyISAM 只有表锁;InnoDB 行锁基于索引,RR 下临键锁防幻读;意向锁是表锁用于快速判断锁冲突;死锁四大条件,统一访问顺序是解决死锁最有效的手段。
写业务 SQL 的时候,一定要思考这条 SQL 会加什么锁,锁范围多大,防止并发场景锁等待、死锁故障。