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的行锁
并发场景下两个事务各持有一把锁,同时等待对方释放持有的锁,就会触发死锁。
具体规避方案
统一全局锁申请顺序
这是从根源破坏循环等待条件的最优方案,不管转账方向,始终按照固定规则申请锁,规则可以根据业务自定义,比如:
- 按表名字典序,始终先操作
bank_1表的记录,再操作bank_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
相关产品推荐
相关产品推荐

