线上SQL超时场景分析-MySQL超时之间隙锁 | 京东物流技术团队

news2024/12/23 8:56:41

前言

之前遇到过一个由MySQL间隙锁引发线上sql执行超时的场景,记录一下。

背景说明

分布式事务消息表:业务上使用消息表的方式,依赖本地事务,实现了一套分布式事务方案

消息表名:mq_messages

数据量:3000多万

索引:create_time 和 status

status:有两个值,1 和 2, 其中99%以上的状态都是2,表示分布式事务全部已经执行完成,可以删除。

消息表处理逻辑

1. 启动一个独立的定时任务,删除status=2的历史数据,具体的sql如下:

    delete from mq_messages where create_time<xxx and status=2 limit 200

2. 定时任务执行频率:3分钟跑一次任务,一个任务执行200次 删除。这个条件基本上筛选出了90%以上的数据

业务逻辑:线上业务在执行时,不断的往表里插入status=1的数据,主键id随着时间是递增的

sql超时产生的场景

一次大型促销活动流量峰值的时候,出现了一次数据库连接被打满的情况,初步定位是数据量太大了导致锁表导致的。为了防止数据库连接被再次打满,需要尽快的删除状态为2的数据,手动执行定时任务,删除数据,具体sql为:

delete from mq_messages where status=2 limit 2000

三分钟执行一次任务,一个任务执行200次删除。

然后,数据库连接马上被打满,数据库挂了

复盘分析

线上是否存在表锁?

初始化表结构(简化后的表结构)
CREATE TABLE `my_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) NOT NULL,
  `b` int(11) NOT NULL,
  `state` int(11) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `a` (`a`),
KEY `state` (`state`) USING BTREE
) 
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

存储过程准备测试数据
DELIMITER $$
  CREATE PROCEDURE pro_copy_date()
  BEGIN
  SET @i=1;
  WHILE @i<=100000 DO
  INSERT INTO my_test VALUES(@i,@i,@i,1); 
  SET @i=@i+1;
  END WHILE;
END $$
call pro_copy_date();
UPDATE my_test SET state =2 WHERE id <= 99990;

验证

1. 数据基本情况

表中一共有10万条数据,只有后10条的state=1(id>99990)

2. 事务隔离级别可重复读

3. 开启一个事务A,并且不提交

执行 DELETE FROM my_test WHERE state =2 LIMIT 2000;

4. 开启另一个事务B

• 更新id=2001的数据,可以更新成功

• 更新id=2000的数据,被阻塞

说明没有表锁

5. 开启另一个事务C

• 插入状态为2的数据,可以插入成功

• 插入状态为0的数据,可以插入成功

• 插入状态为1的数据,被阻塞

说明state的1和2的间隙被锁导致不能插入

结论

线上不存在表锁,而是间隙锁

间隙锁

线上间隙锁场景分析

表中state一共两个值1和2。因此会产生三个间隙 (-∞, 1), (1, 2), (2, +∞) 和两个孤值1和2。根据前开后闭原则,对应的临建锁区间为 (-∞, 1], (1, 2],(2, +∞)

执行DELETE FROM my_test WHERE state =2 LIMIT 2000时,扫描到的行数为(state=2, id=1)到(state=2,i d=2000)。state=2落在区间](1,2]。因此锁住的范围是(state=1,id=100000) 到 (state=2,id=2000),如图所示:

对于线上场景锁的范围是(state=1, id=status为1的最大id) 到 (state=2, id=要删除的记录中id的最大值)。由于线上只会插入state=1而且,id是递增的。新插入的id是表的最大值,所以新插入的记录一定会落在锁区间,所以新插入的记录都会被阻塞。

间隙锁作用

解决幻读

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的数据行。

幻读专门指的是新插入的数据。

在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。幻读在“当前读”下才会出现。innodb解决幻读的方法,间隙锁。

幻读带来的问题

新建测试表:

CREATE TABLE `my_test2` (
  `id` INT (11) NOT NULL,
  `b` INT (11) DEFAULT NULL,
  `c` INT (11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE = INNODB;

-- 插入测试数据
NSERT INTO my_test2  VALUES(0, 0, 0),(5, 5, 5),(10, 10, 10),(15, 15, 15);

测试sql 1

begin;
select * from t where b=5 for update;

这个语句会命中 b=5 的这一行,对应的主键 id=5,因此在 select 语句执行完成后,id=5 这一行会加一个写锁,这个写锁会在执行 commit 语句的时候释放。

由于字段 b 上没有索引,因此这条查询语句会做全表扫描。那么,其他被扫描到的不满足条件的记录上,会不会被加锁呢?

假如只会在id为5的记录上加锁:

事务A事务B事务C
T1BEGIN; SELECT * FROM my_test2 where b=5 FOR UPDATE; 结果(5,5,5)
T2UPDATE my_test2 SET b=5 WHERE id = 0;
T3SELECT * FROM my_test2 where b=5 FOR UPDATE; 结果(0,5,0)(5,5,5)
T4INSERT INTO my_test2(1,5,1)
T5SELECT * FROM my_test2 where b=5 FOR UPDATE; 结果(0,5,0)(1,5,1)(5,5,5)
T6commit
事务A事务B
T1BEGIN; SELECT * FROM my_test2 where b=5 FOR UPDATE;
T2UPDATE my_test2 SET b=5 WHERE id = 0; UPDATE my_test2 SET c=5 WHERE id = 0;
T3commit

假如只会在id为5的记录上加锁,会破坏事务A的加锁声明,即“把所有 b=5 的行锁住,不准别的事务进行读写操作

事务A事务B事务C
T1BEGIN; SELECT * FROM my_test2 WHERE b=5 FOR UPDATE; UPDATE my_test2 SET c=10 WHERE b=5;
T2UPDATE my_test2 SET b=5 WHERE id = 0;
T3INSERT INTO my_test2(1,5,1)
T4commit

T1时刻: id=5的这行数据,的c的值改成了10,事务还没提交,binlog还没写

T2时刻:id=0 这一行变成 (0,5,0), 变更写入binlog;

T3时刻:id=1 这一行变成 (1,5,1), 变更写入binlog;

T4时刻:事务A提交,写入binlog。

此时主库的数据为(0,5,0),(1,5,1),(5,5,10)

因此binlog写入的日志为:

UPDATE my_test2 SET b=5 WHERE id = 0;
INSERT INTO my_test2(1,5,1)
UPDATE my_test2 SET c=10 WHERE b=5;

从库执行完成binglog后数据就变成了(0,5,10),(1,5,10),(5,5,10),因此出现了数据的不一致

出现数据不一致的原因,是只锁了那一刻需要变更的行,并不能阻挡现有数据变成b=5

如果把扫描到的行全部加锁会如何哪?由于b没有索引,索引得扫描全表才知道那一行需要更新,所以表中的每一条记录都会被锁住。

事务A事务B事务C
T1BEGIN; SELECT * FROM my_test2 where b=5 FOR UPDATE; UPDATE my_test2 SET c=10 WHERE b=5;
T2UPDATE my_test2 SET b=5 WHERE id = 0; (block)
T3INSERT INTO my_test2(1,5,1)
T4commit

T1时刻: id=5的这行数据,的c的值改成了10,事务还没提交,binlog还没写

T2时刻:id为0的行被锁住,不能更新,等待锁释放;

T3时刻:id=1 这一行变成 (1,5,1), 变更写入binlog;

T4时刻:事务A提交,写入binlog。

T5时刻:事务A已提交,id=0的锁被释放,事务B更新成功,变成 (0,5,0),写入binlog

此时主库的数据为(0,5,0),(1,5,1),(5,5,10)

因此binlog写入的日志为:

INSERT INTO my_test2(1,5,1)
UPDATE my_test2 SET c=10 WHERE b=5;
UPDATE my_test2 SET b=5 WHERE id = 0;

从库执行完成binglog后数据就变成了(0,5,0),(1,5,10),(5,5,10),因此还是存在数据不一致

锁定了查找过程中扫描的行,有效的避免了修改带来的数据不一致问题。数据之间的间隙插入的数据依然会出现b=5的数据,因此要向解决这个问题我们还需在数据的间隙加锁

事务A事务B事务C
T1BEGIN; SELECT * FROM my_test2 b=5 FOR UPDATE; UPDATE my_test2 SET c=10 WHERE b=5;
T2UPDATE my_test2 SET b=5 WHERE id = 0; (block)
T3INSERT INTO my_test2(1,5,1) (block)
T4commit

T1时刻: id=5的这行数据,的c的值改成了10,事务还没提交,binlog还没写

T2时刻:id为0的行被锁住,不能更新等待锁释放;

T3时刻:间隙(0,5)被锁住,不能插入等待锁释放;

T4时刻:事务A提交,写入binlog。

T5时刻:事务A已提交,id=0的锁被释放,事务B更新成功,变成 (0,5,0),写入binlog

T6时刻:事务A已提交,(0,5)的间隙锁被释放,事务C写入成功,变成 (1,5,1),写入binlog

此时主库的数据为(0,5,0),(1,5,1),(5,5,10)

因此binlog写入的日志为:

UPDATE my_test2 SET c=10 WHERE b=5;
UPDATE my_test2 SET b=5 WHERE id = 0;
INSERT INTO my_test2(1,5,1)

从库执行完成binglog后数据就变成了(0,5,0),(1,5,1),(5,5,10),完美解决了数据不一致

通过上面两个情况分析,如果只锁对应修改的行,会出现两个问题

1. 破坏加锁声明

2. 数据的不一致性

幻读的解决方法

通过上面案例分析,即使把所有的记录都加上锁,还是阻止不了新插入的记录。行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)

间隙锁,锁的就是两个值之间的空隙,表中一共有4条数据,因此会产生五个间隙 (-∞, 0), (0, 5), (5, 10), (10, 15), (15, +∞),在扫描确认要修改的行时,不仅仅要锁住扫描到的行,两边的间隙也要加上锁。

间隙锁和行锁合称 next-key lock(邻键锁),每个 next-key lock 是前开后闭区间。因此上述情况会有五个邻键锁(-∞,0],(0,5],(5,10],(10,15],(15, +∞)

间隙锁可以被多个事务同时加

间隙锁和行锁有区别,行锁只能被一个事务加上,但是间隙锁可以被多个事务加上。

如下图:开启两个事务,

1. 事务A执行:SELECT * FROM my_test2 WHERE id=2 for UPDATE; 会锁住(0,5)这个间隙。

2. 事务B执行SELECT * FROM my_test2 WHERE id=3 for UPDATE;,同样也会锁住(0,5)这个间隙,而且可以成功。

间隙锁的目前是保护这个间隙不能插入数据,但他们不冲突。

加锁规则

原则1:加锁的基本单位是 next-key lock,next-key lock 是前开后闭区间。

原则2:查找过程中访问到的对象才会加锁。

优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。

优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

唯一索引上的范围查询会访问到不满足条件的第一个值为止

加锁规则—等值查询间隙锁

事务A执行UPDATE my_test2 SET b=100 WHERE id =7;

根据原则1,加锁的区间应该为(5,10].

根据优化2,这是一个等值查询 ,而 id=10 不满足查询条件,next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)。

因此:事务B的插入会被阻塞,事务C的更新可以成功

事务A:

事务B:

事务C:

加锁规则—非唯一索引等值查询

事务A执行SELECT id FROM my_test2 WHERE c=5 lock in share mode``;

根据原则1,加锁的区间应该为(0,5],由于c不是唯一索引还得往后扫描,因此(5,10]也会被加锁。根据优化2,会退化成(5,10)。因此索引c上的锁区间为(0,10)。

由于这个查询走的是索引覆盖,并不需要去主键索引查数据,因此id=5的行并不会被锁住 。

所以更新会成功,插入不会成功

事务A执行SELECT * FROM my_test2 WHERE c=5 lock in share mode;

由于 查询全部的数据就需要,去主键索引上查找id=5的数据,根据原则2,id=5的这行数据也要被锁住,因此更新会被阻塞。

注意,如果执行的语句为SELECT id FROM my_test2 WHERE c=5 for UPDATE;虽然这个语句也会走索引覆盖,但是用for update mysql会认为你接下来要更新这行,因此顺便会给id=5的这行加锁。

加锁规则—非唯一索引,存在等值

新插入两条数数据(20,20,5)和(30,30,5)

执行sql: DELETE FROM my_test2 WHERE c=5 LIMIT 2;

根据加锁原则,只会扫描c=5的数据,因此加锁区间为

(c=0,id=0) 到 (c=5,id=20)

INSERT INTO my_test2 VALUES(-1,0,0); //不阻塞

INSERT INTO my_test2 VALUES(1,0,0); //阻塞

INSERT INTO my_test2 VALUES(19,0,5); //阻塞

INSERT INTO my_test2 VALUES(21,0,5); //不阻塞

执行结果验证:

数据库底层实现博大精深,本文所述,根据线上场景进行了一些研究和探讨,希望能为相关场景提供一些启示。文章中难免会有不足之处,希望读者能给予宝贵的意见和建议。谢谢!

作者:京东物流 刘浩

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

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

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

相关文章

Facebook广告被暂停是什么原因?Facebook广告账号被封怎么办?

许多做海外广告投放的小伙伴经常遇到一个难题&#xff0c;那就是投放的Facebook广告被拒或 Facebook 广告帐户被关闭赞停的经历&#xff0c;随之而来的更可能是广告账户被封&#xff0c;导致资金的损失。本文将从我自身经验&#xff0c;为大家分享&#xff0c;Facebook广告被暂…

kafka 集群企业部署最佳实践

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

Git安装配置保姆级教程和Git创建仓库的基本原理和常用命令

目录 前言 一、Git简介 1.Git 与 SVN 区别点 2.Git的介绍 3.Git 工作流程 4.Git 工作区、暂存区和版本库 二、Git安装配置 1.Linux 平台上安装 2.Windows 平台上安装 三、Git 创建仓库和下载 1、首先需要注册一个gitee账号 2.git初始化并提交到远程仓库 3.另一用户…

chrome 的vue3的开发者devtool不起作用

问题&#xff1a; 刚刚vue2升级到vue3&#xff0c;旧的devtool识别不了vue3数据。 原因&#xff1a; devtool版本过低。升级到最新。 解决&#xff1a; 去github下载vuetool项目代码&#xff1a; GitHub - vuejs/devtools: ⚙️ Browser devtools extension for debugging…

Linux学习笔记之五(父子进程、孤儿进程、僵尸进程、守护进程)

Linux 1、进程1.1、进程的六种状态1.2、创建子进程1.3、添加子进程任务1.4、孤儿进程、僵尸进程、守护进程1.4.1、避免僵尸进程1.4.2、创建守护进程1.4.3、杀死守护进程 1.5、综合练习 1、进程 进程可以简单的理解为一个正在执行的程序&#xff0c;它是计算机系统中拥有资源和…

django建站过程(4)创建文档显示页面

django建站过程&#xff08;4&#xff09;创建文档显示页面 创建文档显示页面项目主文件夹schoolapps中的文件urls.py在APP“baseapps”中创建url.py文件编写视图模板继承bootstrap创建head.html创建doclist.html创建docdetail.html 使用 markdown 编辑器安装模块Model 模型的d…

Hello World背后的逻辑

一门语言的开发入门&#xff0c;总是抬手就能整出一个「Hello World Demo」。比如下面这样&#xff1a; 显然&#xff0c;熟悉 iOS 开发的同学都知道&#xff0c;上面这个来自 Objective-C。 今天&#xff0c;我们就从这熟悉的代码入手&#xff0c;来一起研究研究「Hello Worl…

泄露35TB数据,医疗巨头Henry Schein遭受黑猫勒索组织攻击

近日&#xff0c;据Bleeping Computer 网站消息&#xff0c;BlackCat&#xff08;黑猫&#xff09;勒索软件团伙将医疗保健巨头Henry Schein 添加到了其暗网泄露网站&#xff0c;并声称其破坏了该公司的网络&#xff0c;窃取了35 TB的敏感文件&#xff0c;这些文件包括了Henry …

BES 在大规模向量数据库场景的探索和实践

导读 本文整理自 2023 年 9 月 5 日 QCon 全球软件开发大会 2023 北京站 —— 向量数据库分论坛的同名主题演讲《BES 在大规模向量数据库场景的探索和实践》。 全文5989字&#xff0c;预计阅读时间15分钟。 向量数据库是一种专门用于存储和查询向量数据的数据库系统。通过 Emb…

verdi如何打开时可以加载配置比如字体

打开tcl使能 找到配置字体的命令 其实其他有需要的文件配置都可以在这里找到对应的指令 存储文件 新建verdi001.tcl文件 输入想要调整的字体以及大小 verdiSetFont -font "Bitstream Vera Sans" -size "18" verdiSetFont -monoFont "Courier&q…

【 云原生 | K8S 】kubectl 详解

目录 1 kubectl 2 基本信息查看 2.1 查看 master 节点状态 2.2 查看命名空间 2.3 查看default命名空间的所有资源 2.4 创建命名空间app 2.5 删除命名空间app 2.6 在命名空间kube-public 创建副本控制器&#xff08;deployment&#xff09;来启动Pod&#xff08;nginx-wl…

做哪些副业可以日赚一百?对程序员来说简直不要太容易!

日赚一百&#xff1f;对程序员来说简直不要太容易&#xff01;下面给程序员们推荐一些日赚100的副业&#xff1a; ①外包接单 程序员简单粗暴赚钱的副业之一。 外包接单的类型包括但不限于&#xff1a;软件开发、硬件开发、小程序功能开发、web开发……大到一个系统的开发、…

什么样的CRM系统更适合外贸企业?

外贸CRM系统作为外贸客户关系管理的工具&#xff0c;已经成为了当下外贸企业对外贸易过程中不可或缺的一环。那什么样的CRM系统更适合外贸企业&#xff1f;小Z向您推荐Zoho CRM。下面说说它到底有什么好处和作用。 一、搭建更高效的客户关系管理系统 外贸企业从前期推广、开发…

202205(第13届)蓝桥杯Scratch图形化编程青少组(国赛_中级)真题

202205(第13届)蓝桥杯Scratch图形化编程青少组(国赛_中级)真题 第 1 题 以下程序&#xff0c;小猫在移动完成后不能回到初始位置的是&#xff1f;&#xff08; &#xff09; A&#xff1a; B&#xff1a; C&#xff1a; D&#xff1a; 第 2 题 以下程序&#xff0c;询问…

SAP 10策略测试及简介

从今天开始将把PP模块中常用的一些策略进行一个测试,编写成系统的文档,有点策略经常不用自己都忘了一些策略的特性。所以还是有必须形成文档的形式记录下来 1、首先准备好物料 成品物料为AB0,在MRP3视图中维护对应的策略组的10 同时选择消耗模式为2.消耗期间都是999 2、其他…

环境变量小结

一 常见环境变量介绍 1 PATH 到了现在&#xff0c;我们也知道我们轻轻敲下ls指令&#xff0c;其实会转为一个可执行文件在运行&#xff0c;也就变成了一个进程&#xff0c;所以ls是让文件运行&#xff0c;./test也是让文件运行&#xff0c;凭什么我们的可执行文件就要加个./(这…

IDEA调试总结

前言 由于 IDEA 每个人使用的版本不同以及快捷键的设置不同&#xff0c;所以忽略了快捷键的使用。如果不知道快捷键请在 IDEA 工具栏里面点开 Run 菜单即可知悉 图标介绍 下面咱们进入看图说话环节&#xff0c;下列图标小伙伴知道是啥功能么&#xff1f;日常开发进行 Debug 使…

Spring-Security前后端分离权限认证

前后端分离 一般来说&#xff0c;我们用SpringSecurity默认的话是前后端整在一起的&#xff0c;比如thymeleaf或者Freemarker&#xff0c;SpringSecurity还自带login登录页,还让你配置登出页,错误页。 但是现在前后端分离才是正道&#xff0c;前后端分离的话&#xff0c;那就…

React状态管理方案盘点

您好&#xff0c; 如果喜欢我的文章或者想上岸大厂&#xff0c;可以关注公众号「量子前端」&#xff0c;将不定期关注推送前端好文、分享就业资料秘籍&#xff0c;也希望有机会一对一帮助你实现梦想 前言 本文不会介绍各个状态管理工具的具体使用或者如何二次封装&#xff0c…

自动驾驶系统激光雷达传感器反射率标定板

自动驾驶技术正在全球范围内快速发展和推广。在中国&#xff0c;自动驾驶技术也得到了高度重视和大力支持。中国政府已经出台了一系列政策&#xff0c;推动自动驾驶技术的发展和应用。例如&#xff0c;上海、北京等地已经开放了自动驾驶测试道路&#xff0c;并开展了自动驾驶公…