你经历过数据库死锁事故吗?

你经历过数据库死锁事故吗?
最新回答
灰尘往事

2026-05-18 20:47:04

我经历过数据库死锁事故,以下结合具体案例说明死锁的发生过程、原因及避免方法:

死锁案例背景

业务涉及新增订单、修改订单、查询订单等操作,为保证订单唯一性,在新增订单时通过 select ... for update 语句进行幂等性校验,即先查询订单是否存在,不存在才插入记录。当业务量较大时,出现死锁问题。

死锁发生过程
  • 表结构:订单表 t_order 中,id 字段为主键索引,order_no 字段为普通索引(非唯一索引)。
CREATE TABLE `t_order` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` int DEFAULT NULL, `create_date` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_order` (`order_no`) USING BTREE) ENGINE=InnoDB ;
  • 初始数据:t_order 表已有 6 条记录。

  • 事务操作:假设有两个事务,事务 A 要插入订单 1007,事务 B 要插入订单 1008。两个事务都先执行 select ... for update 语句查询订单是否存在,不存在则插入记录。

    两个事务都陷入等待状态,发生死锁,因为都在相互等待对方释放锁。
死锁产生原因
  • 幻读问题与 next - key 锁:在可重复读隔离级别下,存在幻读问题。Innodb 引擎为解决该问题引入 next - key 锁,它是记录锁和间隙锁的组合。

    记录锁(Record Lock):锁的是记录本身。

    间隙锁(Gap Lock):锁的是两个值之间的空隙,防止其他事务在这个空隙间插入新数据,避免幻读现象。

  • 行锁释放时机:行锁在事务提交(commit)后释放,并非一条语句执行完就释放。
  • 本次死锁具体原因:执行 select id from t_order where order_no = 1008 for update; 语句时,因 order_no 不是唯一索引,行锁类型为间隙锁,间隙锁范围是 (1006, +∞)。事务 B 往该间隙锁里插入 id = 1008 的记录会被锁住。插入语句会在插入间隙上再次获取插入意向锁,插入意向锁与间隙锁冲突,当其他事务持有该间隙的间隙锁时,需等待其他事务释放间隙锁才能获取插入意向锁。而间隙锁与间隙锁之间兼容,两个事务中的 select ... for update 语句不会相互影响。事务 A 和事务 B 执行完 select ... for update 语句后都持有范围为 (1006,+∞) 的间隙锁,接下来的插入操作为获取插入意向锁,都在等待对方事务的间隙锁释放,造成循环等待,导致死锁。

避免死锁的方法
  • 数据库层面策略(打破循环等待条件)

    设置事务等待锁的超时时间:在 InnoDB 中,参数 innodb_lock_wait_timeout 用于设置超时时间,默认 50 秒。当一个事务的等待时间超过该值后,事务进行回滚,锁释放,另一个事务可继续执行。当发生超时后,会出现提示信息。

    开启主动死锁检测:将参数 innodb_deadlock_detect 设置为 on(默认开启),主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。当检测到死锁后,会出现提示信息。

  • 业务角度预防:将 order_no 字段设置为唯一索引列,利用其唯一性保证订单表不会出现重复订单。不过插入一个已经存在的订单记录时会抛出异常。