一次mysql死锁的排查过程

一次mysql死锁的排查过程  

一、背景  
  17号晚上要吃饭了,看旁边的妹子和佐哥还在调代码,就问了下什么问题啊,还在弄,妹子说,在测试环境测试给用户并发发送卡券时,出现了死锁,但看代码没有死锁,问题如下图 



  看日志确实发生了死锁,按照死锁产生的原因:一般死锁是两把锁两个人争抢,每个人都获得其中一把,谁都不让谁,等待对方释放锁,死循环导致的,图示如下 
 
  
   不过这次说看代码没有问题,感觉这个问题比较诡异,跟他们说先吃饭,吃完,一起群力群策研究研究这个。 

二、问题点  
1. ### SQL: select * from score_user where user_id = ? for update,这个sql查询是发送了死锁 

三、排查过程  
1. 根据经验和死锁产生的条件,猜测代码并发执行,一个线程先锁住了表A的记录,另外一个线程由于原因没有线索表A记录,而锁住了表B的记录,接下来,锁住A记录的线程等待B的锁是否,锁住B的线程等待A的锁释放,所以产生了原因,所以先看代码 
2. 代码如下面所示,可以看到,基本逻辑,都是先插入score_gain_stream 
Java代码   收藏代码
  1. @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED,rollbackFor = Exception.class)  
  2.     public boolean generateScoreInfo(String userId, Integer score,  
  3.             Long scoreRuleId, int scoreType, int scoreStatus, String scoreWay,  
  4.             String orderId, String inviteeId, String reqId, Integer eventVersion) {  
  5.   
  6.         //0:参数判断  
  7.         if(null == score || score <= 0) {  
  8.             log.warn("score null or < 0, userId:" + userId + "scoreRuleId:" + scoreRuleId + ",scoreWay:" + scoreWay);  
  9.             return true;  
  10.         }  
  11.           
  12.         //1:获取用户等级     
  13.         int memberLevel = MemberLevel.GENERAL_MEMBER;  
  14.           
  15.         ScoreUser dbScoreUser = scoreUserManager.getScoreUserByUserIdForUpdate(userId);  
  16.         boolean isCreate = null == dbScoreUser ? true : false;  
  17.         if (!isCreate) {  
  18.             memberLevel = dbScoreUser.getMemberLevel();  
  19.         }  
  20.           
  21.         // 2:构造/生成积分流水  
  22.         ScoreGainStream scoreGainStream = contructSocreGainStream(userId, score, scoreRuleId, scoreType, scoreStatus, scoreWay,  
  23.                 orderId, inviteeId, reqId, eventVersion,memberLevel);  
  24.           
  25.         boolean streamFlag = addScoreGainStream(scoreGainStream);  
  26.           
  27.         if(!streamFlag){  
  28.             log.error("addScoreGainStream error,data:" + scoreGainStream.toString());  
  29.             return false;  
  30.         }  
  31.           
  32.         // 3:判断用户类型  
  33.         if(isCreate){//新增积分用户信息  
  34.             try {  
  35.                 boolean addFlag = addScoreUser(userId, memberLevel, scoreType, score);  
  36.                 if(!addFlag){  
  37.                     log.error("generateScoreInfo addScoreUser error, userId:" + userId + "|" + "score:" + score );  
  38.                     throw new RuntimeException("generateScoreInfo addScoreUser error");  
  39.                 }  
  40.             } catch (Exception e) {  
  41.                 if(e instanceof DuplicateKeyException){  
  42.                     log.warn("addScoreUser DuplicateKeyException,userId:" + userId + "|" + "score:" + score);  
  43.                     //查询用户信息  
  44.                     ScoreUser updateUser = contructUpdateScoreUser(scoreUserManager.getScoreUserByUserIdForUpdate(userId), score, scoreStatus);  
  45.                       
  46.                     boolean flag = scoreUserManager.updateUserScoreInfoById(updateUser) > 0 ? true : false;  
  47.                     if(!flag){  
  48.                         log.error("generateScoreInfo updateUserScoreInfoById error, data:" + updateUser.toString());  
  49.                         throw new RuntimeException("generateScoreInfo updateUserScoreInfoById error");  
  50.                     }  
  51.                       
  52.                     return true;  
  53.                       
  54.                 }else{  
  55.                     log.error("addScoreUser error,userId:" + userId + "|" + "score:" + score, e);  
  56.                     return false;  
  57.                 }  
  58.             }  
  59.               
  60.             return true;  
  61.               
  62.         }else{//更新积分用户信息  
  63.             ScoreUser updateScoreUser = contructUpdateScoreUser(dbScoreUser, score, scoreStatus);  
  64.               
  65.             boolean flag = scoreUserManager.updateUserScoreInfoById(updateScoreUser) > 0 ? true : false;  
  66.             if(!flag){  
  67.                 log.error("generateScoreInfo updateUserScoreInfoById error, data:" + updateScoreUser.toString());  
  68.                 throw new RuntimeException("generateScoreInfo updateUserScoreInfoById error");  
  69.             }  
  70.               
  71.             return true;  
  72.         }  
  73.     }  

3. 看代码,不会发生死锁的,多个线程同时在执行,每个线程都开启事务,每个线程都加锁查询score_user,发现都没有查询到,那么每个线程都执行插入score_gain_stream操作,都成功,接下来,进行插入score_user,这里面只有一个线程可以成功,有唯一主键,其他线程这里会报错,接下来代码抓取异常,进行加锁查询,此时报错,死锁了 

4. 理论上,报错,这里没有涉及争抢资源的情况,大家都在等待score_user释放,就一个锁,怎么会死锁呢,看来代码解决不了问题了 

5. 再去查下mysql的死锁日志,看看死锁具体怎么产生的,如下图链接如何查询死锁日志 
http://825635381.iteye.com/blog/2339503  



看紫色中的三部分, 
TRANSACTION 1292943095需要 
RECORD LOCKS space id 553 page no 376 n bits 368 index `index_user_id` of table `tbj`.`score_user 
这个位置的X锁,一直等待这个X锁 

TRANSACTION 1292943097这个已经持有 
RECORD LOCKS space id 553 page no 376 n bits 368 index `index_user_id` of table `tbj`.`score_user 
这个位置的S锁,这样导致TRANSACTION 1292943095无法在这个位置获得X锁 

TRANSACTION 1292943097这个事务接下来也在 
RECORD LOCKS space id 553 page no 376 n bits 368 index `index_user_id` of table `tbj`.`score_user 
这个位置的等待X锁 

所以问题点有了: 
1. 为什么有一个线程会持有S锁,看前面的代码结构没有加过S锁? 
2. 还有为什么TRANSACTION 1292943097这个事务不能继续加X锁提交? 


6.这边开始排查为什么会有S锁,查了很多资料,终于,在官网文档查询到了,如下 
[b]
Java代码   收藏代码
  1. INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.Prior to inserting the row, a type of gap lock called an insertion intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock.   
  2. 大体的意思是:insert会对插入成功的行加上排它锁,这个排它锁是个记录锁,而非next-key锁(当然更不是gap锁了),不会阻止其他并发的事务往这条记录之前插入记录。在插入之前,会先在插入记录所在的间隙加上一个插入意向gap锁(简称I锁吧),并发的事务可以对同一个gap加I锁。如果insert 的事务出现了duplicate-key error ,事务会对duplicate index record加共享锁。这个共享锁在并发的情况下是会产生死锁的,比如有两个并发的insert都对要对同一条记录加共享锁,而此时这条记录又被其他事务加上了排它锁,排它锁的事务提交或者回滚后,两个并发的insert操作是会发生死锁的。  
[/b] 

原理分析:这就找到上面问题为什么加上S锁的问题,当并发插入时,出现duplicate异常时,mysql会默认加上S锁,这就是为什么会出现死锁日志里面有个事务加上S锁了,也就同时解释了第二个问题,为什么事务没能提交,因为第一个事务也发生了duplicate异常,同时也对同一个位置加上了S锁,这样就出现了一种情况,多个线程对同一个位置持有S锁,每个线程都去这个位置争抢X锁,S和X锁两者是互斥关系,所以出现循环等待,死锁就此产生
 

关于mysql锁的机制,单独写个博客来介绍 

四、解决办法  
1. 并发插入时,不在一个事务内进行再次事务提交 
2. 通过其他手段,如预创建账户,解决这个要并发插入的问题 
3. 改并发为串行执行 

五、解决过程  

六、问题总结  
1. mysql并发插入,出现duplicate时,会默认加S锁,这个坑啊,坑啊,要研究下为什么这么加  



七、为什么会发生?  
1. 知识体系,需要再次完善,技术无止境 
  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值