cover_image

满帮集团|一次项目Innodb死锁分析

蔡旭 满帮技术团队
2018年09月21日 03:05


项目开发中有时会遇到死锁问题。在单线程下运行没有问题,但是多线程就会出现死锁。程序报错如下:


图片

下面我们就来分析一下问题,找出解决方法


 背景及问题介绍

项目每天需要读取外部原始数据(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会发现还有没有修改的数据行,就好象发生了幻觉一样。

不可重复读

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问并修改该同一数据。由于第二个事务的修改在第一个事务中的两次读数据之间,导致第一个事务两次读到的的数据可能不一样。 不可重复读针对数据修改造成不一致,而幻读针对数据的插入和删除造成不一致.

如何避免幻读

1)Read Committed

在隔离级Read Committed, 不会解决幻读, 也就避免不了幻读

2)Read Repeatable

这是innodb默认的隔离级别。Innodb使用GAP锁来避免幻读, 即锁定特定数据的前后间隙让数据无法被插入。

     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 RepeatableGAP锁的目的是在隔离级Read Repeatable下避免幻读. 而在隔离级Read Committed下不解决幻读问题, 也就是不会添加GAP锁. 我们只要降低隔离级别到Read Committed就解决问题。

Spring中指定隔离级

项目开发大部分使用的spring来处理事务. 通过annonation的isolation参数指定隔离级别

图片

方案二 调整数据分片处理逻辑


调整分片处理逻辑, 保证处理后的数据按照初始分片规则更新到数据库中. 这样保证了在每个分片更新中的user_id是递增长避免每个分片的GAP锁互相干扰,减少死锁可

图片

如何定位死锁原因?

1)通过业务异常定位问题

通过观察到的异常栈信息, 找到出问题的代码和相关SQL语句, 初步定位问题

2)确定数据库隔离级别。

执行select @@global.tx_isolation,可以确定数据库的隔离级别,我们数据库的隔离级别是RC,这样可以很大概率排除gap锁造成死锁的嫌疑;

3)找DBA执行下show InnoDB STATUS看看最近死锁的日志。

通过DBA帮忙找到更详细的死锁信息

    结论     


死锁问题的分析是比较耗精力去找出原因。从我们实践中看, 很多时候是因为Mysql默认的隔离级别Read Repeatable为了解决幻读问题, 而添加了GAP锁。如果业务中对应幻读和可重复读没有要求, 可以快速地尝试着降低事务的隔离级别观察死锁问题是否有所缓解, 然后再来具体分析原因,给出解决方案。访问mysql官方文档https://dev.mysql.com/doc/refman/5.6/en/innodb-deadlocks.html)了解更多信息。



作者简介

蔡旭, 资深工程师, 负责满帮集团货车帮基础业务中心。

多年企业级系统开发和架构工作经验, 对分布式, 微服务, 高并发的领域均有涉猎。


图片图片

点击关注我们图片

热爱技术的小伙伴,欢迎投稿~


继续滑动看下一个
满帮技术团队
向上滑动看下一个