记一次线上SQL死锁事故:如何避免死锁?

背景

之前我参与过一个项目,在项目初期,我们是没有将读写表分离的,而是基于一个主库完成
读写操作。在业务量逐渐增大的时候,我们偶尔会收到系统的异常报警信息,DBA 通知我
们数据库出现了死锁异常。
按理说业务开始是比较简单的,就是新增订单、修改订单、查询订单等操作,那为什么会出
现死锁呢?经过日志分析,我们发现是作为幂等性校验的一张表经常出现死锁异常。我们和
DBA 讨论之后,初步怀疑是索引导致的死锁问题。后来我们在开发环境中模拟了相关操
作,果然重现了该死锁异常。

生成问题重现

接下来我们就通过实战来重现下该业务死锁异常。首先,创建一张订单记录表,该表主要用
于校验订单重复创建:

sql 复制代码
CREATE TABLE `order_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` int(11) DEFAULT NULL,
`status` int(4) DEFAULT NULL,
 `create_date` datetime(0) DEFAULT NULL,
 PRIMARY KEY (`id`) USING BTREE,
 INDEX `idx_order_status`(`order_no`,`status`) USING BTREE
 ) ENGINE = InnoDB

为了能重现该问题,我们先将事务设置为手动提交。这里要注意一下,MySQL 数据库和
Oracle 提交事务不太一样,MySQL 数据库默认情况下是自动提交事务,我们可以通过以
下命令行查看自动提交事务是否开启:

sql 复制代码
mysql> show variables like 'autocommit';
 +---------------+-------+
 | Variable_name | Value |
 +---------------+-------+
 | autocommit | ON |
 +---------------+-------+
 1 row in set (0.01 sec)

下面就操作吧,先将 MySQL 数据库的事务提交设置为手动提交,通过以下命令行可以关
闭自动提交事务:

sql 复制代码
mysql> set autocommit = 0;
 Query OK, 0 rows affected (0.00 sec)

订单在做幂等性校验时,先是通过订单号检查订单是否存在,如果不存在则新增订单记录。
知道具体的逻辑之后,我们再来模拟创建产生死锁的运行 SQL 语句。首先,我们模拟新建
两个订单,并按照以下顺序执行幂等性校验 SQL 语句(垂直方向代表执行的时间顺序):

此时,我们会发现两个事务已经进入死锁状态。我们可以在 information_schema 数据库
中查询到具体的死锁情况,如下图所示:

看到这,你可能会想, 为什么 SELECT 要加 for update 排他锁,而不是使用共享锁呢? 试
想下,如果是两个订单号一样的请求同时进来,就有可能出现幻读。也就是说,一开始事务
A 中的查询没有该订单号,后来事务 B 新增了一个该订单号的记录,此时事务 A 再新增一
条该订单号记录,就会创建重复的订单记录。面对这种情况,我们可以使用锁间隙算法来防
止幻读。

避免死锁的措施

知道了死锁问题源自哪儿,就可以找到合适的方法来避免它了。
避免死锁最直观的方法就是在两个事务相互等待时,当一个事务的等待时间超过设置的某一
阈值,就对这个事务进行回滚,另一个事务就可以继续执行了。这种方法简单有效,在
InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的。
另外,我们还可以将 order_no 列设置为唯一索引列。虽然不能防止幻读,但我们可以利用
它的唯一性来保证订单记录不重复创建,这种方式唯一的缺点就是当遇到重复创建订单时会
抛出异常。
我们还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用 Redis 以及
ZooKeeper 来实现,运行效率比数据库更佳。


推荐阅读

相关推荐
ClouGence1 天前
Oracle 数据同步为什么会出现数据不一致?长事务是常被忽略的原因
数据库·后端·oracle
飞将1 天前
从零实现数据库(2)——HashIndex + IndexManager
数据库
Nturmoils2 天前
订单列表慢查询,先看 WHERE、ORDER BY 和 LIMIT
数据库
渣波2 天前
拒绝 SQL 焦虑!手把手带你用 NestJS + Prisma + DTO 写出“防弹”级后端代码
javascript·数据库·后端
倔强的石头_3 天前
KingbaseES 新版MySQL 兼容版体验:旧版迁移 + 功能实测
数据库
zzzzzz3104 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
倔强的石头_6 天前
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
数据库
云技纵横6 天前
唯一索引 INSERT 死锁实战:5 秒复现交叉插入的 S 锁循环等待
sql·mysql
冬奇Lab7 天前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
ClouGence7 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle