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 版本号)

相关推荐
童谣135 分钟前
越华环保集团市级统筹申报数字化中台:政策校验与减排仿真一体化架构
数据库·架构
制造数据与AI践行者老蒋43 分钟前
离谱!PromptTemplate 遇上 JSON,花括号直接引发解析战争
数据库·microsoft·json
中微极客1 小时前
用LangChain 0.3构建生产级RAG与Agent:从API集成到Streamlit部署
数据库·人工智能·langchain
吴声子夜歌1 小时前
MongoDB 4.x——并发优化
数据库·mongodb
不如语冰2 小时前
AI大模型入门-Python进阶-上下文管理与with语句
开发语言·数据结构·数据库·人工智能·pytorch·redis·python
提笔了无痕3 小时前
MySQL SQL 从 EXPLAIN 到索引优化,搞懂 SQL 为什么慢
android·sql·mysql
l1t4 小时前
DeepSeek总结的DuckLake 架构深度剖析-2
数据库·架构
YOU OU4 小时前
Redis事务
数据库·redis·缓存
辰同学ovo4 小时前
从一条 SQL 到三个原则:MySQL 心智模型
sql·mysql·adb
数智化管理手记4 小时前
财务大数据怎么管住资金风险?财务大数据和财务数智化到底怎么结合?
大数据·网络·数据库·人工智能·数据挖掘