90% 的程序员都认为 innodb 是行级锁,但实际上使用不当,它也是表级锁!
看过我博客的网友都知道,我之前写过一篇文章《MySQL InnoDB存储引擎select和update查询阻塞insert语句,形成表级锁》。拯救过不少人,今天我们再来一次大拯救!
最近生产上的 MySQL 数据库,是不是的就来一次 DeadLock,其中我做了故障排查,昨天做了相关的升级,导致昨天非常的忙,很多网友加我好友,都没有及时回应,直到晚上升级结束,我在群里做了相关的解释!
截了一段错误日志信息如下:
21:02:02.563 ERROR dao.CommonDao [pool-15-thread-19] [jbc.trade.xttblog.com] [703c9ddbe4b143609035365ca46bff35] - db error , tableId=jbc.trade.xttblog.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
其中涉及到的更新语句如下:
update i_pay_record set checktime = now() where order_id in (62818)
很奇妙吧,执行一条 update sql 竟然会有死锁?
具体死锁的提前是 i_pay_record 表中的 order_id 字段有索引。
下面我们通过新建一张表 xttblog,来说明问题。
CREATE TABLE `xttblog` ( `id` BIGINT(20) NOT NULL, `user_id` BIGINT(20) NOT NULL, `item_id` BIGINT(20) NOT NULL, `status` TINYINT(4) NOT NULL, PRIMARY KEY (`id`), KEY `idx_1` (`user_id`,`item_id`,`status`) ) ENGINE=INNODB DEFAULT CHARSET=utf-8
当我们执行下面的 update 语句时,就有可能发生死锁!
update xttblog set status=1 where user_id=? and item_id=?
mysql 的事务支持与存储引擎有关,MyISAM 不支持事务,INNODB 支持事务,更新时可能采用的是行级锁,也可能是表级锁。我们这里采用的是 INNODB 做存储引擎,意味着会将 update 语句做为一个事务来处理。前面的文章中我提到了行级锁必须建立在索引的基础上,上面的更新语句用到了索引 idx_1,所以这里肯定会加上行级锁。
行级锁并不是直接锁记录,而是锁索引(前面的文章也解释过)。
如果一条 SQL 语句用到了主键索引,mysql 会锁住主键索引;如果一条语句操作了非主键索引,mysql 会先锁住非主键索引,再锁定主键索引。
这个 update 语句会执行以下步骤:
- 由于用到了非主键索引,首先需要获取 idx_1 上的行级锁
- 紧接着根据主键进行更新,所以需要获取主键上的行级锁
- 更新完毕后,提交,并释放所有锁
如果在步骤 1 和 2 之间突然插入一条语句:update xttblog …..where id=? and user_id=? 这条语句,那么会先锁住主键索引,然后锁住 idx_1。
这时,悲剧就发生了!
一条语句获取了 idx_1 上的锁,等待主键索引上的锁;另一条语句获取了主键上的锁,等待 idx_1 上的锁,这样就出现了死锁。
很惊奇吧,其实一点也不奇怪,只要你了解了 MySQL 的一些底层设计原理!
那么发生这种问题,有解决方案吗?
当然有了,要不然我写这篇文章干什么?
解决方案,最笨最靠谱的做法就是:先获取需要更新的记录的主键,然后再逐条更新!
-- 先获取需要更新的记录的主键 select id from user_item where user_id=? and item_id=? -- 逐条更新 select id from user_item where user_id=? and item_id=? for (Long id : idList) { userItemDAO.updateStatus(id, userId, 1); } update user_item set status=? where id=? and user_id=?
这样就可以解决问题了,但是这个解决方案与先前的更新语句不一样,先前的更新语句对所有记录的更新在一个事务中,采用循环更新后并不在同一个事务中,所以在 for 循环外面还得开一个事务。
Exception e = (Exception)getDbfeelTransactionTemplate().execute(new TransactionCallback() { public Object doInTransaction(TransactionStatus status) { try { for(Long id:idList) { userItemDAO.updateStatus(id,userId,1); } return null; }catch(DAOException e) { status.setRollbackOnly(); return e; } catch (Exception e) { status.setRollbackOnly(); return e; } } });
在采用 INNODB 的 MySQL 中,更新操作默认会加行级锁,行级锁是基于索引的,在分析死锁之前需要查询一下 mysql 的执行计划,看看是否用到了索引,用到了哪个索引,对于没有用索引的操作会采用表级锁。如果操作用到了主键索引会先在主键索引上加锁,然后在其他索引上加锁,否则加锁顺序相反。在并发度高的应用中,批量更新一定要带上记录的主键,优先获取主键上的锁,这样可以减少死锁的发生。
不是说 update 不会发生死锁,而是你的程序没遇到高并发而已!关于死锁的故障分析排查,我们以后继续!
: » 再议 InnoDB 是行级锁?从一条 update 语句引起的死锁问题来拯救你!
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/252394.html