提及Mysql死锁,以前写过一次有关Mysql加锁的基本介绍,对于一些基本的Mysql锁或者死锁都有一个简单的认识,能够看下这篇文章为何开发人员须要了解数据库锁。有了上面的经验以后,本觉得对于死锁都能手到擒来,没想到再一个阳光明媚的下午报出了一个死锁,可是这一次却没想象的那么简单。java
在某天下午,忽然系统报警,抛出个异常:mysql
仔细一看好像是事务回滚异常,写着的是由于死锁回滚,原来是个死锁问题,因为我对Mysql锁仍是有必定 了解的,因而开始主动排查这个问题。git
首先在数据库中查找Innodb Status,在Innodb Status中会记录上一次死锁的信息,输入下面命令:github
SHOW ENGINE INNODB STATUS
死锁信息以下,sql信息进行了简单处理:sql
------------------------ LATEST DETECTED DEADLOCK ------------------------ 2019-02-22 15:10:56 0x7eec2f468700 *** (1) TRANSACTION: TRANSACTION 2660206487, ACTIVE 0 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 31261312, OS thread handle 139554322093824, query id 11624975750 10.23.134.92 erp_crm__6f73 updating /*id:3637ba36*/UPDATE tenant_config SET open_card_point = 0 where tenant_id = 123 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 1322 page no 534 n bits 960 index uidx_tenant of table `erp_crm_member_plan`.`tenant_config` trx id 2660206487 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 2660206486, ACTIVE 0 sec starting index read mysql tables in use 1, locked 1 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 31261311, OS thread handle 139552870532864, query id 11624975758 10.23.134.92 erp_crm__6f73 updating /*id:3637ba36*/UPDATE tenant_config SET open_card_point = 0 where tenant_id = 123 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 1322 page no 534 n bits 960 index uidx_tenant of table `erp_crm_member_plan`.`tenant_config` trx id 2660206486 lock mode S *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 1322 page no 534 n bits 960 index uidx_tenant of table `erp_crm_member_plan`.`tenant_config` trx id 2660206486 lock_mode X locks rec but not gap waiting *** WE ROLL BACK TRANSACTION (1) ------------
给你们简单的分析解释一下这段死锁日志,事务1执行Update语句的时候须要获取uidx_tenant这个索引再where条件上的X锁(行锁),事务2执行一样的Update语句,也在uidx_tenant上面想要获取X锁(行锁),而后就出现了死锁,回滚了事务1。当时我就很懵逼,回想了一下死锁产生的必要条件:数据库
public int saveTenantConfig(PoiContext poiContext, TenantConfigDO tenantConfig) { try { return tenantConfigMapper.saveTenantConfig(poiContext.getTenantId(), poiContext.getPoiId(), tenantConfig); } catch (DuplicateKeyException e) { LOGGER.warn("[saveTenantConfig] 主键冲突,更新该记录。context:{}, config:{}", poiContext, tenantConfig); return tenantConfigMapper.updateTenantConfig(poiContext.getTenantId(), tenantConfig); } }
这段代码的意思是保存一个配置文件,若是发生了惟一索引冲突那么就会进行更新,固然这里可能写得不是很规范,其实能够用app
insert into ... on duplicate key update
也能够达到一样的效果,可是就算用这个其实也会发生死锁。看了代码以后同事又给我发了当时业务日志,分布式
能够看见这里有三条同时发生的日志,说明都发生了惟一索引冲突进入了更新的语句,而后发生的死锁。到这里答案终于稍微有点眉目了。学习
这个时候再看咱们的表结构以下(作了简化处理):ui
CREATE TABLE `tenant_config` ( `id` bigint(21) NOT NULL AUTO_INCREMENT, `tenant_id` int(11) NOT NULL, `open_card_point` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uidx_tenant` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
咱们的tenant_id是用来作惟一索引,咱们的插入和更新的where条件都是基于惟一索引来操做的。
UPDATE tenant_config SET open_card_point = 0 where tenant_id = 123
到了这里感受插入的时候对惟一索引加锁有关系,接下来咱们进行下一步的深刻剖析。
上面咱们说有三个事务进入update语句,为了简化说明这里咱们只须要两个事务同时进入update语句便可,下面的表格展现了咱们整个的发生过程:
时间线 | 事务1 | 事务2 | 事务3 |
---|---|---|---|
1 | insert into xx | insert into xx | insert into xx |
2 | 获取当前行X锁 | ||
3 | 须要检测惟一索引是否冲突获取S锁,阻塞 | 须要检测惟一索引是否冲突获取S锁,阻塞 | |
4 | commit; | 获取到到S锁 | 获取到S锁 |
5 | 发现惟一索引冲突,执行Update语句(此时S锁未释放) | 发现惟一索引冲突,执行Update语句 | |
6 | 获取该行的X锁,被事务3的S锁阻塞 | 获取该行的X锁,被事务2的S锁阻塞 | |
7 | 发现死锁,回滚该事务 | update成功,commit; |
小提示:S锁是共享锁,X锁是互斥锁。通常来讲X锁和S,X锁都互斥,S锁和S锁不互斥。
咱们从上面的流程中看见发生这个死锁的关键须要获取S锁,为何咱们再插入的时候须要获取S锁呢?由于咱们须要检测惟一索引?在RR隔离级别下若是要读取那么就是当前读,那么其实就须要加上S锁。这里发现惟一键已经存在,这个时候执行update就会被两个事务的S锁互相阻塞,从而造成上面的循环等待条件。
小提示: 在MVCC中,当前读和快照读的区别:当前读每次须要加锁(可使共享锁或者互斥锁)获取到最新的数据,而快照读是读取的是这个事务开始的时候那个快照,这个是经过undo log去进行实现的。
这个就是整个死锁的缘由,能出现这种死锁的还有一个状况,就是同一时间来三个插入操做,其中先插入的那个事务若是最后回滚了,其他两个事务也会出现这种死锁。
这里的核心问题是须要把S锁给干掉,这里有三个可供参考的解决方案:
第一种方法不太现实,毕竟隔离级别不能轻易的修改。第三种方法又比较麻烦。因此第二种方法是咱们最后肯定的。
说了这么多,最后作一个小小的总结吧。排查死锁这种问题的时候有时候光看死锁日志有时候会解决不了问题,须要结合整个的业务日志,代码以及表结构来进行分析,才能获得正确的结果。固然上面有一些数据库锁的基本知识若是不了解能够查看个人另外一篇文章为何开发人员须要了解数据库锁。
最后这篇文章被我收录于JGrowing-CaseStudy篇,一个全面,优秀,由社区一块儿共建的Java学习路线,若是您想参与开源项目的维护,能够一块儿共建,github地址为:https://github.com/javagrowing/JGrowing 麻烦给个小星星哟。
若是你们以为这篇文章对你有帮助,你的关注和转发是对我最大的支持,O(∩_∩)O: