大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
上一次
更新文章,还是两个月前,一回想这两个月自己的技术是 一点没进步
,但我也没闲着,每个周末都拿着相机出去拍照,也算是学习到了另外一项技能吧。
人的精力确实是有限的,当我想要在摄影上取得一定成绩时,就不可避免的要投入大量时间,以至于每天晚上以及每个周末都没办法分出时间给到专业技能的提升,但毕竟摄影很难当饭吃,饭碗
还是得保住,所以从现在开始,还是得坚持专业技能的探索与沉淀。
以后每次更新博文,都放几张自己的摄影作品,以证明我花在摄影上的时间,没有白费。😊
前言
最近有一个简单的需求,需要更改数据库表的某个字段长度从varchar(4096) 到varchar(8192),原本以为这个更新动作没有什么风险,毕竟公司的RDS平台提供了完备的线上库DDL语句执行能力,可最终还是发生意外了,在凌晨1点,因为数据库主库和从库的同步延迟大于了30s而触发了告警,组里的人被挨个叫醒。
我们组使用的数据库是MySQL,架构是一主两从的主从架构,提前预告一下,导致主库和从库的同步延迟增加的表因就是改表引发了从库的死锁,如果你对根因感兴趣,那么就跟着我的视角,一起看看这起线上改表事故吧。
正文
一. 死锁四要素
无论是代码中的死锁还是数据库中的死锁,只要发生死锁,那么一定就是满足了如下四要素。
- 互斥。锁同时只能被同一个线程持有;
- 占有且等待。线程在等待另一个锁时不释放当前持有的锁;
- 不可抢占。其它线程无法抢占当前线程持有的锁;
- 循环等待。两个线程相互在等待对方持有的锁。
那么希望在本文后续的阐述中,大家能明白上述四要素是如何被满足的。
二. Gh-ost改表工具
公司对MySQL的在线改表,使用的工具是gh-ost工具,而死锁的发生,和这个gh-ost工具脱不了干系。
先通过下图概览下gh-ost改表的一个工作原理。
对于主库,有如下处理行为。
- 将binlog应用到影子表(ghost table)。这里需要注意的第一点是在将binlog应用到影子表时,如果遇到insert语句,则会替换为replace,需要注意的第二点是主库中将增量的数据replace到影子表,同样会记录到binlog中;
- 原表(original table)分片拷贝数据到影子表。也就是一批一批的将原表数据插入到影子表,这里需要关注的点是分片拷贝数据使用的是insert ignore语句,该语句用于插入时忽略错误,这里主要用于忽略重复键错误;
- 执行cut-over操作。步骤1和步骤2会在主库中交替执行,直到数据全部拷贝完毕,最后会执行cut-over操作,也就是通过表名替换,将影子表替换掉原表。
对于从库,处理行为如下。
- 从库读取binlog。为了保持主从同步,从库会读取主库binlog并回放主库的数据流量,以保持从库和主库的结构和数据一致性;
- 从库并行回放binlog事件。为了提高回放速度以降低主从延迟,从库会采取并行回放的方式来应用binlog事件。
上述的改表流程,会引发一个问题,就是在从库并行读取binlog并回放数据流量时,会并发的执行replace语句,而并发执行replace语句,一定概率会导致死锁的发生,这就是表因。
三. REPLACE语句死锁原理
replace的执行逻辑有两部分,如下所示。
- 如果插入的记录不存在,则直接插入记录;
- 如果存在唯一键冲突,则先删除冲突的旧记录,再插入新记录。
引发死锁的是第二种情况,当走到第二部分逻辑时,会有如下的行为。
- replace插入数据时会检测
唯一键冲突
; - 一旦检测到冲突则会获取next-key lock;
- 随后执行记录删除操作;
- 然后获取insert intention lock。insert intention lock叫做插入意向锁,是X锁,彼此之间不冲突,但和其它X锁以及S锁冲突。
- 最后完成记录插入。
下面用一个例子辅助理解。
首先有如下一张表。
CREATE TABLE test (
id int(11) NOT NULL,
a varchar(10) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY u_k_a (a),
)
并且假设表里已经有一条记录的a字段等于8,此时如果有两个session并发执行replace,如下所示。
# session1
REPLACE INTO t(id, a) VALUES (10, 8);
# session2
REPLACE INTO t(id, a) VALUES (11, 8);
那么会出现下面的情况。
- session1先执行replace,检测到唯一键a发生了冲突,此时会获取(-10, 8]的next-key lock;
- session2随后执行replace,检测到唯一键a发生了冲突,此时会获取(-10, 8]的next-key lock,因为(-10, 8]的next-key lock已经被session1获取,所以session2进入等待;
- session1继续执行删除操作,成功删除;
- session1继续执行插入操作,此时需要获取(-10, 8]的insert intention lock,这一步会阻塞住。
现在思考一下,上述第4步为什么会阻塞住,首先(-10, 8]区间被session1加了next-key lock,尽管insert intention lock和next-key lock是冲突的,但是同一个会话中肯定是可以加成功的,但是注意到(-10, 8]区间有一个session2在等待加next-key lock,正是因为这个,session1无法成功对(-10, 8]区间加insert intention lock,否则对于session2来说,会出现 锁分裂
现象。
这里可以看一下MySQL中加锁对应函数处的注释,如下所示。
This is very important that LOCK_INSERT_INTENTION should not overtake a WAITING Gap or Next-Key lock on the same heap_no, because the following insertion of the record would split the gap duplicatiing the waiting lock, violating the rule taht a transaction can have at most one waiting lock.
回到我们的例子中就是,如果session1加(-10, 8]的insert intention lock成功,那么就有可能会在(-10, 8]这个区间上插入数据,从而session2等待的next-key lock就会分裂成两个next-key lock,这就违反了一个事务只能最多等待一个锁的规则。
最终就是session1要加(-10, 8]的insert intention lock,但因为session2等待获取(-10, 8]的next-key lock,所以session1进入等待,而session2又在等待session1持有的(-10, 8]的next-key lock,从而满足死锁四要素,死锁发生,继而从库读取binlog的线程就夯住了,主从延迟就逐渐增加。
四. 问题解决
问题解决没什么好说的,就是联系DBA把所有集群的MySQL内核版本进行了升级并配置了一个参数,据说这个参数可以在从库里,对并行回放的worker去掉唯一索引检查,既然不检查唯一索引,也就不存在死锁的问题,本文的问题自然就得到解决。
总结
MySQL中的间隙锁或临键锁如果有会话正在等待获取,那么此时是无法对同一区间再加insert intention lock(插入意向锁)的,这个知识点比较冷门,但已经纳入我们组的面试题库,如果面开水厂,可能用得到。
大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈