项目每天需要读取外部原始数据(1000万), 为了加快处理速度进行了优化,并行批量读取数据进行加工,然后并行更新结果到数据表中。如果数据已经存在了就更新,不存在就新增。逻辑如下图:
数据表结构如下:
数据 Upsert逻辑如下:
user_id是主键, 按照主键去更新和新增应该互不影响, 为什么会出现死锁问题呢?
创建一个测试表,并插入2条记录
开启事务1
更新一条不存在的数据 id=2
开启事务2
事务2尝试插入一条id=8新记录,但是被阻塞了。 为什么事务1更新一条不存在的记录id=2会阻塞事务2插入一条id=8的记录呢?
查看当前锁
观察到事务2被一个X, GAP的锁阻了。为什么事务1更新一条不存在的记录id=2会产生X, GAP锁呢? 要说明为什么会有GAP锁需要从幻读说起。
幻读指同一个事务下, 连续执行多次相同SQL导致不同结果, 第二次SQL可能会返回之前不存在的数据。可以参考 MySQL官方文档(https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html)介绍。幻读是事务互相影响的一种现象,例如事务1对一个表中的数据进行了修改。 同时,事务2向表中插入一条新数据。然后事务1会发现还有没有修改的数据行,就好象发生了幻觉一样。
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问并修改该同一数据。由于第二个事务的修改在第一个事务中的两次读数据之间,导致第一个事务两次读到的的数据可能不一样。 不可重复读针对数据修改造成不一致,而幻读针对数据的插入和删除造成不一致.
在隔离级Read Committed, 不会解决幻读, 也就避免不了幻读
这是innodb默认的隔离级别。Innodb使用GAP锁来避免幻读, 即锁定特定数据的前后间隙让数据无法被插入。
Mysql Innodb中锁的实现都是锁住索引, 而不是数据行. GAP锁就是锁定2个临近索引叶子节点的范围. GAP锁不仅仅锁住所需索引(如果锁住的这行不存在)还会锁住一个索引范围,这个范围依据锁住的这条索引数据而定。上下刚好是两个相邻索引叶节点的范围。包含下范围,不包含上范围。 例如执行下面的sql, 会导致产生范围在 [7,10)的GAP锁. 它的目的是避免其他事务在[7,10)插入数据, 也保证了sql语句在当前事务内可以多次执行而得到相同的结果, 避免幻读.
update test set data='' where id=8;
回过头再来看这次并发数据导致的死锁问题。 模拟下面的数据场景。
外部数据加载是按照user_id顺序加载, 但是需要并发处理, 所以数据是不能保证按照user_id顺序更新数据表. user_id会被拆分到不同的更新事务中, 每个事务中的user_id会不连续.
可以看到更新数据被分成了2批, 分别在事务1和事务2中执行。
事务1执行sql产生一个GAP锁[10,15)
update user_asset set data='' where user_id=11;
事务2执行sql产生一个GAP锁[22,30)
update user_asset set data='' where user_id=25;
事务1执行sql
update user_asset set data='' where user_id=29;
user_id=29是在事务2GAP锁[22,30)范围内, 事务1必须等待
事务2执行sql
update user_asset set data='' where user_id=13;
user_id=13是在事务1GAP锁[10,15)范围内, 事务2必须等待
这样事务1和事务2相互等待对方的GAP锁导致死锁.
Mysql的默认隔离级别是Read Repeatable. GAP锁的目的是在隔离级Read Repeatable下避免幻读. 而在隔离级Read Committed下不解决幻读问题, 也就是不会添加GAP锁. 我们只要降低隔离级别到Read Committed就解决问题。
项目开发大部分使用的spring来处理事务. 通过annonation的isolation参数指定隔离级别
调整分片处理逻辑, 保证处理后的数据按照初始分片规则更新到数据库中. 这样保证了在每个分片更新中的user_id是递增长避免每个分片的GAP锁互相干扰,减少死锁可能。
通过观察到的异常栈信息, 找到出问题的代码和相关SQL语句, 初步定位问题
执行select @@global.tx_isolation,可以确定数据库的隔离级别,我们数据库的隔离级别是RC,这样可以很大概率排除gap锁造成死锁的嫌疑;
通过DBA帮忙找到更详细的死锁信息
死锁问题的分析是比较耗精力去找出原因。从我们实践中看, 很多时候是因为Mysql默认的隔离级别Read Repeatable为了解决幻读问题, 而添加了GAP锁。如果业务中对应幻读和可重复读没有要求, 可以快速地尝试着降低事务的隔离级别观察死锁问题是否有所缓解, 然后再来具体分析原因,给出解决方案。访问mysql官方文档(https://dev.mysql.com/doc/refman/5.6/en/innodb-deadlocks.html)了解更多信息。
作者简介
蔡旭, 资深工程师, 负责满帮集团货车帮基础业务中心。
多年企业级系统开发和架构工作经验, 对分布式, 微服务, 高并发的领域均有涉猎。
点击关注我们
热爱技术的小伙伴,欢迎投稿~