You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java MySQL事务死锁解决:双向跨行转账并发场景如何规避死锁?

MySQL反向转账并发场景死锁规避方案

原业务实现伪代码如下:

con.setAutoCommit(false);
try {
   SELECT cash FROM bank_1 WHERE user = 1;

   long money = rs.getLong("cash");
   // 余额校验逻辑
   if (money >= transferAmount) {
       UPDATE bank_1 SET cash=cash-transferAmount WHERE user = 1;
       UPDATE bank_2 SET cash=cash+transferAmount WHERE user = 2;
   }
   con.commit();
} catch(Exception e1) { 
   con.rollBack(); 
} finally { 
    con.setAutoCommit(true);
    try { con.close(); } catch(Exception e) {}
}

死锁产生的核心原因

当前场景死锁的本质是两个并发事务的锁申请顺序相反,触发了死锁四大必要条件中的循环等待:

  • 正向转账事务(1号用户转2号用户):先持有bank_1.user=1的行锁,再申请bank_2.user=2的行锁
  • 反向转账事务(2号用户转1号用户):先持有bank_2.user=2的行锁,再申请bank_1.user=1的行锁
    并发场景下两个事务各持有一把锁,同时等待对方释放持有的锁,就会触发死锁。

具体规避方案

  • 统一全局锁申请顺序

这是从根源破坏循环等待条件的最优方案,不管转账方向,始终按照固定规则申请锁,规则可以根据业务自定义,比如:

  1. 按表名字典序,始终先操作bank_1表的记录,再操作bank_2表的记录
  2. 按用户ID大小,始终先操作用户ID更小的账户记录
    即便是反向转账场景,也按照约定的顺序先加锁靠前的资源,再加锁靠后的资源,完全避免锁顺序冲突的可能。
  • 用SELECT ... FOR UPDATE提前加行锁

原代码中的SELECT是快照读,不会加行锁,行锁是在执行UPDATE时才申请,可能拉长锁申请的时间窗口。可以将余额查询改为当前读,查询时就锁定对应行:

SELECT cash FROM bank_1 WHERE user = 1 FOR UPDATE;

既可以避免查询到的余额和实际更新时的余额不一致的问题,也可以提前完成锁申请,缩短暂态窗口。

  • 缩短事务执行时长

将所有和数据库操作无关的业务逻辑(比如参数校验、非DB侧的计算)全部挪到事务外执行,事务内部只保留必要的数据库读写、提交操作,减少锁的持有时间,降低死锁概率。

  • 设置锁等待超时兜底

可以根据业务需要调整MySQL的innodb_lock_wait_timeout参数(默认50秒),或者在业务代码层面为事务设置超时时间,一旦锁等待超过阈值自动回滚事务,释放持有的锁,避免死锁长期阻塞业务。


内容的提问来源于stack exchange,提问作者pokemon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 12:06:01