MySQL InnoDB 共享行锁(S) & 排他行锁(X)
注意:行锁只在 InnoDB 引擎,并且事务开启下生效;自动提交 autocommit=1 时锁会立刻释放,看不到锁效果 。
行锁是记录锁,锁住某一行数据,不是锁整张表。
1、共享行锁 S(Shared,读锁)
加锁语法 :select ... for share;(MySQL8.0,旧版 LOCK IN SHARE MODE)
- 特性:
- 多个事务可以同时加S锁(读读兼容)
- 如果事务A对一行加S锁,其他事务还可以继续加S锁 ,但不能加X排他锁
- S锁事务可以读数据;不能修改这行数据,要修改必须升级为X锁
使用场景
- 先读取数据,后续业务要修改,希望防止别人把这条数据删掉/做重大修改,但允许别人读取
举例:订单校验,先查询订单状态,后续要更新订单;不希望别的事务直接更新/删除这条订单,但允许其他业务读取订单信息。
sql
begin;
select * from t_order where id=100 for share; -- 加S共享行锁
-- 业务逻辑校验
update t_order set status=2 where id=100; -- 尝试修改,S锁升级X锁,如果此时有其他S锁,会阻塞等待
commit;
- 多事务共同读取同一行,数据要保证读取期间不被别的事务改写
适合:数据核对、统计,允许多方读,但禁止别人写。
⚠️ S锁坑点:
如果多个事务同时加S锁,然后都想更新这行,互相等待,直接死锁 。
事务1:S锁 → 想升级X锁;事务2:S锁 → 想升级X锁;互相阻塞,死锁。
所以业务要更新的场景,更推荐直接用X锁,不要用S锁。
2、排他行锁 X(Exclusive,写锁)
加锁语法 :select ... for update;
DML语句(update / delete / insert)会自动给命中行加X排他锁。
- 特性:
- 互斥 :一行加了X锁之后,其他事务既不能加S锁,也不能加X锁
- 持有X锁的事务,可以读写这行;其他事务读写都会阻塞等待锁释放。
使用场景
- 并发更新,防止超卖、数据错乱(最常用)
扣库存:查询库存同时上锁,不允许其他事务修改该行,避免并发超卖
sql
begin;
select stock from goods where id=1 for update; -- 加X行锁
update goods set stock = stock -1 where id=1;
commit;
- 业务确定接下来就要修改/删除该行,直接上写锁,避免幻读、脏写
- delete、update 执行时,InnoDB自动加X行锁,不需要手动写for update。
锁兼容矩阵(重点)
| 当前持有锁 | 申请S锁 | 申请X锁 |
|---|---|---|
| S锁 | ✅ 通过 | ❌ 阻塞 |
| X锁 | ❌ 阻塞 | ❌ 阻塞 |
对比总结
| 锁类型 | 语法 | 核心特点 | 适用场景 | 不适合场景 |
|---|---|---|---|---|
| S共享行锁 | select ... for share |
允许多个事务读;阻止别人写 | 多读,短时间校验,允许其他事务读取 | 后续要更新,多并发更新,容易死锁 |
| X排他行锁 | select ... for update/update/delete |
独占,别人不能读也不能写 | 并发修改、扣库存、订单状态变更 | 大量并发只读场景,会造成大量阻塞 |
实际业务最佳实践
- 绝大多数并发更新场景,直接用 X锁(for update),尽量少用S锁,规避S锁带来的死锁风险。
- S锁适合:只做校验读取,确定本事务不会修改该行的场景。
- 行锁生效前提:where条件命中索引,如果没走索引,InnoDB会退化成表锁,整张表锁住,性能灾难。
- 锁直到事务
commit / rollback才释放,事务不要写大业务,尽快提交,减少锁持有时间。
补充区分快照读 vs 当前读
- 普通select:快照读,不加锁,走MVCC,不受行锁影响,可以读到旧版本数据。
for share / for update / update / delete:当前读,会真实加行锁。
举个例子:A事务给一行加X锁;B事务普通select依然可以查到数据(MVCC快照),但B执行
select ... for update就会阻塞。