MySQL 的锁机制是实现并发控制的核心,不同存储引擎和隔离级别下的锁策略差异巨大。以下按锁粒度 → 锁模式 → 特殊锁 → 死锁处理的顺序系统讲解
一、按粒度分类:表锁 vs 行锁
锁的粒度决定了锁的并发度和开销,是锁机制的首要维度
1. 表锁(Table Lock)
对整个表加锁,开销小、加锁快,但并发度最低。
MyISAM 存储引擎
-- 自动加表锁:读锁(共享锁)SELECT*FROMusersWHEREid=1;-- 自动加 READ LOCK-- 自动加表锁:写锁(排他锁)UPDATEusersSETname='张三'WHEREid=1;-- 自动加 WRITE LOCK锁机制:
- 读锁(READ LOCK):允许其他会话读,禁止写
- 写锁(WRITE LOCK):禁止其他会话读和写
- 锁排队:读锁和写锁互斥,串行执行
适用场景:读多写少的静态表(如配置表),不适合高并发写入场景。
InnoDB 存储引擎的表锁
-- 手动加表级 READ LOCKLOCKTABLESusersREAD;-- 手动加表级 WRITE LOCKLOCKTABLESusersWRITE;-- 查看表锁状态SHOWOPENTABLESWHEREIn_use>0;⚠️ 注意:InnoDB 默认使用行锁,表锁仅用于特殊场景(如 DDL 操作),手动加表锁会降级并发性能
2. 行锁(Row Lock)
对索引记录加锁,开销大、加锁慢,但并发度最高
-- InnoDB 自动加行锁(RC/RR 隔离级别)UPDATEusersSETname='张三'WHEREid=1;-- 对 id=1 的行加排他锁行锁的 3 种实现方式
| 行锁类型 | 锁定范围 | 示例 | 说明 |
|---|---|---|---|
| Record Lock | 精确索引记录 | WHERE id = 1 | 锁定主键值为 1 的索引记录 |
| Gap Lock | 索引间隙(不含记录) | WHERE id BETWEEN 10 AND 20(RR 级别) | 防止幻读,锁定 10-20 的开区间 |
| Next-Key Lock | 记录 + 间隙 | WHERE id >= 10(RR 级别) | Gap Lock + Record Lock,锁定 (负无穷, 10] |
⚠️关键:行锁必须命中索引,否则退化为表锁!
-- ❌ 行锁失效:name 无索引,退化为表锁UPDATEusersSETage=20WHEREname='张三';-- 锁全表!-- ✅ 行锁生效:name 有索引ALTERTABLEusersADDINDEXidx_name(name);UPDATEusersSETage=20WHEREname='张三';-- 仅锁匹配行二、按模式分类:共享锁、排他锁、意向锁
1. 共享锁(S Lock,Shared Lock)
允许其他事务读,禁止写
-- 显式加共享锁(FOR SHARE 是 MySQL 8.0+ 语法,兼容 READ LOCK)SELECT*FROMusersWHEREid=1FORSHARE;-- MySQL 8.0+SELECT*FROMusersWHEREid=1LOCKINSHAREMODE;-- MySQL 5.x-- 效果:其他事务可读,但无法获取排他锁(无法 UPDATE/DELETE)应用场景:读取数据后需保证不被修改(如财务对账)
2. 排他锁(X Lock,Exclusive Lock)
禁止其他事务读和写
-- 自动加排他锁UPDATEusersSETname='张三'WHEREid=1;-- 显式加排他锁SELECT*FROMusersWHEREid=1FORUPDATE;关键行为:
- UPDATE/DELETE:自动加 X 锁
- SELECT … FOR UPDATE:显式加 X 锁,用于悲观锁场景(如秒杀扣库存)
- 锁升级:同一事务可对同一行先加 S 锁,再加 X 锁
3. 意向锁(Intention Lock)
表级锁,表明事务稍后要对表中的行加锁,是 InnoDB 实现表锁和行锁共存的关键
-- 事务 1:对 users 表某行加 X 锁(自动加意向排他锁 IX)BEGIN;UPDATEusersSETname='张三'WHEREid=1;-- 自动在 users 表加 IX 锁-- 事务 2:尝试对 users 表加表锁LOCKTABLESusersREAD;-- 被阻塞!因为检测到 IX 锁| 意向锁类型 | 说明 | 兼容性 |
|---|---|---|
| IS(意向共享锁) | 事务想对表中的行加 S 锁 | 与表级 READ 锁兼容 |
| IX(意向排他锁) | 事务想对表中的行加 X 锁 | 与任何表锁都冲突 |
意义:避免表锁和行锁冲突导致的死锁,提升检查效率
三、InnoDB 特殊锁机制
1. 间隙锁(Gap Lock)
在RR(可重复读)隔离级别下,防止幻读的关键机制
-- 假设 users 表中 id 为 1, 5, 10, 20-- 事务 1(RR 级别)BEGIN;SELECT*FROMusersWHEREidBETWEEN5AND15FORUPDATE;-- 锁定范围:(5, 10) 和 (10, 15) 这两个间隙-- 关键字:锁定开区间 (5, 15),不包含 5 和 15-- 事务 2(被阻塞)INSERTINTOusers(id,name)VALUES(8,'李四');-- 被阻塞!间隙被锁INSERTINTOusers(id,name)VALUES(3,'王五');-- 成功,不在锁定间隙目的:防止其他事务在范围内插入新记录,避免"幻读"
2. 临键锁(Next-Key Lock)
Gap Lock + Record Lock,锁定左开右闭区间(prev_record, record]
-- 事务 1(RR 级别)BEGIN;SELECT*FROMusersWHEREid>=10FORUPDATE;-- 锁定范围:(负无穷, 1], (1, 5], (5, 10], (10, 正无穷)-- 关键点:包含记录本身及其左侧的间隙-- 事务 2INSERTINTOusers(id,name)VALUES(9,'赵六');-- 被阻塞(锁定 (5,10] 间隙)INSERTINTOusers(id,name)VALUES(11,'孙七');-- 被阻塞(锁定 (10, 正无穷))InnoDB 默认锁:在 RR 级别,普通索引加 Next-Key Lock,主键索引加 Record Lock。
3. 插入意向锁(Insert Intention Lock)
一种特殊的间隙锁,表明事务想在某个间隙插入记录,多个事务可同时在不同位置插入(提高并发)
-- 事务 1BEGIN;SELECT*FROMusersWHEREid=5FORUPDATE;-- 对 id=5 加 X 锁-- 事务 2BEGIN;SELECT*FROMusersWHEREid=5FORUPDATE;-- 被阻塞,等待事务 1 释放-- 事务 3(可成功)INSERTINTOusers(id,name)VALUES(6,'周八');-- 成功!只等待 id=5 的锁机制:插入时先加 Insert Intention Lock,检查间隙是否冲突,不冲突则等待 Gap Lock 释放后插入
4. 自增锁(AUTO-INC Lock)
表级锁,用于保证自增列值的唯一性
CREATETABLEusers(idBIGINTAUTO_INCREMENTPRIMARYKEY,nameVARCHAR(50));-- 插入时自动加自增锁INSERTINTOusers(name)VALUES('张三'),('李四'),('王五');-- 锁定自增计数器,保证三个 ID 连续且唯一锁模式(innodb_autoinc_lock_mode):
- 0(Traditional):每次插入都加表级锁,性能最差,保证连续
- 1(Consecutive):批量插入加表级锁,简单插入用轻量锁(默认)
- 2(Interleaved):无锁,性能最好,但 ID 可能不连续(适合高并发)
四、不同隔离级别下的锁行为
RC(读已提交)
- 行锁:仅对匹配行加 Record Lock
- 无 Gap Lock:不锁定间隙,允许插入,可能产生幻读
- 一致性:每次 SELECT 生成新 Read View,可能读到其他事务已提交数据
-- 事务 1(RC 级别)BEGIN;SELECT*FROMusersWHEREid=10FORUPDATE;-- 仅锁定 id=10 的行-- 事务 2INSERTINTOusers(id,name)VALUES(9,'吴九');-- ✅ 成功,间隙未锁RR(可重复读)
- 行锁:对匹配行和间隙加 Next-Key Lock
- Gap Lock:锁定范围,防止幻读
- 一致性:事务启动时生成 Read View,保证可重复读
-- 事务 1(RR 级别)BEGIN;SELECT*FROMusersWHEREidBETWEEN5AND15FORUPDATE;-- 锁定 (5,15] 间隙-- 事务 2INSERTINTOusers(id,name)VALUES(8,'郑十');-- ❌ 被阻塞Serializable
- 读加锁:普通 SELECT 自动加S 锁
- 完全串行:所有操作串行执行,并发度最低,数据一致性最强
五、死锁(Deadlock)与排查
死锁产生条件
- 互斥条件:资源不能共享
- 请求与保持:持有资源同时申请新资源
- 不可剥夺:资源只能主动释放
- 循环等待:形成等待环路
经典死锁案例
-- 事务 1BEGIN;UPDATEusersSETname='A'WHEREid=1;-- 持有 id=1 的 X 锁-- 等待 id=2 的 X 锁UPDATEusersSETname='B'WHEREid=2;-- 事务 2(同时执行)BEGIN;UPDATEusersSETname='C'WHEREid=2;-- 持有 id=2 的 X 锁-- 等待 id=1 的 X 锁,形成循环等待UPDATEusersSETname='D'WHEREid=1;-- Deadlock!InnoDB 处理:自动检测死锁,回滚持有锁最少的事务
死锁排查命令
-- 查看最近一次死锁信息SHOWENGINEINNODBSTATUS\G-- 输出:-- -------------------------- LATEST DETECTED DEADLOCK-- -------------------------- *** (1) TRANSACTION:-- UPDATE users SET name = 'B' WHERE id = 2-- *** (2) TRANSACTION:-- UPDATE users SET name = 'D' WHERE id = 1-- *** WE ROLL BACK TRANSACTION (2) -- 回滚事务 2-- 查看当前锁等待SELECT*FROMinformation_schema.INNODB_LOCK_WAITS;-- 查看当前事务SELECT*FROMinformation_schema.INNODB_TRX;-- 查看当前锁SELECT*FROMinformation_schema.INNODB_LOCKS;六、锁优化最佳实践
1. 索引设计优化
-- ❌ 导致表锁UPDATEusersSETname='张三'WHEREname='李四';-- name 无索引-- ✅ 保证行锁ALTERTABLEusersADDINDEXidx_name(name);UPDATEusersSETname='张三'WHEREname='李四';-- name 有索引2. 事务粒度控制
-- ❌ 长事务持有锁时间过长BEGIN;-- 处理 10 秒业务逻辑UPDATEusersSETscore=score+10WHEREid=1;COMMIT;-- ✅ 缩短事务,减少锁持有时间-- 业务逻辑提前处理BEGIN;UPDATEusersSETscore=score+10WHEREid=1;COMMIT;3. 避免热点行
-- ❌ 高并发下所有请求锁同一行UPDATEinventorySETstock=stock-1WHEREproduct_id=1;-- ✅ 拆分到多行,减少冲突UPDATEinventorySETstock=stock-1WHEREproduct_id=1ANDwarehouse_id=MOD(NOW(),10);4. 死锁预防
- 固定顺序访问:所有事务按相同顺序申请锁(如先锁 id=1,再锁 id=2)
- 降低隔离级别:将 RR 改为 RC,减少 Gap Lock
- 批量操作拆分:将大事务拆分为小事务,减少锁持有时间
5.监控锁等待
-- 查看锁等待超时时间SHOWVARIABLESLIKE'innodb_lock_wait_timeout';-- 默认 50 秒-- 设置会话级超时时间SETSESSIONinnodb_lock_wait_timeout=10;-- 10 秒超时-- 开启死锁日志SETGLOBALinnodb_print_all_deadlocks=ON;七、总结
| 锁类型 | 粒度 | 模式 | 适用场景 | 性能影响 |
|---|---|---|---|---|
| 表锁 | 粗 | S/X | MyISAM 读多写少表 | 并发度低 |
| 行锁 | 细 | S/X | InnoDB 高并发 OLTP | 并发度高 |
| 意向锁 | 表 | IS/IX | InnoDB 表锁检查 | 几乎无影响 |
| 间隙锁 | 行间隙 | Gap | RR 级别防幻读 | 可能阻塞插入 |
| 临键锁 | 行+间隙 | Next-Key | RR 级别默认锁 | 最严格 |
| 自增锁 | 表 | AUTO-INC | 自增列赋值 | 批量插入影响性能 |
核心原则:
- 优先行锁:InnoDB 行锁并发度远高于表锁
- 命中索引:确保 WHERE 条件走索引,避免退化为表锁
- 缩短事务:减少锁持有时间,降低死锁概率
- 监控死锁:定期分析死锁日志,优化事务顺序