MySQL Gap Lock引发insert语句堵住问题排查

一、众所周知,MySQL在RR隔离级别下,会出现幻读的问题。
出现幻读的前提条件:
Innodb存储引擎,在RR隔离级别下,并且使用了当前读;
出现幻读的表现:
一个事务在前后两次查询同一范围数据的时候(当前读),后一次查询看到了前一次查询没有看到的行。两点需要说明:
1 、在可重复读隔离级别下,普通查询是快照读,不会看到其他事务插入的数据。幻读只在当前读才会出现;
2 、幻读专指新插入的行。当前读的作用就是能读到其他事务已经提交的新插入的记录。

二、幻读带来的影响是会导致主从之间数据不一致,是很严重的问题。
三、如何解决幻读的问题:
产生幻读的原因是:mysql的record lock(行锁)只能锁住行,但是新插入记录这个动作,是更新了记录之间的“间隙”。因此,为了解决幻读,innodb引入了新的锁,也就是间隙锁(Gap Lock)。顾名思义,间隙锁,锁的就是两个值之间的空隙。

四、以上介绍了间隙锁,它能帮我们解决了幻读的问题,但同时也会给我们带来一些“困扰”。接下来结合一个线上的故障case,讲解一下间隙锁,给我们带来了什么样的困扰:
1、故障现象描述:
用户报障,一个insert的SQL语句被堵住了,从DBbrain看到的现象update的SQL持有锁,导致insert语句无法写入:


图片1.png

2、分析:
1)、从上图可以看到,insert语句处于锁等待状态:LOCK WAIT。Dbbrain显示持有锁的事务为:“UPDATE t_push_task SET status=9 WHERE push_id=1384715944290652160 AND access_id=1500015064 limit 1” 以及
“UPDATE t_push_task SET status=9 WHERE push_id=1384729752719482880 AND access_id=1500015064 limit 1”

insert语句为:
INSERT INTO t_push_task SET status=0, access_id=1600007315, type=8, push_req='�????(:?�[{"Ids":["zhihuishu_class_204117682"],"IdsStru":null,"InnerOperator":1,"OuterOperator":1,"IsNot":false,"TagType":"xg_user_define"}]J�00:4895P�Z?���?��?�\n?�{"alert":{"title":"通知","subtitle":"","body":"刘婷班 ......

2)、是否这两个update语句有性能问题呢?


图片2.png

从explain执行计划分析,该SQL扫描的行数只有一行,并且是走主键索引扫描,所以update语句并没有性能问题;

3)、那为什么update语句会把insert语句堵住呢?
我们知道,insert插入记录,更新的是记录之间的“间隙”。那么是否有可能是由于间隙锁的原因,导致insert无法插入呢?

4)、我们来看看表结构,如下:
CREATE TABLE t_push_task (
push_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
group_id varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
status int(8) NOT NULL,
access_id bigint(11) NOT NULL,
type bigint(20) NOT NULL,
target_list mediumtext COLLATE utf8mb4_unicode_ci,
push_req blob,
create_time datetime DEFAULT CURRENT_TIMESTAMP,
push_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
push_node varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
expire_sec int(11) NOT NULL DEFAULT '259200',
start_time datetime DEFAULT NULL,
finish_time datetime DEFAULT NULL,
source int(8) NOT NULL,
msg_type int(8) NOT NULL,
msg_status int(8) NOT NULL DEFAULT '0',
push_content mediumtext COLLATE utf8mb4_unicode_ci,
last_modify_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
global_push_type varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
upload_id bigint(20) unsigned NOT NULL DEFAULT '0',
already_send_num bigint(20) unsigned NOT NULL DEFAULT '0',
queue_id varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT '',
collapse_id int(8) NOT NULL DEFAULT '0' COMMENT '????push_id',
expect_send_num bigint(20) unsigned DEFAULT '0',
current_index int(11) DEFAULT '-1' COMMENT '?????????',
PRIMARY KEY (push_id,push_time),
KEY idx_status_create (access_id,status,create_time),
KEY idx_status_push (access_id,status,start_time),
KEY idx_status_push_queenid (queue_id,status,push_time),
KEY idx_source_type_content_push (access_id,create_time,source,msg_type,push_content(512)),
KEY idx_push_time (push_time),
KEY idx_id_pushtime_type_msgtype_source (access_id,push_time,type,msg_type,source),
KEY idx_id_type_status_pushtime (access_id,type,status,push_time),
KEY idx_id_collapseid (access_id,collapse_id),
KEY id_idx_status (push_id,access_id,status)
) ENGINE=InnoDB AUTO_INCREMENT=500534759 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
/*!50100 PARTITION BY RANGE (to_days(push_time))
(PARTITION p201904 VALUES LESS THAN (737545) ENGINE = InnoDB,
PARTITION p201905 VALUES LESS THAN (737576) ENGINE = InnoDB,
PARTITION p201906 VALUES LESS THAN (737606) ENGINE = InnoDB,
PARTITION p201907 VALUES LESS THAN (737637) ENGINE = InnoDB,
PARTITION p201908 VALUES LESS THAN (737668) ENGINE = InnoDB,
PARTITION p201909 VALUES LESS THAN (737698) ENGINE = InnoDB,
PARTITION p201910 VALUES LESS THAN (737729) ENGINE = InnoDB,
PARTITION p201911 VALUES LESS THAN (737759) ENGINE = InnoDB,
PARTITION p201912 VALUES LESS THAN (737790) ENGINE = InnoDB,
PARTITION p202001 VALUES LESS THAN (737821) ENGINE = InnoDB,
PARTITION p202002 VALUES LESS THAN (737850) ENGINE = InnoDB,
PARTITION p202003 VALUES LESS THAN (737881) ENGINE = InnoDB,
PARTITION p202004 VALUES LESS THAN (737911) ENGINE = InnoDB,
PARTITION p202005 VALUES LESS THAN (737942) ENGINE = InnoDB,
PARTITION p202006 VALUES LESS THAN (737972) ENGINE = InnoDB,
PARTITION p202007 VALUES LESS THAN (738003) ENGINE = InnoDB,
PARTITION p202008 VALUES LESS THAN (738034) ENGINE = InnoDB,
PARTITION p202009 VALUES LESS THAN (738064) ENGINE = InnoDB,
PARTITION p202010 VALUES LESS THAN (738095) ENGINE = InnoDB,
PARTITION p202011 VALUES LESS THAN (738125) ENGINE = InnoDB,
PARTITION p202012 VALUES LESS THAN (738156) ENGINE = InnoDB,
PARTITION p202101 VALUES LESS THAN (738187) ENGINE = InnoDB,
PARTITION p202102 VALUES LESS THAN (738215) ENGINE = InnoDB,
PARTITION p202103 VALUES LESS THAN (738246) ENGINE = InnoDB,
PARTITION p202104 VALUES LESS THAN (738276) ENGINE = InnoDB,
PARTITION p202105 VALUES LESS THAN (738307) ENGINE = InnoDB,
PARTITION p202106 VALUES LESS THAN (738337) ENGINE = InnoDB,
PARTITION p202107 VALUES LESS THAN (738368) ENGINE = InnoDB,
PARTITION p202108 VALUES LESS THAN (738399) ENGINE = InnoDB,
PARTITION p202109 VALUES LESS THAN (738429) ENGINE = InnoDB,
PARTITION p202110 VALUES LESS THAN (738460) ENGINE = InnoDB,
PARTITION p202111 VALUES LESS THAN (738490) ENGINE = InnoDB,
PARTITION p202112 VALUES LESS THAN (738521) ENGINE = InnoDB,
PARTITION p2022 VALUES LESS THAN (741443) ENGINE = InnoDB) */

从表结构可以知道,该表主键为:PRIMARY KEY (push_id,push_time)。而update语句的where条件为:push_id=1384729752719482880 AND access_id=1500015064,故update语句走的是主键索引的前缀索引,长度为8个字节(push_id类型为bigint,长度为8个字节;push_time类型为datetime,长度也是8个字节);
由于push_id是自增字段auto_increment,故insert语句插入的时候,push_id会进行自增,会在当前最大max(push_id)的基础上,自增加1;

5)、用户提供的线索:
用户反馈,由于业务逻辑设计不合理的原因,其update语句中的push_id实际上在表中是并不存在的,并且远远大于当前表中的最大push_id,当前表中最大push_id为:


图片3.png

这就很奇怪了,update语句where 条件的push_id=1384715944290652160在表当中根本就不存在,也就是说该update语句实际上没生效。update语句和insert语句风马牛不相及,为啥update会阻塞insert呢???

6)、测试实例实验一把,看看是否会出现同样的情况?
a、创建表、写入记录
MySQL [(none)]> use test;
MySQL [test]> CREATE TABLE t ( id int(11) NOT NULL, c int(11) DEFAULT NULL, d int(11) DEFAULT NULL, PRIMARY KEY (id), KEY c (c)) ENGINE=InnoDB;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> show create table t\G;
*************************** 1. row ***************************
Table: t
Create Table: CREATE TABLE t (
id int(11) NOT NULL,
c int(11) DEFAULT NULL,
d int(11) DEFAULT NULL,
PRIMARY KEY (id),
KEY c (c)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

MySQL [test]> select * from t;
+----+------+------+
| id | c | d |
+----+------+------+
| 0 | 0 | 0 |
| 5 | 5 | 5 |
| 10 | 10 | 10 |
| 15 | 15 | 15 |
| 20 | 20 | 20 |
| 25 | 25 | 25 |
+----+------+------+
6 rows in set (0.00 sec)

b、开启两个会话,分别是session A、session B
session A:
MySQL [test]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> update t set d=99 where id=100;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0

session A更新了表中不存在的id,并且比当前max(id)还要大。此时session A的事务还未提交;

session B:
MySQL [(none)]> use test;
MySQL [test]> insert into t values(26,26,26);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

session B想要插入比当前比当前max(id)=25要大的一条记录26,结果发现被堵住了,产生了锁等待;

实验结果现象和上述用户的故障case是一样的,为什么会出现这种情况呢?

7)、next-key lock(record lock + gap lock)加锁原则:
原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间;
原则 2:查找过程中访问到的对象才会加锁;
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁;
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁;
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止;

由于用户的表当中并没有push_id=1384715944290652160或者push_id=1384729752719482880,用上述加锁规则来判断:

a、加锁的单位是next-key lock,故“UPDATE t_push_task SET status=9 WHERE push_id=1384715944290652160 AND access_id=1500015064 limit 1”的加锁范围为:
(max(push_id),+∞];
b、根据优化2原则:update语句是一个等值查询(push_id=1384715944290652160),向右遍历比1384715944290652160大的是+∞,而+∞不满足查询条件,故next-key lock退化成间隙锁,锁住的范围为(max(push_id),+∞);
c、由于insert语句写入,push_id会自增,比当前最大的max(push_id)自增加1,所有落入了加锁范围(max(push_id),+∞)内,导致用户的insert语句被阻塞。
d、即:如果索引没有命中,会有间隙锁, 向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间, 锁住整个区间内的数据
由此就能解释,为什么insert会被update阻塞了。将原因同步给用户之后,用户承诺会优化业务代码,规避间隙锁的问题。

8)、思考:
a、上述case是更新了一个比表中max(push_id)还要大的push_id,那如果更新的push_id刚好是表中存在的一条记录,比如更新的是当前的max(push_id)=500536561,那么会如何加锁呢?
根据加锁原则优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。此时只会对push_id=500536561加锁;

b、如果更新的push_id在表中不存在,但是比max(push_id)要小,例如更新的push_id=447689301,介于447689203 和 448344009之间。此时会如何加锁呢?


图片4.png

根据原则1,加锁单位是next-key lock,故加锁的范围是(447689203,448344009];
根据优化2,这是一个等值查询,push_id=447689301,向右遍历且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁,故最终加锁的范围为(447689203,448344009)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容