如何将转账业务多步数据库操作合并为单个事务保证数据一致性
改造方案说明
首先明确核心前提:第三方转账接口属于外部IO操作,耗时不可控,绝对不能包裹在数据库事务内部,否则会导致事务长期持有行锁,引发大量请求阻塞甚至数据库雪崩。我们通过拆分两个独立的原子本地事务+异常兜底的方式,即可保证整个流程的数据一致性。
具体执行流程(含事务边界定义)
第一个本地事务:转账预处理
- 流程开始后先执行
BEGIN开启事务 - 执行更新冻结余额的SQL:
UPDATE User SET hold_balance = hold_balance + 100 WHERE name = 'andy' AND balance >= 100;这里额外加
balance >=100的判断,是为了从数据库层面拦截余额不足的非法转账请求,避免超扣 - 执行插入交易记录的SQL:
INSERT INTO Tx (name, amount, status) VALUES ('andy', 100, 'IN_PROGRESS'); - 以上两个操作都执行成功就执行
COMMIT提交事务,任意一步报错都执行ROLLBACK回滚,整个预处理流程直接终止,不会进入后续步骤 - 事务提交后,再调用第三方转账接口
- 流程开始后先执行
第二个本地事务:结果确认&数据更新
不管第三方接口调用成功、失败、超时还是抛出异常,甚至服务中途中断重启,都要执行这个原子事务:- 先执行
BEGIN开启事务 - 先对当前操作的用户和交易记录加行锁,避免并发修改:
SELECT hold_balance, balance FROM User WHERE name = 'andy' FOR UPDATE; SELECT status FROM Tx WHERE id = [刚才插入的交易ID] FOR UPDATE; - 执行释放冻结余额的SQL:
UPDATE User SET hold_balance = hold_balance - 100 WHERE name = 'andy'; - 再根据第三方接口的返回结果分支处理:
- 接口调用成功:
- 执行扣减余额的SQL:
UPDATE User SET balance = balance - 100 WHERE name = 'andy'; - 执行更新交易状态的SQL:
UPDATE Tx SET status = 'OK' WHERE id = [交易ID];
- 执行扣减余额的SQL:
- 接口调用失败/超时/抛出异常:
- 仅执行更新交易状态的SQL:
UPDATE Tx SET status = 'FAILED' WHERE id = [交易ID];
- 仅执行更新交易状态的SQL:
- 接口调用成功:
- 以上所有操作执行成功就执行
COMMIT提交事务,任意一步报错都执行ROLLBACK回滚即可。
- 先执行
极端异常兜底机制
如果服务在调用第三方接口之后、第二个事务提交之前永久中断,导致交易状态一直停留在IN_PROGRESS,只需要加一个简单的定时调度任务即可兜底:
- 定期扫描创建时间超过阈值(比如5分钟)且状态为
IN_PROGRESS的交易记录 - 调用第三方接口查询该笔交易的实际执行结果,再按照上述第二个事务的逻辑补全数据变更,就能保证最终一致性。
内容的提问来源于stack exchange,提问作者nametal
相关产品推荐
相关产品推荐

