终极指南:JGrowing实战案例解析MySQL死锁排查的完整过程记录

终极指南:JGrowing实战案例解析MySQL死锁排查的完整过程记录

【免费下载链接】JGrowing Java is Growing up but not only Java。Java成长路线,但学到不仅仅是Java。 【免费下载链接】JGrowing 项目地址: https://gitcode.com/gh_mirrors/jg/JGrowing

在日常开发中,MySQL死锁问题常常让开发者头疼不已。JGrowing(Java成长路线)项目中的实战案例为我们提供了一次深入理解死锁排查的绝佳机会。本文将通过一个真实案例,详细讲解如何从异常报警到最终解决MySQL死锁问题的完整过程,帮助开发者掌握实用的死锁排查技巧。

问题初现:阳光明媚下午的异常报警 ☀️

某天下午,系统突然抛出事务回滚异常,提示因死锁导致回滚。作为有一定MySQL锁知识基础的开发者,首先想到的是查看InnoDB状态获取死锁信息。通过执行以下命令:

SHOW ENGINE INNODB STATUS

得到的死锁日志显示,两个事务都在争夺同一行记录的X锁(行锁)。但与常见的循环等待型死锁不同,这里两个事务似乎在争夺同一个资源,这让排查工作陷入了困惑。

深入分析:从代码到表结构的全面排查 🔍

业务代码逻辑

查看相关业务代码发现,保存配置的逻辑存在潜在问题:

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);
    }
}

这段代码的逻辑是:尝试插入记录,如果发生唯一索引冲突则进行更新。虽然可以用insert into ... on duplicate key update达到同样效果,但这并不能避免死锁问题。

表结构设计

问题表结构如下(简化处理):

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是唯一索引,插入和更新操作都基于该索引进行。

死锁产生的关键原因

通过分析业务日志发现,多个事务同时执行插入操作,导致唯一索引冲突后进入更新流程。在RR隔离级别下,插入操作需要检测唯一索引冲突,这会获取S锁(共享锁)。当多个事务同时持有S锁并尝试获取X锁(排他锁)时,就会产生死锁。

死锁发生过程演示 🎭

以下是死锁发生的简化时间线:

时间线事务1事务2事务3
1insert into xxinsert into xxinsert into xx
2获取当前行X锁
3 需要检测唯一索引冲突获取S锁,阻塞需要检测唯一索引冲突获取S锁,阻塞
4commit;获取到S锁获取到S锁
5 发现唯一索引冲突,执行Update语句(此时S锁未释放)发现唯一索引冲突,执行Update语句
6 获取该行的X锁,被事务3的S锁阻塞获取该行的X锁,被事务2的S锁阻塞
7 发现死锁,回滚该事务update成功,commit;

小提示:S锁是共享锁,X锁是互斥锁。X锁与S锁、X锁互斥,S锁与S锁不互斥。

解决方案:三种可行方案的对比与选择 ✅

方案一:降低隔离级别

将RR(可重复读)隔离级别降低为RC(读已提交)。RC隔离级别使用快照读,不会加S锁。但修改隔离级别可能影响其他业务,风险较高。

方案二:使用select for update加X锁

在插入前使用select * for update加X锁,避免获取S锁。这是比较理想的解决方案,不需要修改隔离级别,也不会引入分布式组件。

方案三:引入分布式锁

使用Redis或ZK等实现分布式锁,确保同一时间只有一个事务操作该记录。这种方法增加了系统复杂度,适用于更复杂的场景。

最终选择方案二,通过在插入前加X锁的方式避免了S锁竞争,从而解决死锁问题。

总结:死锁排查的黄金法则 📚

排查死锁问题时,不能仅依赖死锁日志,还需要结合业务代码、日志和表结构进行综合分析。掌握MySQL锁机制的基本知识至关重要,推荐阅读JGrowing项目中的为什么开发人员必须要了解数据库锁?一文深入学习。

通过本次实战案例,我们不仅解决了具体的死锁问题,更重要的是掌握了一套系统的死锁排查方法,为今后处理类似问题提供了宝贵经验。记住,在并发编程中,细节决定成败,深入理解底层原理才能写出更健壮的代码。

【免费下载链接】JGrowing Java is Growing up but not only Java。Java成长路线,但学到不仅仅是Java。 【免费下载链接】JGrowing 项目地址: https://gitcode.com/gh_mirrors/jg/JGrowing

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

抵扣说明:

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

余额充值