RC 隔离级别下 MySQL InnoDB 死锁典型案例

RC 隔离级别下 MySQL InnoDB 死锁典型案例

前置知识点:

  1. RC 没有间隙锁,只有记录锁;死锁只来自不同事务加锁顺序不一致;
  2. RR 有间隙锁/临键锁,死锁场景更多;RC 死锁全部是「行锁争抢顺序颠倒」导致;
  3. 死锁四条件:互斥、持有并等待、不可剥夺、循环等待。

案例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 死锁核心特点总结

  1. RC 死锁全部源于行锁获取顺序不一致,不存在RR那种间隙锁导致的诡异死锁;
  2. 只要所有事务访问资源统一按主键升序获取锁,就能彻底杜绝RC下死锁;
  3. RC下 for update / update / delete 都加记录排他锁,相互阻塞,交叉顺序必死锁;
  4. 对比RR:RR除了行锁顺序问题,还会因为间隙锁、临键锁出现更多无规律死锁,这也是大厂高并发选用RC的原因之一------死锁更容易分析、规避。

通用解决方案(线上落地)

1. 所有多资源更新,强制按主键ID升序操作;

2. 缩短事务,不要事务内sleep、远程调用;

  1. 批量更新统一用in,让数据库按主键排序加锁;

4. 超高并发前置分布式锁,从业务层避免多事务同时争抢多行。

5、减少悲观锁 for update 大范围锁定,优先乐观锁(version 版本号)

相关推荐
落叶-IT2 小时前
Java异常处理深度实战教程:异常传播的失败场景分析
数据库·oracle
执子手 吹散苍茫茫烟波3 小时前
常见的数据库隔离级别以及企业里常用的是什么方案
数据库
Database_Cool_3 小时前
数据库慢查询优化首选方案:阿里云 RDS 性能洞察+自动诊断
数据库·人工智能·阿里云
YOU OU4 小时前
Redis初识
数据库·redis·缓存
长孙豪翔4 小时前
在.net中读写config文件的各种方法
java·数据库·.net
深盾科技_Virbox5 小时前
加密狗授权能力选型:从授权模型到全生命周期管理
java·网络·数据库
峥无5 小时前
深入理解MySQL事务与MVCC机制
数据库·mysql
行思理5 小时前
MongoDB 大数据备份,新手教程
数据库·mongodb
-To be number.wan5 小时前
数据库系统 | 规范化理论
数据库·学习