记一次线上问题引发的对 Mysql 锁机制分析 | 京东物流技术团队

news2024/11/17 13:51:44

背景

最近双十一开门红期间组内出现了一次因 Mysql 死锁导致的线上问题,当时从监控可以看到数据库活跃连接数飙升,导致应用层数据库连接池被打满,后续所有请求都因获取不到连接而失败

整体业务代码精简逻辑如下:

@Transaction
public void service(Integer id) {
    delete(id);
    insert(id);
}


数据库实例监控:

当时通过分析上游问题流量限流解决后,后续找时间又重新分析了下问题发生的根本原因,现将其总结如下:本篇文章会先对 Mysql 中的各种锁进行分析,包括互斥锁、间隙锁和插入意向锁,让大家对各种锁的使用场景有一个了解,然后在此基础上再对本问题进行分析,希望大家未来再碰到相似场景时,能够快速的定位问题

Mysql 锁机制

在 Mysql 中为了解决对同一行记录并发写的问题,引入了行锁机制,多个事务不能同时对一行数据进行修改操作,当需要对数据库中的一行数据进行修改时,会首先判断该行数据是否加锁,如果没加锁,那么当前事务加锁成功,可以进行后续的修改操作;但如果该行数据已经被其他事务加锁,则当前事务只有等待加锁的事务释放锁后才能加锁成功,继续执行修改操作

本篇文章中所有实验用到的建表语句:

create table `test` (
    `id` int(11) NOT NULL,
    `num` int(11) NOT NULL,
    PRIMARY KEY (`id`),
    KEY `num` (`num`)
) ENGINE = InnoDB;

insert into
    test
values
(10, 10),
(20, 20),
(30, 30),
(40, 40),
(50, 50);




Shared and Exclusive Locks

shared(S) lock 表示共享锁,当一个事务持有某行上的 S 锁后可以对该行的数据进行读操作,通过语句 select … from test lock in share mode 可以添加共享锁,**一般使用的较少,**不做过多阐述

exclusive(X) lock 表示互斥锁,当一个事务对某行数据进行 update 或 delete 操作时都要先获取到该记录上的 X 锁,如果已经有其他事务获取到了该记录上的 X 锁,那么当前事务会阻塞等待直到上一事务释放了对应记录上的 X 锁

S 锁之间不互斥,多个事务可以同时获取一条记录上的 S 锁 X 锁之间互斥,多个事务不能同时获取同一条记录上的 X 锁 S 锁和 X 锁之间互斥,多个事务不能同时获取同一条记录上的 S 锁和 X 锁

当多个事务同时去 update 索引上同一条记录时,都需要先获取到该记录上的 X 锁,所谓的锁也就是会在内存中生成一个数据结构来记录当前的事务信息、锁类型和是否等待等信息。下图中就是 T1 和 T2 同时去更新 id = 30 的这行记录,并且 T1 成功获取到了锁,其在内存中生成的锁结构信息中字段 is_wating 为 false,可以继续执行事务的后续逻辑,而 T2 获取锁失败,则生成的锁结构信息字段 is_wating 为 true,阻塞等待 T1 上的锁释放

互斥锁在 Mysql 日志中的锁信息为:lock_mode X locks rec but not gap

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000000274f; asc     'O;;
 2: len 7; hex b60000019d0110; asc        ;;




Gap Locks

上一小节中介绍了 Exclusive Locks,该锁可以避免多个事务同时对一行记录进行更新操作,但不能解决幻读的问题,所谓的幻读就是指一个事务在前后两次查询同一个范围时,后一次查询到了前一次没有的记录

session Asession B
T1select num from test where num > 10 and num < 15 for update; (0 rows)
T2insert into test values(12, 12);
T3select num from test where num > 10 and num < 15 for update; (1 rows)

在上面这个场景中,session A 分别在 T1、T3 时刻进行了两次范围查询,session B 在 T2 时刻插入了一条该范围内的数据,如果 session A 能在 T3 时刻查询出 session B 插入的数据,就说明发生了幻读。此时只使用互斥锁是无法解决幻读的,因为 num = 12 的记录在数据库中还不存在,不能给其加上互斥锁来防止 T2 时刻 session B 的插入

因此为了解决幻读问题,只有引入新的锁机制,也就是间隙锁(Gap Locks)。间隙锁和互斥锁不同,互斥锁是行锁,只会锁定一行特定的记录,而间隙锁则是锁定两行记录之间的空隙,防止其他事务在此间隙中插入新的记录

引入了间隙锁之后,session A 在 T1 时刻会给 id = 20 记录生成一个 Gap Locks,之后 session B 在 T2 时刻想要插入记录时,需要先判断待插入位置的后一条记录上是否存在 Gap Locks,很明显此时 id = 20 的记录上已经存在了 Gap Locks,那么session B 就需要在 id = 20 的记录上生成一个插入意向锁,并进入锁等待

间隙锁在 Mysql 中的锁日志信息如下:lock_mode X locks gap before rec

RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table `test`.`test` trx id 38849 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 8000001e; asc     30 ;;
 1: len 6; hex 00000000969c; asc       ;;
 2: len 7; hex a60000011a0128; asc       (;;
 3: len 4; hex 8000001e; asc     ;;


间隙锁虽然解决了幻读问题,但因每次都会锁住一段间隙,大大降低了数据库整体的并发度,且因间隙锁和间隙锁之间不互斥,不同事务可以同时对同一间隙加上 Gap Locks,这也往往是各种死锁产生的源头

Next-Key Locks

Next-Key Locks 是 (Shard/Exclusive Locks + Gap Locks) 的结合,当 session A 给某行记录 R 添加了互斥型的 Next-Key Locks 后, 相当于拥有了记录 R 的 X 锁和记录 R 的 Gap Locks

在上面 Gap Locks 的例子中事务 1 加的就是 Next-Key Locks,即同时给 id = 20 的记录加了 X 锁和 Gap 锁

在可重复读隔离级别下,update 和 delete 操作默认都会给记录添加 Next-Key Locks,Mysql 中 Next-Key Locks 的锁日志信息为:lock_mode X

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000000274f; asc     'O;;
 2: len 7; hex b60000019d0110; asc        ;


Insert Intention Locks

插入意向锁(Insert Intention Locks) 也是一种间隙锁,由 INSERT 操作在行数据插入之前获取

在插入一条记录前,需要先定位到该记录在 B+ 树中的存储位置,然后判断待插入位置的下一条记录上是否添加了 Gap Locks,如果下一条记录上存在 Gap Locks,那么插入操作就需要阻塞等待,直到拥有 Gap Locks 的那个事务提交,同时执行插入操作等待的事务也会在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但目前处于阻塞状态,生成的锁结构就是插入意向锁

实验模拟如下:

session 1session 2session 3
T1begin;
T2select * from test where id = 25 for update;
T3insert into test values(26, 26); (blocked)
T4insert into test values(26, 26); (blocked)

对于语句 select * from test where id = 25 for update 因当前表中不存在该记录,在可重复读隔离级别下,为了避免幻读,会给 (20, 30] 间隙加上 Gap Locks

从锁日志可以看出 session 1 给记录 30 添加了间隙锁(lock_mode X locks gap before rec)

RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table `test`.`test` trx id 38849 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 8000001e; asc     30 ;;
 1: len 6; hex 00000000969c; asc       ;;
 2: len 7; hex a60000011a0128; asc       (;;
 3: len 4; hex 8000001e; asc     ;;




当 session 2 插入记录 26 时,会在 B+ 树中先定位到待插入位置,再判断插入位置的间隙是否存在 Gap Locks,也就是判断待插入位置的后一记录 id = 30 是否存在 Gap Locks,如果存在需要在该记录上生成插入意向锁等待

RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table `test`.`test` trx id 38850 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 8000001e; asc    30 ;;
 1: len 6; hex 00000000969c; asc       ;;
 2: len 7; hex a60000011a0128; asc       (;;
 3: len 4; hex 8000001e; asc     ;;




此时 session 2 和 session 3 都在 id = 30 的记录上添加了插入意向锁等待 session 1 上的 Gap Locks 释放,生成的锁记录如下:

线上问题分析

在对 Mysql 中的各种锁结构有了一个清晰的了解之后,回过头来再看看前面的线上问题

@Transaction
public void service(Integer id) {
    delete(id);
    insert(id);
}


对于上面的业务代码可能存在下面两种情况:

  • 传入的参数 id 在原数据库中不存在
  • 传入的参数 id 在原数据库中存在

本次主要会针对 id 记录在原数据库中不存在进行分析

session 1session 2session 3
T1delete from test where id = 15;
T2delete from test where id = 15;delete from test where id = 15;
T3insert into test values(15, 15);
T4insert into test values(15, 15);
T5insert into test values(15, 15);

因 id = 15 在数据库中不存在,在 T1 时刻 session 1 会给其所在间隙的下一条记录添加上 Gap Locks,又因 Gap Locks 不互斥, 在 T2 时刻 session 2 和session 3 都会同时获取到 id = 20 的 Gap 锁

下图中 tx: T1、T2、T3 分别代表 session 1、session 2 和 session 3

当在 T3 时刻 session 1 插入 id = 15 的记录时,会判断其插入位置的后一条记录是否存在 Gap Locks,如果存在,则需要在该记录上生成 Insert Intention Locks 并等待持有 Gap Locks 的事务释放锁

在 T4 时刻 session 2 执行插入语句,同样会因插入位置的后一条记录中存在 Gap Locks 而需要生成 Insert Intention Locks 等待。此时很明显就形成了死锁,session 1 生成插入意向锁等待 session 2 和 session 3 上的 Gap 锁释放,而 session 2 同样生成插入意向锁等待 session 1 和 session 3 上的 Gap 锁释放

在 T4 时刻检测到死锁后,Mysql 会选择其中一个事务进行回滚,假设此时 session 2 被回滚,释放了其持有的所有锁资源,session 1 可以继续执行吗? 很明显不可以,session 1 还同时在等待 session 3 上的 Gap 锁释放,继续阻塞等待

在 T5 时刻 session 3 开始执行插入语句,此时同 T4 时刻,死锁形成,session 1 生成的插入意向锁正在等待 session 3 上的 Gap Locks 释放,session 3 上生成的插入意向锁正在等待 session 1 上的 Gap Locks 释放,此时 session 3 回滚释放所有锁资源后,session 1 才可以最终执行成功

在完成了三个并发线程的死锁分析后,可能有人会想虽然有死锁,但通过死锁检测可以很快的检测出,程序也可以正常的执行,这有什么问题呢? 其实上面没有问题主要是因为并发量较小,死锁检测可以很快检测出,如果此时将并发量扩大 100 倍甚至 1000 倍后,还会没有问题吗?

看看当时出现线上问题时,接口的调用量情况,

进一步在本地模拟 300 个线程并发执行,因人脑并发分析所有事务的执行情况的话会非常复杂,本次只以事务 1 为一个点来进行分析

从图中可以看到当 T1 在执行插入语句时,需要等待 T2- T101 上持有的 Gap Locks 释放,之后 T2 - T6 可能同时执行插入语句,然后进行死锁检测,事务回滚,看着似乎只要后续有事务执行了插入语句就会执行死锁回滚,正常运行,但在死锁检测的过程中还会有新事务(T101 - T 200 )获取到 Gap Locks,造成锁等待队列中的事务越来越多,**而 Mysql 的整体死锁检测时间复杂度为 O(n^2),**锁等待队列中的事务较多时,每一次有新事务进行锁等待,死锁检测都需要遍历锁等待队列中在其之前等待的事务,判断是否会因自己的加入形成环,此时检测会非常消耗 CPU 资源,造成数据库整体性能下降,死锁检测耗时增加,Mysql 活跃连接数大幅增加,并且因锁等待而连接无法释放,最终造成应用层连接池被打满

综上分析,本次出现问题的最主要原因是在短时间内存在大并发的请求对同一行数据进行先删除再插入操作(先更新再插入同理),造成了死锁等待,应用层连接池被打满,大量上游请求超时重试,进一步导致锁等待,最终影响了所有依赖该数据库的业务

因此对于未来在业务代码中存在相似逻辑的地方,一定要做好防重校验,避免短时间内存在对同一行数据的先更新再插入的并发操作。同时在可重复读隔离别下,更新和删除操作默认都会添加 Next-Key Locks,间隙锁的引入使得死锁问题在并发情况下很容易出现,这也是在业务逻辑实现上需要考虑的问题。

总结

本文以一个线上问题为背景,对 Mysql 中的各种锁机制进行了详细的总结,分析了各个锁的加锁时机和具体使用场景,其中特别要注意间隙锁的使用,因间隙锁和间隙锁之间不互斥,当多个事务之间并发执行时很容易形成死锁

作者:京东物流 张弓言

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1211347.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法通关村——数组中第K大的数字

数组中第K大的数字 1、题目描述 ​ LeetCode215. 数组中的第K个最大元素。给定整数数组nums和整数k&#xff0c;请返回数组中第k个最大的元素。请注意&#xff0c;你需要找的是数组排序后的第k个最大的元素&#xff0c;而不是第k个不同的元素。 示例1&#xff1a; 输入&#…

五、nacos安装指南

Nacos安装指南 1.Windows安装 开发阶段采用单机安装即可。 1.1.下载安装包 在Nacos的GitHub页面&#xff0c;提供有下载链接&#xff0c;可以下载编译好的Nacos服务端或者源代码&#xff1a; GitHub主页&#xff1a;https://github.com/alibaba/nacos GitHub的Release下载…

【Web 实战】记一次攻防实战

经典开局一个登录框 由于漏洞应该还未修复。对于数据和相关网址打个码见谅一下 常规思路&#xff08;爆破&#xff09; 常规操作进行一波 尝试弱口令然后开始爆破 对于此种有验证码的爆破&#xff0c;可以借用一个bp插件。 captcha-killer-modified-jdk14.jar 具体使用我就…

centos8 执行yum install ntpdate命令,报错未找到匹配的参数: ntpdate

1、执行 yum install ntpdate 报错 上次元数据过期检查&#xff1a;1:17:06 前&#xff0c;执行于 2023年11月15日 星期三 10时32分18秒。 未找到匹配的参数: ntpdate 错误&#xff1a;没有任何匹配: ntpdate 报错截图&#xff1a; 2、CentOS8系统中&#xff0c;原有的时间…

GPT 5也要来了?看看​OpenAI CEO Sam Altman最近的采访

OpenAI CEO Sam Altman 在接受金融时报采访中&#xff0c;透露了更多OpenAI的计划&#xff1a;他们正在寻求从微软获得更多资金支持&#xff0c;以构建真正的通用人工智能&#xff08;AGI&#xff09;。同时还透露了关于GPT 5的一些信息和公司AGI愿景目标&#xff01;他认为&am…

使用order by 排序后的是10 6 7 8 9 而不是 6 7 8 9 10?

问题 sql order by 排序后的为什么 是10 6 7 8 9 而不是 6 7 8 9 10? 思路 在 SQL 中&#xff0c;ORDER BY 默认的排序方式是升序&#xff08;从小到大&#xff09;。所以&#xff0c;如果您简单地使用 ORDER BY 对某个列进行排序&#xff0c;它会将数字按照升序排列&#…

物流接单APP源码 货运APP源码 拉货搬家app源码 货运小程序uniapp+thinkphp

拉货搬家大货车货运物流运输货拉拉货跑腿司机接单物流货运 技术栈 : 后端php7.0版本 框架 thinkphp mysq5.6 前端 uniapp 用户列表 用户分组 实名认证 驾驶证认证 车主认证 搬家拉货 优惠营销 微信管理 评论管理

【仿真】ruckig在线轨迹生成器示例

该场景说明了使用 CoppeliaSim 中提供的 Ruckig 在线轨迹生成功能的各种方法&#xff1a; 1. 在线程脚本内使用单个阻塞函数&#xff08;红色&#xff09; 2. 在线程脚本中使用多个非阻塞函数&#xff08;黄色&#xff09; 3. 在非线程脚本中使用多个非阻塞函数&#xff08;…

url找不到404的问题,url被拼接

今天遇到一个测试feign调用的功能&#xff0c;如图所示 先说结论 Controller换成RestController 将日志设置为debug模式 被DispatcherServlet FORWARD了 找到路径 对属性设置断点&#xff0c;看下是哪注进来的 我们再去找encodedPath 此处是undertow的源码&#xff0c;但是und…

实验室EM3电磁铁

锦正茂EM3电磁铁&#xff0c;可以通过更换电磁铁极头在一定范围内改善磁场的大小和磁场的均匀度 &#xff0c;并且可以通过调整极头间距改变磁场的大小。主要用于磁滞现象研究、磁化系数测量、霍尔效应研究、磁光实验、磁场退火、核磁共振、电子顺磁共振、生物学研究、磁性测量…

ERP和MES对接都有什么方式

万界星空科技MES生产管理系统的应用比较广&#xff0c;在和ERP连接时&#xff0c;必须先掌握什么模块是和MES业务流程有关的。 通过详细介绍ERP和MES对接方式的不同特点、应用场景和操作方法&#xff0c;可帮助企业更好地实现信息集成&#xff0c;提升生产效率和质量&#xff…

excel中用NORM.INV函数计算正态累积分布的逆

NORM.INV函数返回正态累积分布的逆。它的形式为NORM.INV(probability,mean,standard_dev)。 正态累积分布函数和正态概率密度函数互为逆。 参数说明&#xff1a; probability&#xff1a;对应正态分布的累积分布值。例如该值等于0.9&#xff0c;表示累积概率之和是0.9Mean&am…

舞台演出控制软件:QLab Pro

QLab Pro是一款功能强大的现场多媒体控制器软件&#xff0c;专为Mac用户设计。它提供了一个直观简洁的用户界面&#xff0c;使得用户能轻松管理和组织所有的媒体资源。QLab Pro支持导入各种音频和视频文件&#xff0c;并具备强大的音频、视频处理和灯光控制功能&#xff0c;可以…

JAVA 中集合取交集

日常工作 经常需要取两个数据集的交集。对常用的List 和Set集合做了一个测试 public static void main(String[] args) {List<Integer> list1 Lists.newArrayList();List<Integer> list2 Lists.newArrayList();Set<Integer> set3 Sets.newHashSet();Set&l…

自定义windows右键菜单,软件卸载后 右键菜单残留 打开方式残留 解决方法

问题&#xff1a; 更改windows右键菜单软件卸载残留&#xff0c;其仍然出现在文件的打开方式列表&#xff0c;右键菜单中。 解决方法1&#xff1a;推荐使用registry workshop批量搜索删除注册表 绿色版&#xff1a; 蓝奏云&#xff1a;https://wwzd.lanzouw.com/iPJNp1em339…

简易搜索引擎SEWeibo

背景 有一组微博事件数据&#xff0c;之前做了一些数据分析与挖掘的工作。想着用C做一个简单的搜索引擎玩玩。 亮点&#xff1a; 搜索支持关系关键字作为搜索条件&#xff0c;以文本情感极性作为初筛条件&#xff0c;以TF-IDF为搜索排序依据以Reactor模式为基础&#xff0c;…

win10关闭讲述人、粘滞键功能的快捷键启动

简单记录下在win10关闭讲述人、粘滞键快速启动的快捷键&#xff0c;这两个功能对正常人没什么用。误触发很烦。 禁用讲述人 按windows键&#xff0c;输入“轻松使用设置”&#xff0c;点“讲述人”&#xff0c;如下图取消讲述人开关和快捷键的勾选。 禁用粘滞键 按windows…

CSGO游戏搬砖还能做吗?CSGO饰品未来走势如何?

CSGO饰品市场会崩盘吗&#xff1f;CSGO还能做多久&#xff1f; 如何看待CSGO饰品市场的整体走向&#xff1f; 从整体来说&#xff0c;CSGO的饰品市场与规模肯定会持续不断的上升&#xff0c;大盘不会发生特别大的波动&#xff0c;目前处于稳定期&#xff01;&#xff01;&…

什么是数据泄露?泄露途径有哪些?企业如何免遭数据泄露?

数据泄露指将机密信息、私人信息或其他敏感信息发布到不安全的环境中。数据泄露可能由意外引起&#xff0c;也可能是蓄意攻击的结果。 每年都有数百万人卷入数据泄露&#xff0c;包括意外看错病人图表的医生&#xff0c;以及大规模尝试访问政府计算机以发现敏感信息。 因为敏…

一文详解oa人事系统!

一、什么是OA系统 OA系统全称为Office Automation&#xff0c;即办公自动化系统。它是一种专门为企业和机构的日常办公工作提供服务的综合性软件平台&#xff0c;具有信息管理、流程管理、知识管理&#xff08;档案和业务管理&#xff09;、协同办公等多种功能。 OA系统可以帮…