一个死锁的case

博客介绍了在线上环境中遇到的一个由insert引发的死锁问题。通过DEMO详细展示了死锁的发生过程,分析了事务中的间隙锁导致的死锁原因。提出了两种解决方案:提前插入或降低事务隔离级别,并讨论了RR隔离级别下InnoDB的幻读问题。最后强调了避免长事务和事务中插入操作以减少死锁风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题描述

今天线上流水的consumer出现了一个insert导致的死锁问题,这里通过一个DEMO复现一下case的整个过程,并进行详细的分析。

表结构如下:

mysql> show create table test_table;
| Table      | Create Table                                                                                
| test_table | CREATE TABLE `test_table` (
  `id` int NOT NULL AUTO_INCREMENT,
  `a` int NOT NULL,
  `b` int DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `test_table_a_uindex` (`a`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |

1 row in set (0.00 sec)
复制代码

表数据:

mysql> select * from test_table;
+----+----+------+
| id | a  | b    |
+----+----+------+
|  1 |  1 |    1 |
| 20 | 20 |   20 |
| 50 | 50 |   50 |
+----+----+------+
3 rows in set (0.02 sec)
复制代码

事务中的代码是先update,如果记录不存在再去insert。

事务1:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update test_table set b = 1 where a = 30;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
复制代码

事务1开启事务,并update一条不存在的记录(此时会对a,[20,50]加间隙锁)。

事务2:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update test_table set b = 1 where a = 31;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
复制代码

事务2开启事务,并update一条不存在的记录(此时同样会对a,[20,50加间隙锁])。

事务1:

mysql> insert into test_table values(30,30,1);
mysql> waiting...
复制代码

事务1插入一条记录,此时会被阻塞...

事务2:

ysql> insert into test_table values(31,31,1);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
复制代码

事务2同样插入一条记录,此时会报错,死锁,并回滚事务。

事务1:

mysql> insert into test_table values(30,30,1);
Query OK, 1 row affected (12.58 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
复制代码

事务2回滚后,事务1插入成功,事务提交成功。

问题分析

1.事务1、事务2中分别进行了一次update操作,并且操作的记录都不存在,此时,事务1、2分别会对a,[20,50]范围加一个间隙锁。

注:间隙锁与间隙锁之间可以兼容(共享锁)

2.事务1中进行了一次insert操作,此时由于事务2对a,[20,50]范围加了一个间隙锁,所以事务1的insert操作处于阻塞状态。

3.事务2中也进行了一次insert操作,同样被事务1的间隙锁阻塞。此时事务1在等事务2释放间隙锁,而事务2也在等事务1释放间隙锁,构成死锁,所以事务2报错“死锁”并进行事务回滚。

4.事务2回滚后,事务2的间隙锁被释放,事务1的insert操作执行成功,事务1提交成功。

解决方案

考虑了两种解决方案:

方案一

将事务中的插入操作提到事务之前执行,每次事务开始前先select一下,如果记录不存在插入一条空记录进去,在事务中只需要执行update操作。

缺点:多进行了一次select操作,可能对接口性能造成影响,需要重新进行压测判断。

方案二

降低mysql事务隔离级别,从RR下调到RC。

缺点:存在幻读问题

综合考虑,由于这个集群只用于流水和统计数据的存储,所以采用了 方案二:调低事务隔离级别

知识扩展

InnoDB中RR隔离级别是否存在幻读问题?

回答这个问题前,我先假设你知道数据库隔离级别的定义针对的都是“当前读”

首先我们来看一段InnoDB官方文档的话:

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition. For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it. For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions by other sessions into the gaps covered by the range.

大致意思就是,在 RR 级别下,如果查询条件能使用上唯一索引,或者是一个唯一的查询条件,那么仅加行锁,如果是一个范围查询,那么就会给这个范围加上 gap 锁或者 next-key锁 (行锁+gap锁)。

InnoDB 的 RR 隔离界别对范围会加上 GAP,不会存在幻读。

小结

  • 事务最好不要太长,否则容易出现锁等待、死锁等问题
  • insert操作最好不要放到事务里,否则容易引发死锁问题(相互等待)。

### C语言中文件操作导致死锁的产生原因及解决方法 #### 死锁的产生原因 在C语言中的文件操作可能引发死锁的情况主要源于以下几个方面: 1. **共享资源的竞争** 当多个线程或进程同时访问同一个文件时,如果它们都试图对该文件进行写入操作,并且各自持有不同的锁,则可能会形成循环等待的状态。例如,当一个线程正在读取某一部分数据的同时,另一个线程希望修改这部分数据,但由于第一个线程尚未释放其持有的锁,从而造成了阻塞现象[^1]。 2. **不当的同步机制** 如果程序员未能合理安排文件锁的获取顺序或者忘记解锁已获得的锁,也可能间接促成死锁局面的发生。比如,在跨平台开发环境中,某些系统的实现细节差异使得原本预期的行为变得不确定起来,进而增加了潜在的风险因素[^3]。 ```c FILE *file = fopen("example.txt", "r+"); flockfile(file); // 获取文件流上的锁 // 执行一系列复杂计算... functhatmightblock(); // 可能会因其他资源竞争而陷入长时间等待 fflush(file); fun unlockingfunction(); // 假设这里存在错误未正确调用 fununlockingfunction() ``` 在此示例代码片段中可以看出,假如 `fun lockingfunction()` 和 `fun unlockingfunction()` 的配对关系出现问题(如遗漏解除锁定的操作),就极有可能造成长时间占用资源却不释放的局面,最终演变为死锁状况。 #### 解决方案 ##### 方法一:采用单一入口管理文件访问权限 为了减少并发冲突带来的隐患,建议集中化管理所有针对目标文件的一切I/O请求活动。可以通过创建专用函数负责协调这些事务流程,确保每次仅允许单一线程/进程介入其中完成相应任务后再退出临界区[^1]。 ##### 方法二:定义清晰一致的加锁协议 类似于先前讨论过的通用解决方案之一——确立固定的加锁序列规则同样适用于此处情境。对于涉及同一份文档的不同部分区域间的相互作用过程而言尤为重要;只要遵循既定方针行动就不会轻易触发连锁反应式的僵局事件[^3]。 ```c int file_operation(FILE *fp, int operation_type){ flockfile(fp); switch(operation_type){ case READ_OPERATION: /* Perform read */ break; case WRITE_OPERATION: /* Perform write with proper checks */ break; default: fprintf(stderr,"Invalid Operation\n"); } fflush(fp); fun_unlocking_function(); // Ensure this always gets called before leaving critical section. unflockfile(fp); return SUCCESS; } ``` 上述样例展示了如何封装基本功能单元以便更好地掌控整体结构布局,同时也强调了无论何种情形都要记得及时清理现场的重要性。 ##### 方法三:引入超时期限概念规避持久停滞 考虑到实际情况当中难免会出现意外干扰致使正常进度被打断的情形,因此适时加入一些防护手段很有必要。比如说设置合理的最大容忍延迟阈值一旦超出范围便立即终止当前动作转而寻求替代途径继续前进[^4]。 ```c struct timespec timeout={0}; timeout.tv_sec=MAX_WAIT_TIME_SECS; if(ftrylockfile(fp)==EWOULDBLOCK){ // TryLock returns non-zero value when unable to obtain lock immediately within specified period. perror("Failed acquiring File Lock after waiting."); exit(EXIT_FAILURE); }else{ perform_task(); ffl ush(fp); fun_unlocking_function(); unflockfile(fp); } ``` 这段示范说明了怎样运用尝试型API接口配合自定义计时器达成目的效果。 ##### 方法四:定期审查日志记录寻找蛛丝马迹 长远来看建立健全的日志管理体系有助于快速定位问题根源所在并积累宝贵经验教训供日后借鉴参考之用。每当察觉到可疑迹象的时候都应该详尽记载下来作为后续分析依据[^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值