mysql死锁-非主键索引更新引起的死锁

背景:最近线上经常抛出mysql的一个Deadlock,细细查来,长了知识!

分析:错误日志如下:

21:02:02.563 ERROR dao.CommonDao        [pool-15-thread-19] [jbc.trade.qunar.com] [703c9ddbe4b143609035365ca46bff35] - db error , tableId=jbc.trade.qunar.com, sql=update i_pay_record set checktime = now() where order_id in (62818) , par

ams=null

com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

 

其中sql更新语句:

<span style="background-color:#ffffff; color:#f92672">update i_pay_record set checktime = now() where order_id in (<span style="color:#ae81ff">62818)</span></span>

很奇妙吧,执行一条sql会有死锁吗?

答案是:会有。

其中奥妙就在 i_pay_record 中的 order_id 字段有索引。

表结构如下:

<span style="background-color:#ffffff; color:#f92672">CREATE TABLE <span style="color:#e6db74">`user_item` (
  `id` BIGINT(<span style="color:#ae81ff">20) <span style="color:#f92672">NOT <span style="color:#ae81ff">NULL,
  <span style="color:#e6db74">`user_id` BIGINT(<span style="color:#ae81ff">20) <span style="color:#f92672">NOT <span style="color:#ae81ff">NULL,
  <span style="color:#e6db74">`item_id` BIGINT(<span style="color:#ae81ff">20) <span style="color:#f92672">NOT <span style="color:#ae81ff">NULL,
  <span style="color:#e6db74">`status` TINYINT(<span style="color:#ae81ff">4) <span style="color:#f92672">NOT <span style="color:#ae81ff">NULL,
  PRIMARY <span style="color:#f92672">KEY (<span style="color:#e6db74">`id`),
  <span style="color:#f92672">KEY <span style="color:#e6db74">`idx_1` (`user_id`,`item_id`,`status`)
) <span style="color:#f92672">ENGINE=INNODB DEFAULT CHARSET=utf<span style="color:#ae81ff">-8</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

SQL语句如下:

<span style="background-color:#ffffff; color:#f92672">update user_item set status=<span style="color:#ae81ff">1 <span style="color:#f92672">where user_id=? and item_id=?</span></span></span>

原因分析

mysql的事务支持与存储引擎有关,MyISAM不支持事务,INNODB支持事务,更新时采用的是行级锁。这里采用的是INNODB做存储引擎,意味着会将update语句做为一个事务来处理。前面提到行级锁必须建立在索引的基础,这条更新语句用到了索引idx_1,所以这里肯定会加上行级锁。

行级锁并不是直接锁记录,而是锁索引,如果一条SQL语句用到了主键索引,mysql会锁住主键索引;如果一条语句操作了非主键索引,mysql会先锁住非主键索引,再锁定主键索引。

这个update语句会执行以下步骤:

1、由于用到了非主键索引,首先需要获取idx_1上的行级锁

2、紧接着根据主键进行更新,所以需要获取主键上的行级锁;

3、更新完毕后,提交,并释放所有锁。

如果在步骤1和2之间突然插入一条语句:update user_item .....where id=? and user_id=?,这条语句会先锁住主键索引,然后锁住idx_1。

蛋疼的情况出现了,一条语句获取了idx_1上的锁,等待主键索引上的锁;另一条语句获取了主键上的锁,等待idx_1上的锁,这样就出现了死锁。

解决方案

1、先获取需要更新的记录的主键

<span style="background-color:#ffffff; color:#f92672">select id from user_item where user_id=? and item_id=?</span>

2、逐条更新

<span style="background-color:#ffffff; color:#f92672">select id from user_item where user_id=? and item_id=?  
for (Long id : idList) {  
    userItemDAO.updateStatus(id, userId, <span style="color:#ae81ff">1);  
}  
update user_item <span style="color:#f92672">set status=? where id=? and user_id=?</span></span></span>

3、这样貌似解决了,都是对单条进行操作,都是先获取主键上的锁,再获取idx_1上的锁。

不过这个解决方案与先前的更新语句不一样,先前的更新语句对所有记录的更新在一个事务中,采用循环更新后并不在同一个事务中,所以在for循环外面还得开一个事务。

<span style="background-color:#ffffff">Exception e = (Exception)getDbfeelTransactionTemplate().execute(new TransactionCallback() {
	<span style="color:#f92672">public Object doInTransaction(TransactionStatus status) {
		<span style="color:#f92672">try {
		  	for(<span style="color:#e6db74">Long id:idList) {
		<span style=<span style="color:#e6db74">"white-space:pre">	</span>userItemDAO.updateStatus(id,userId,<span style="color:#ae81ff">1)
		}
				<span style="color:#f92672">return <span style="color:#ae81ff">null;
		}catch(DAOException e) {
			status.setRollbackOnly();
			<span style="color:#f92672">return e;
		}
		<span style="color:#f92672">catch (Exception e) {
			status.setRollbackOnly();
			return e;
		}
	}
});</span></span></span></span></span></span></span></span></span></span>

小结:在采用INNODB的MySQL中,更新操作默认会加行级锁,行级锁是基于索引的,在分析死锁之前需要查询一下mysql的执行计划,看看是否用到了索引,用到了哪个索引,对于没有用索引的操作会采用表级锁。如果操作用到了主键索引会先在主键索引上加锁,然后在其他索引上加锁,否则加锁顺序相反。在并发度高的应用中,批量更新一定要带上记录的主键,优先获取主键上的锁,这样可以减少死锁的发生。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东境物语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值