惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
小众软件
小众软件
F
Fortinet All Blogs
爱范儿
爱范儿
WordPress大学
WordPress大学
N
Netflix TechBlog - Medium
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
C
Check Point Blog
博客园 - 聂微东
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
aimingoo的专栏
aimingoo的专栏
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
A
About on SuperTechFans
博客园 - 【当耐特】
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
宝玉的分享
宝玉的分享
Jina AI
Jina AI
H
Hackread – Cybersecurity News, Data Breaches, AI and More

Hsu Yeung 的博客

三圣花市 | Hsu Yeung 的博客 网站支持 Live Photo 图片展示 | Hsu Yeung 的博客 梦 | Hsu Yeung 的博客 周末午餐 | Hsu Yeung 的博客 张家界 | Hsu Yeung 的博客 话剧《寻她芳踪·张爱玲》 | Hsu Yeung 的博客 博客自动生成文章目录 | Hsu Yeung 的博客 灭螂行动! | Hsu Yeung 的博客 张靓颖成都演唱会 | Hsu Yeung 的博客 最近对博客做的一些微调总结 | Hsu Yeung 的博客 Windows 上安装 Lua | Hsu Yeung 的博客 网站支持展示 B 站 iframe 视频 SpringBoot 中使用 RedisTemplate 存取整数值的坑 | Hsu Yeung 的博客 SQL JOIN 中 ON 与 WHERE 的区别 值得记录的一些工作近况 | Hsu Yeung 的博客 周传雄成都演唱会 | Hsu Yeung 的博客 并行操作导致获取数据库连接超时 | Hsu Yeung 的博客 风信子 | Hsu Yeung 的博客 Linux 安装并配置 Nginx | Hsu Yeung 的博客 使用 logrotate 切割 nginx 日志 郁金香土培结果 | Hsu Yeung 的博客 MySQL LEFT JOIN 右表有多条数据但只取最新的一条 | Hsu Yeung 的博客 从零开始挑选相机 | Hsu Yeung 的博客 郁金香 | Hsu Yeung 的博客 我的第一束花 | Hsu Yeung 的博客 快乐的一周 | Hsu Yeung 的博客 重庆,来了 | Hsu Yeung 的博客 散步 | Hsu Yeung 的博客 Git 工作区、暂存区、版本库之间的关系 | Hsu Yeung 的博客 Collectors.toMap() 操作 value 为 null 的情况
获取数据库锁等待超时问题 | Hsu Yeung 的博客
2024-03-21 · via Hsu Yeung 的博客

前言

最近在 QA 环境测试订单退款流程审批通过的功能的时候看见 “Lock wait timeout exceeded; try restarting transaction” 的异常,但是这个业务逻辑并不复杂,怎么会出现锁等待超时的问题呢?跟着代码梳理了下整体的方法调用链,大致逻辑如下:


  1. 查询订单退款记录数据
  2. 更新订单退款记录的一些状态、备注等字段
  3. 调用订单退款服务
    1. 调用公共退款服务
    2. 根据退款发起结果以新事务的方式更新订单退款记录

其中第 3 步的根据退款发起结果以新事务的方式更新订单退款记录这一操作调用的方法上开启了事务并且指定了事务传播机制为 REQUIRES_NEW,再看第 2 步的操作,也是在对订单退款记录做更新,且更新时已经开启了事务,这下就明白问题所在了。

案发现场

问题代码如下(适当对方法和参数做了调整、注释,只展示了复现问题必要的一些逻辑):

订单退款记录服务:

@Transactional(rollbackFor = Exception.class)
@Override
public void auditPass(WorkflowOrderRefundAuditPassReq req) {
    String processInstanceId = req.getProcessInstanceId();
    TxOrderRefund orderRefund = this.checkAndReturnOrderRefundRecord(processInstanceId);
    // ... 其他业务逻辑 ...

    // 修改订单退款记录
    // orderRefund.setXXX();
    updateById(orderRefund);

    // 调用支付退款服务
    orderPayService.refundPay(orderRefund.getOrderCode());
}

支付退款服务:

@Transactional(rollbackFor = Exception.class)
public void refundPay(String orderCode) {
    // ... 其他业务逻辑 ...
    orderRefundService.updateOneNewTran(orderRefund);
}

其中 updateOneNewTran 方法上事务注解为:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)

破案

第 2 步更新订单退款记录时会将该条数据锁住,如果不是同一个事务其他对该条数据进行修改的操作必须要等待前一个事务结束才能获取到锁进行操作。
这里的第 3 步由于事务传播机制是 REQUIRES_NEW,因此会开启一个新的事务,而不是加入当前的事务,因此必须要等待第 2 步的操作结束将事务提交后才能成功拿到锁,但是第 2 步的操作和第 3 步操作在同一个大方法下,因此就形成了一个死循环:2 等着 3 执行完然后提交事务,3 又需要等 2 执行完才能拿到锁执行自己的事务,直到触发数据库的锁等待超时异常。

解决方案

解决方案也比较简单,只需要将第 3 步中的事务传播机制改为默认的 Requires 即可,让其加入已有事务而不是开启一个新的事务。

当然,还有其他解决方案,比如将第 2 步的订单退款记录更新操作放到第 3 步的退款操作中去,这样就不会存在两个事务竞争同一个锁的情况了。只要能破坏锁竞争的条件,都是可行的解决方案,根据实际情况选择一种合适的方案即可。

温故知新

  1. Spring 事务传播机制
  2. MySQL 学习笔记