`
无量
  • 浏览: 1133898 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论
阅读更多
一次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
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED,rollbackFor = Exception.class)
	public boolean generateScoreInfo(String userId, Integer score,
			Long scoreRuleId, int scoreType, int scoreStatus, String scoreWay,
			String orderId, String inviteeId, String reqId, Integer eventVersion) {

		//0:参数判断
		if(null == score || score <= 0) {
			log.warn("score null or < 0, userId:" + userId + "scoreRuleId:" + scoreRuleId + ",scoreWay:" + scoreWay);
			return true;
		}
		
		//1:获取用户等级   
		int memberLevel = MemberLevel.GENERAL_MEMBER;
		
		ScoreUser dbScoreUser = scoreUserManager.getScoreUserByUserIdForUpdate(userId);
		boolean isCreate = null == dbScoreUser ? true : false;
		if (!isCreate) {
			memberLevel = dbScoreUser.getMemberLevel();
		}
		
		// 2:构造/生成积分流水
		ScoreGainStream scoreGainStream = contructSocreGainStream(userId, score, scoreRuleId, scoreType, scoreStatus, scoreWay,
				orderId, inviteeId, reqId, eventVersion,memberLevel);
		
		boolean streamFlag = addScoreGainStream(scoreGainStream);
		
		if(!streamFlag){
			log.error("addScoreGainStream error,data:" + scoreGainStream.toString());
			return false;
		}
		
		// 3:判断用户类型
		if(isCreate){//新增积分用户信息
			try {
				boolean addFlag = addScoreUser(userId, memberLevel, scoreType, score);
				if(!addFlag){
					log.error("generateScoreInfo addScoreUser error, userId:" + userId + "|" + "score:" + score );
					throw new RuntimeException("generateScoreInfo addScoreUser error");
				}
			} catch (Exception e) {
				if(e instanceof DuplicateKeyException){
					log.warn("addScoreUser DuplicateKeyException,userId:" + userId + "|" + "score:" + score);
					//查询用户信息
					ScoreUser updateUser = contructUpdateScoreUser(scoreUserManager.getScoreUserByUserIdForUpdate(userId), score, scoreStatus);
					
					boolean flag = scoreUserManager.updateUserScoreInfoById(updateUser) > 0 ? true : false;
					if(!flag){
						log.error("generateScoreInfo updateUserScoreInfoById error, data:" + updateUser.toString());
						throw new RuntimeException("generateScoreInfo updateUserScoreInfoById error");
					}
					
					return true;
					
				}else{
					log.error("addScoreUser error,userId:" + userId + "|" + "score:" + score, e);
					return false;
				}
			}
			
			return true;
			
		}else{//更新积分用户信息
			ScoreUser updateScoreUser = contructUpdateScoreUser(dbScoreUser, score, scoreStatus);
			
			boolean flag = scoreUserManager.updateUserScoreInfoById(updateScoreUser) > 0 ? true : false;
			if(!flag){
				log.error("generateScoreInfo updateUserScoreInfoById error, data:" + updateScoreUser.toString());
				throw new RuntimeException("generateScoreInfo updateUserScoreInfoById error");
			}
			
			return true;
		}
	}

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]
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. 
大体的意思是: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. 知识体系,需要再次完善,技术无止境

欢迎大家关注我的公众号:


  • 大小: 52.9 KB
  • 大小: 19.1 KB
  • 大小: 9 KB
  • 大小: 41.8 KB
  • 大小: 49.7 KB
  • 大小: 26.7 KB
1
0
分享到:
评论
2 楼 无量 2016-11-22  
woodding2008 写道
楼主怎么解决的

昨天没时间了,今天继续写
1 楼 woodding2008 2016-11-22  
楼主怎么解决的

相关推荐

    一次Mysql死锁排查过程的全纪录

    在测试环境测试给用户并发发送卡券时,出现了死锁,通过查找相关的资料解决了这个,所以想着总结出来,所以下面这篇文章主要是关于一次Mysql死锁排查过程的全纪录,需要的朋友可以参考下,希望大家从中能有所帮助。

    Mysql解除死锁状态排查

    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; show status like ‘%lock%’等语句排查详解

    一次神奇的MySQL死锁排查记录

    主要给大家介绍了一次神奇的MySQL死锁排查的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Mysql具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

    记一次排查线上MySQL死锁过程,不能只会curd,还要知道加锁原理.doc

    记一次排查线上MySQL死锁过程,不能只会curd,还要知道加锁原理.doc

    MySQL死锁的产生原因以及解决方案

    在实际应用中经常会遇到的与锁相关的异常情况,当两个事务需要一组有冲突的锁,而不能将事务继续下去的话,就会出现死锁,严 重影响应用的正常执行。 在数据库中有两种基本的锁类型:排它锁(Exclusive Locks,即X锁...

    MySQL redo死锁问题排查及解决过程分析

    MySQL Server hang 住,无法测试下去,原生版本不存在这个问题,而新版本上出现了这个问题,不禁心头一颤,心中不禁感到奇怪,还好现场环境还在,为排查问题提供了一个好的环境,随即便投入到紧张的问题排查过程当中...

    PHP 解决session死锁的方法

    当异步请求后台处理一个大数据量操作时 请求其他控制器都没返回信息了。。起初以为是Ext 框架设置了ajax同步造成的。后来发现时session 死锁造成其他控制器在等待session 完成后才能操作。(主要是用户登录判断需要...

    【MySQL面试第二弹】MySQL 服务占用cpu 100%,如何排查问题?

    推荐阅读学习:MySQL最全整理(面试题+笔记+导图),面试大厂不再被MySql难倒! 一、引子 对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),按照本文的步骤排查,基本1分钟即可搞定!...

    InnoDB调试死锁的方法

    • 并发事务,间隙锁可能互斥 (1)A删除不存在的记录,...• 并发插入相同记录,可能死锁(某一个回滚) • 并发插入,可能出现间隙锁死锁(难排查) • show engine innodb status; 可以查看InnoDB的锁情况,也可以调试死锁

    mysql+面试题+2022最新

    当表数据量很大时,建立索引或者修改表结构会很慢,而且在操作的过程中,数据库甚至处于死锁状态,那么有没有其他的好的办法呢? 方式1、“影子策略” 创建一张与原表(tb)结构相同的新表(tb_new) 在新表上创建...

    mysql面试题(涉及索引、事务、锁)

    MySQL遇到的死锁问题、如何排查与解决 索引类别(B+树索引、全文索引、哈希索引)、索引的原理 什么是自适应哈希索引(AHI) 遇到过索引失效的情况没,什么时候可能会出现,如何解决 如何选择合适的分布式主键方案 ...

    Java常见面试题208道.docx

    127.RowBounds 是一次性查询全部结果吗?为什么? 128.mybatis 逻辑分页和物理分页的区别是什么? 129.mybatis 是否支持延迟加载?延迟加载的原理是什么? 130.说一下 mybatis 的一级缓存和二级缓存? 131.mybatis ...

Global site tag (gtag.js) - Google Analytics