RC 隔离级别下 MySQL InnoDB 死锁典型案例
前置知识点:
- RC 没有间隙锁,只有记录锁;死锁只来自不同事务加锁顺序不一致;
- RR 有间隙锁/临键锁,死锁场景更多;RC 死锁全部是「行锁争抢顺序颠倒」导致;
- 死锁四条件:互斥、持有并等待、不可剥夺、循环等待。
案例1:两个事务更新两条记录,加锁顺序相反(最常见)
表: account
sql
CREATE TABLE account (
id BIGINT PRIMARY KEY,
balance INT
);
-- 数据
insert into account values(1,1000),(2,1000);
场景:转账,A转B、B转A,RC级别。
事务T1(1→2转账)
sql
begin;
update account set balance=balance-100 where id=1; -- 锁id=1
sleep(2);
update account set balance=balance+100 where id=2; -- 申请锁id=2
commit;
事务T2(2→1转账)
sql
begin;
update account set balance=balance-100 where id=2; -- 锁id=2
sleep(2);
update account set balance=balance+100 where id=1; -- 申请锁id=1
commit;
死锁形成:
- T1持有1锁,等2锁
- T2持有2锁,等1锁
循环等待 → 死锁。
RC / RR 都会出现这个死锁,和隔离级别无关,纯粹加锁顺序颠倒。
案例2:update + select for update 混合,顺序颠倒
同一张account表。
T1:
sql
begin;
update account set balance=balance-50 where id=1; -- 锁1
sleep(2);
select * from account where id=2 for update; -- 等2锁
T2:
sql
begin;
select * from account where id=2 for update; -- 锁2
sleep(2);
update account set balance=balance-50 where id=1; -- 等1锁
同样循环等待死锁。RC下 for update 依然加行排他锁,会产生死锁。
案例3:批量更新,in 集合顺序不一致引发死锁
商品库存表 stock(product_id, stock) ,主键 product_id。
需求:一次扣减多个商品库存。
T1 扣 1001,1002
sql
begin;
update stock set stock=stock-1 where product_id in (1001,1002);
InnoDB 执行 in 会按主键从小到大依次加锁:先锁1001,再锁1002。
T2 扣 1002,1001
sql
begin;
update stock set stock=stock-1 where product_id in (1002,1001);
依然按主键排序加锁:先锁1001,再锁1002 → 不会死锁。
会死锁的写法:分开多条update,顺序相反
T1:
sql
begin;
update stock set stock=stock-1 where product_id=1001;
sleep(1);
update stock set stock=stock-1 where product_id=1002;
T2:
sql
begin;
update stock set stock=stock-1 where product_id=1002;
sleep(1);
update stock set stock=stock-1 where product_id=1001;
循环等待死锁。
大厂规范:批量操作必须统一按主键升序加锁,避免该死锁。
案例4:先查询for update,再更新;事务锁获取顺序交叉
订单表 order(id, user_id, status) ,主键id。
T1 操作订单1、再订单2:
sql
begin;
select * from order where id=1 for update;
sleep(2);
update order set status=2 where id=2;
T2 操作订单2、再订单1:
sql
begin;
select * from order where id=2 for update;
sleep(2);
update order set status=2 where id=1;
死锁。RC下for update 是排他行锁,和RR行为一致。
案例5:唯一索引冲突 + 插入+更新交叉死锁(RC特有场景,无间隙锁)
表:
sql
CREATE TABLE goods (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sn VARCHAR(32) UNIQUE,
num INT
);
数据:sn='A' 已存在。
T1:
sql
begin;
-- 更新已有sn=A行,加该行记录锁
update goods set num=num+1 where sn='A';
sleep(2);
-- 插入sn='B'(无锁冲突)
insert into goods(sn,num) values('B',10);
T2:
sql
begin;
-- 插入sn='B',无冲突,持有B行锁
insert into goods(sn,num) values('B',10);
sleep(2);
-- 更新sn='A',申请A行锁
update goods set num=num+1 where sn='A';
T1持有A锁等B锁;T2持有B锁等A锁 → 死锁。
RC没有间隙锁,这里死锁完全来自两条独立行锁循环等待。
RC 死锁核心特点总结
- RC 死锁全部源于行锁获取顺序不一致,不存在RR那种间隙锁导致的诡异死锁;
- 只要所有事务访问资源统一按主键升序获取锁,就能彻底杜绝RC下死锁;
- RC下 for update / update / delete 都加记录排他锁,相互阻塞,交叉顺序必死锁;
- 对比RR:RR除了行锁顺序问题,还会因为间隙锁、临键锁出现更多无规律死锁,这也是大厂高并发选用RC的原因之一------死锁更容易分析、规避。
通用解决方案(线上落地)
1. 所有多资源更新,强制按主键ID升序操作;
2. 缩短事务,不要事务内sleep、远程调用;
- 批量更新统一用in,让数据库按主键排序加锁;
4. 超高并发前置分布式锁,从业务层避免多事务同时争抢多行。
5、减少悲观锁 for update 大范围锁定,优先乐观锁(version 版本号)