MySQL: 并发replace into的死锁问题分析

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介:
测试版本:MySQL5.6.23 测试表:
create table t1 (a int auto_increment primary key, b int, c int, unique key (b)); 并发执行SQL
replace into t1(b,c) values (2,3)  //使用脚本,超过3个会话

背景

replace 操作可以算是比较常用的操作类型之一,我们先来理一下,对于上例,一条简单的replace into操作的主要流程包括哪些。

 

Step 1. 正常的插入逻辑

首先插入聚集索引,在上例中a列为自增列,由于未显式指定,每次Insert前都会生成一个不冲突的新值

随后插入二级索引b,由于其是唯一索引,在检查duplicate key时,加上记录锁,类型为LOCK_X (对于普通的INSERT操作,当需要检查duplicate key时,加LOCK_S锁,而对于Replace into 或者 INSERT..ON DUPLICATE操作,则加LOCK_X记录锁) 。 由于uk记录已存在,返回错误DB_DUPLICATE_KEY。

Step 2. 处理错误

由于检测到duplicate key,因此第一步插入的聚集索引记录需要被回滚掉(row_undo_ins)

Step 3. 转换操作

从InnoDB层失败返回到Server层后,收到duplicate key错误,首先检索唯一键冲突的索引,并对冲突的索引记录(及聚集索引记录)加锁

随后确认转换模式以解决冲突:
#如果发生uk冲突的索引是最后一个唯一索引、没有外键引用、且不存在delete trigger时,使用UPDATE ROW的方式来解决冲突
#否则,使用DELETE ROW + INSERT ROW的方式解决冲突

Step 4. 更新记录

在该例中,对聚集索引和二级索引的更新,都是采用标记删除+插入新记录的方式

对于聚集索引,由于PK列发生变化,采用delete + insert 聚集索引记录的方式更新
对于二级uk索引,同样采用标记删除 + 插入的方式

我们知道,在尝试插入一条记录时,如果插入位置的下一条记录上存在记录锁,那么在插入时,当前session需要对其加插入意向锁,具体类型为LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION

是否能保证自增列的有序性:
默认情况下,参数innodb_autoinc_lock_mode的值为1,因此只在分配自增列时互斥(如果我们将其设为0的话,就会产生AUTO_INC类型的表级锁)
当分配完自增列值后,我们并不知道并发的replace into的顺序。
死锁分析:
回到死锁线程分析,从死锁日志我们大致可以推断出如下序列:
Session 1 执行到Step4, 准备更新二级Uk索引,因此持有uk上heap no 为5的X 行锁和PK上的X行锁
Session 2 检查到uk冲突,需要加X行锁
Session 1 在标记删除记录后,尝试插入新的uk记录,发现预插入点的下一条记录(heap no =5) 上有锁请求,因此尝试加插入意向X锁,产生锁升级, 死锁路径:Session1 => Session 2 => Session1到这里,其实问题已经很明显了。 我们考虑如下场景:假设当前表内数据为:
root@sb1 08:57:41>select * from t1;
+———+——+——+
| a | b | c |
+———+——+——+
| 2100612 | 2 | 3 |
+———+——+——+
1 row in set (0.00 sec)
由于不能保证自增列被更新的有序性,我们假定session 1获得自增列值为2100619, session 2 获得的自增列值为2100614, session 3获得的自增列值为2100616
为了便于阐述,我们假定表上总是只有一条记录
Session 1:
replace into t1 values (2100619, 2, 3); // uk索引上记录(2, 2100612)被标记删除,同时插入新记录(2, 2100619)Purge线程启动,(2, 2100612)被物理删除,Page上只剩下唯一的物理记录(2, 2100619)
Session 2:
replace into t1 values (2100614, 2, 3);
这里我们使用gdb的non-stop模式,使其断在row_update_for_mysql函数(insert尝试失败后,会转换成update),此时session2持有(2, 2100619) 的X锁 Tips:我们可以通过如下命令使用gdb的non-stop模式:
1. 以gdb启动mysqld
2. 设置:
set target-async 1 
set pagination off 
set non-stop on
3. 设置函数断点
Session 3:
replace into t1 values (2100616, 2, 3); // 检测到uk有冲突键,需要获取记录(2, 2100619) 的X锁,等待session 2

 

Session 2:
a)标记删除记录(2, 2100619),同时插入新记录(2, 2100614);
b) (2, 2100614) 比(2, 2100619) 要小,因此定位到该记录之前,也就是系统记录infimum
c)infimum记录的下一条记录(2, 2100619)上有锁等待,需要升级成插入意向X锁,导致死锁发生

假定Purge线程停止,会发生什么呢 ?

我们随便建一个表,然后执行FLUSH TABLE tbname FOR EXPORT来让purge线程停止,执行序列如下:root@sb1 10:26:05>select * from t1;
+———+——+——+
| a | b | c |
+———+——+——+
| 2100710 | 2 | 3 |
+———+——+——+
1 row in set (0.00 sec)session 1
session 1:
replace into t1 values (2100720, 2, 3);
此时Page上存在记录(infimum), (2, 2100710), (2, 2100720), (supremum)
session 2
replace into t1 values (2100715, 2, 3);
同上例,使用gdb断到函数row_update_for_mysql 由于没有启动purge线程,因此老的被标记删除的记录还存在于page内,在扫描二级索引重复键时,也会依次给这些老记录加锁,因此session 2会持有 (2, 2100710)和 (2, 2100720)的X锁
session 3
replace into t1 values (2100718, 2, 3); // 被session2阻塞,等待(2,2100710)的X锁lock_mode:3, heap no:810
session 2
在标记删除二级索引记录,并进行插入时,选择的插入位置为 (2, 2100710), (2,2100720)之间,插入点的下一条记录(2,2100720)上没有其他线程锁等待,当前session锁升级成功
完成插入后,page上的记录分布为(infimum), (2, 2100710), (2, 2100715), (2, 2100720), (supremum)

 

session 3
完成插入,最终page内的记录为(infimum), (2, 2100710), (2, 2100715), (2, 2100718), (2, 2100720), (supremum)
其中只有用户记录(2, 2100718)未被标记删除


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
15天前
|
关系型数据库 MySQL 索引
mysql 分析5语句的优化--索引添加删除
mysql 分析5语句的优化--索引添加删除
12 0
|
1月前
|
SQL 缓存 关系型数据库
Mysql第十四天,Mysql并发参数调整
Mysql第十四天,Mysql并发参数调整
29 0
|
3月前
|
关系型数据库 MySQL Serverless
高顿教育:大数据抽数分析业务引入polardb mysql serverless
高顿教育通过使用polardb serverless形态进行数据汇总,然后统一进行数据同步到数仓,业务有明显高低峰期,灵活的弹性伸缩能力,大大降低了客户使用成本。
|
1月前
|
关系型数据库 MySQL 数据库
深入探讨MySQL并发事务的问题及解决方案
深入探讨MySQL并发事务的问题及解决方案
74 0
|
26天前
|
SQL 关系型数据库 MySQL
【MySQL技术专题】「问题实战系列」深入探索和分析MySQL数据库的数据备份和恢复实战开发指南(8.0版本升级篇)
【MySQL技术专题】「问题实战系列」深入探索和分析MySQL数据库的数据备份和恢复实战开发指南(8.0版本升级篇)
96 0
|
15天前
|
SQL 缓存 关系型数据库
mysql性能优化-慢查询分析、优化索引和配置
mysql性能优化-慢查询分析、优化索引和配置
80 1
|
21天前
|
缓存 关系型数据库 MySQL
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
|
2月前
|
SQL 关系型数据库 MySQL
MySQL技能完整学习列表10、数据导入和导出——1、数据导入(LOAD DATA, mysqldump)——2、数据导出(SELECT ... INTO OUTFILE, mysqldump)
MySQL技能完整学习列表10、数据导入和导出——1、数据导入(LOAD DATA, mysqldump)——2、数据导出(SELECT ... INTO OUTFILE, mysqldump)
49 0
|
1天前
|
负载均衡 关系型数据库 MySQL
MySQL-Proxy实现MySQL读写分离提高并发负载
MySQL-Proxy实现MySQL读写分离提高并发负载
|
2天前
|
关系型数据库 MySQL 中间件
【MySQL实战笔记】07 | 行锁功过:怎么减少行锁对性能的影响?-02 死锁和死锁检测
【4月更文挑战第19天】在高并发环境下,死锁发生在多个线程间循环等待资源时,导致无限期等待。MySQL中,死锁可通过`innodb_lock_wait_timeout`参数设置超时或`innodb_deadlock_detect`开启死锁检测来解决。默认的50s超时可能不适用于在线服务,而频繁检测会消耗大量CPU。应对热点行更新引发的性能问题,可以暂时关闭死锁检测(风险是产生大量超时),控制并发度,或通过分散记录减少锁冲突,例如将数据分拆到多行以降低死锁概率。
18 1