线上SQL超时场景分析-MySQL超时之间隙锁

前言

之前遇到过一个由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的记录上加锁:

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

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没有索引,索引得扫描全表才知道那一行需要更新,所以表中的每一条记录都会被锁住。

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的数据,因此要向解决这个问题我们还需在数据的间隙加锁

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); //不阻塞

执行结果验证:

文章转载自:京东云开发者

原文链接:https://www.cnblogs.com/Jcloud/p/17823419.html

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[java基础揉碎]基本数据类型转换

介绍 当java程序在进行赋值或者运算时&#xff0c;精度小的类型自动转换为精度大的数据类型&#xff0c; 这个就是自动类型转换。 数据类型按精度&#xff08;容量&#xff09;大小排序为: 自动类型转换注意和细节 1.有多种类型的数据混合运算时&#xff0c;系统首先自动…

【Linux的权限命令详解】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 shell命令以及运行原理 Linux权限的概念 Linux权限管理 一、什么是权限&#xff1f; 二、权限的本质 三、Linux中的用户 四、linux中文件的权限 4.1、文件访问…

【成本价特惠】招募证书代理:工信部、PMP、阿里云、华为等认证,机会难得!

扫码和我联系 亲爱的读者朋友们&#xff0c; 今天&#xff0c;我想和大家分享一个难得的机会。我们目前正在积极招募各类证书的代理&#xff0c;包括工信部的证书、PMP&#xff08;项目管理专业人士&#xff09;证书、阿里云证书、华为证书、OCP 证书、CFA 证书等。这些证书在…

C++入门学习(七)整型

整型就是整数类型的数据&#xff08;-1&#xff0c;0&#xff0c;1等等&#xff09; 数据类型占用空间取值范围short(短整型)2字节 (-2^15 ~ 2^15-1) 32768~32767 int(整型)4字节(-2^31 ~ 2^31-1)long(长整形) Windows为4字节, Linux为4字节(32位), 8字节(64位) (-2^31 ~ 2^31…

LiteAD对接FusionCompute

2、FA发放云桌面并与FC对接 &#xff08;1&#xff09;创建虚拟机模板 &#xff08;2&#xff09;创建虚拟机命名规则 &#xff08;3&#xff09;创建虚拟机组 &#xff08;4&#xff09;创建桌面组 &#xff08;5&#xff09;创建域用户和组&#xff08;就相当于在Microsoft …

基于SpringBoot Vue家政服务预约平台系统

大家好✌&#xff01;我是Dwzun。很高兴你能来阅读我&#xff0c;我会陆续更新Java后端、前端、数据库、项目案例等相关知识点总结&#xff0c;还为大家分享优质的实战项目&#xff0c;本人在Java项目开发领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#x…

【linux驱动】详细剖析第一个hello word驱动程序

文章目录 驱动程序的框架驱动程序的使用示例 驱动程序的框架 Linux 驱动的基本框架主要由模块加载函数&#xff0c;模块卸载函数&#xff0c;模块许可证声明&#xff0c;模块参数&#xff0c;模块导出符号&#xff0c;模块作者信息等几部分组成&#xff0c;其中模块参数&#…

C++——vector的使用及其模拟实现

vector的使用及其模拟实现 文章目录 vector的使用及其模拟实现1. vector的使用1.1 构造函数construct1.2 获取当前存储的数据个数size()和最大容量capacity()1.3 访问1.3.1 operator[]运算符重载1.3.2 迭代器访问1.3.3 范围for 1.4 容量相关reserve()和resize()1.5 增&#xff…

【趣味题-04】20240120百鸡百钱(遍历循环排除,类似加减法)

背景需求 题目 5&#xff1a;百鸡百钱 作者&#xff1a;阿夏 时间&#xff1a;2024年1月20日百鸡百钱 公鸡每只 5 元&#xff0c;母鸡每只 3 元&#xff0c;小鸡 3 只一元&#xff0c; 现要求用 100 元钱买 100 只鸡(三种类型的鸡都要买)&#xff0c; 问公鸡、母鸡、小鸡各买几…

Oracle 经典练习题 50 题

文章目录 一 CreateTable二 练习题1 查询"01"课程比"02"课程成绩高的学生的信息及课程分数2 查询"01"课程比"02"课程成绩低的学生的信息及课程分数3 查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩4 查询平均成绩小于…

“加密行业迈向上市潮”!全球第二大稳定币发行商,秘密提交IPO申请!

在过去的15年里&#xff0c;虽然加密行业蓬勃发展&#xff0c;但上市公司的数量却相对稀少。然而&#xff0c;随着加密市场在经济走强的背景下出现反弹&#xff0c;今年区块链领域的投资资金和潜在的IPO似乎均有望迎来转机。 高盛预测&#xff0c;2024年下半年IPO活动将更加活跃…

Red Hat Enterprise Linux 8.9 安装图解

引导和开始安装 选择倒计时结束前&#xff0c;通过键盘上下键选择下图框选项&#xff0c;启动图形化安装过程。需要注意的不同主板默认或者自行配置的固件类型不一致&#xff0c;引导界面有所不同。也就是说使用UEFI和BIOS的安装引导界面是不同的&#xff0c;如图所示。若手动调…

设计一个Key-Value缓存去存储最近的Web Server查询的结果

1: 定义Use Case和约束 Use Cases 我们可以定义如下 Scope: User 发送一个 search request, 缓存命中成功返回DataUser 发送一个 search request, 缓存未命中&#xff0c;未成功返回DataService 有高可用 约束和假设 状态假设 Traffic 分布不是均匀的 热度高的查询总是被…

接口测试遇到500报错?别慌,你的头部可能有点问题

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

class_13:静态成员static关键字

#include <iostream>using namespace std;class Myclass{ public:int datas;static int staticValue; //静态成员变量在类外进行初始化void printInfo(){cout<<datas<<endl;}static int getStaticDatas()//静态成员函数不能直接访问非静态变量和非静态函数&a…

Python sleep函数用法:线程睡眠

如果需要让当前正在执行的线程暂停一段时间&#xff0c;并进入阻塞状态&#xff0c;则可以通过调用 time 模块的 sleep(secs) 函数来实现。该函数可指定一个 secs 参数&#xff0c;用于指定线程阻塞多少秒。 当前线程调用 sleep() 函数进入阻塞状态后&#xff0c;在其睡眠时间…

GPT应用_AutoGPT

项目地址&#xff1a;https://github.com/Significant-Gravitas/AutoGPT 1 功能 1.1 整体功能&#xff0c;想解决什么问题 单独使用 ChatGPT 时&#xff0c;只提供基本的聊天&#xff0c;无法实现复杂多步的功能&#xff0c;以及与其它应用交互&#xff0c;如果想提供某种功…

linux(七):I2C(touch screen)

本文主要探讨210触摸屏驱动相关知识。 I2C子系统 i2c子系统组成部分:I2C核心,I2C总线驱动,I2C设备驱动 I2C核心&#xff1a;I2C总线驱动和设备驱动注册注销方法 I2C总线驱动&#xff1a;I2C适配器(I2C控制器)控制,用于I2C读写时序(I2C_adapter、i2c_a…

2023:既是结束也是开始

2023年注定是不平凡的一年&#xff0c;这一年真的经历了很多事&#xff0c;包括学习、生活、工作等等&#xff0c;上半年忙着毕业以及一些其他的事情&#xff0c;很多挖的坑都没来得及填&#xff0c;下半年研一开学以后终于有了足够的时间学习&#xff0c;接下来就用这篇文章来…

MySQL执行计划全面解析

执行计划 如果不知道执行计划&#xff0c;那就不可能进行SQL优化&#xff0c;那么执行计划是什么呢&#xff1f; 使用explain关键字可以模拟优化器执行SQL查询语句&#xff0c;从而知道MySQL是如何处理你的SQL的&#xff0c;进而分析性能瓶颈 用起来其实很简单&#xff0c;使用…