事务中调用三方接口,如何尽可能的保证两侧系统数据的一致性

结论

不可能实现 100% 的数据一致性。因为调用三方接口 与 数据库操作都不是100%成功的操作。如果一方是可以保证 100% 成功的,那么就可以先执行非 100% 成功的操作,确定执行成功后,在执行 100% 成功的操作,进而达到数据的一致性。
比如:事务内调用接口,先调用接口,根据接口的返回状态来提交或回滚事务。看起来这样做一定不会出现问题,但极端情况下,commit 与 rollback 是不一定会执行成功的。所以还是会出现数据不一致的问题。那么需要做的就是尽可能的减少这种概率。

可以知道,接口影响的三方系统数据是不可进行回滚的,但系统自身但数据是可控的。所以,可以将接口调用这一步放在事务方法的最后一步。

  1. 若接口调用成功:提交事务,若提交成功,则数据保证了一致性。虽然仍存在 commit 失败的场景(数据库挂了,谁都没办法),但排除了业务代码发生异常导致了事务回滚,但接口却调用成功的情况。
  2. 若接口调用失败:回滚事务即可,数据保证了一致性。

代码案例

@Service
public class WithdrawalService extends ServiceImpl<WithdrawalRecordMapper, WithdrawalRecord> {
    
    private final RestTemplate restTemplate = new RestTemplate();
    
    @Resource
    private SpringTransactionUtils transactionUtils;
    
    @Resource
    private UserMapper userMapper;
    
    @Resource
    private WithdrawalRecordMapper withdrawalRecordMapper;
    
    public void withdrawal(Long userId, Long amount) {
        WithdrawalService proxy = (WithdrawalService) AopContext.currentProxy();
        WithdrawalRecord withdrawalRecord = proxy.createRecordAndReduceBalance(userId, amount);
        // withdrawalRecord.status: 1.待审核 2.审核驳回 3.提现失败(执行异常) 4.提现成功
        if (withdrawalRecord.getStatus() != 1) {
            throw new BusinessException("用户余额不足");
        }
        
        TransactionStatus transaction = transactionUtils.begin();
        try {
            
            // 1. 修改提现流水为成功
            WithdrawalRecord update = new WithdrawalRecord();
            update.setWithdrawalRecordId(withdrawalRecord.getWithdrawalRecordId());
            update.setStatus(4);
            withdrawalRecordMapper.updateById(update);
            
            // 2. 记录钱包流水
            // ...
            // 3. 其他业务操作
            // ...
            
            
            // end. 模拟调用三方接口
            ResponseEntity<AlipayTransferResponse> response = restTemplate.getForEntity(
                    String.format("http://localhost:8080/alipay/transfer?userId=%s&type=%s", userId, "success"),
                    AlipayTransferResponse.class
            );
            AlipayTransferResponse responseBody = response.getBody();
            
            // 接口成功 commit
            if (response.getStatusCode().is2xxSuccessful() && responseBody.getCode() == 200) {
                // commit 失败保底处理:打印日志,以便后期进行人工数据补偿
                try {
                    transactionUtils.commit(transaction);
                } catch (Exception exception) {
                    log.error("数据库提交失败,请后续人工补偿数据。提现流水主键 [{}]", withdrawalRecord.getWithdrawalRecordId(), exception);
                    throw exception;
                }
            }
            // 接口失败,抛出异常 rollback,并在一个新的事务中修改流水状态为已失败(注意,新事务也可能 commit 失败或代码BUG导致提交失败,需要后期人工补偿)
            else {
                log.error("三方转账接口调用失败 [{}]", responseBody.getMessage());
                throw new AlipayTransferException(responseBody.getMessage());
            }
        }
        // 系统异常(数据库无法连接、代码错误,如空指针等)、业务异常
        catch (Exception exception) {
            log.error("系统异常", exception);
            transactionUtils.rollback(transaction);
            try {
                ((WithdrawalService) AopContext.currentProxy()).updateRecordStatus2Failed(withdrawalRecord, exception.getMessage());
            } catch (Exception updateEx) {
                log.error(
                        "三方转账接口调用失败后,修改提现流水状态与失败原因失败,请后续人工补偿数据。提现流水主键 [{}]",
                        withdrawalRecord.getWithdrawalRecordId(), updateEx
                );
            }
            // 交给 WebGlobalExceptionHandler 处理,响应用户服务异常
            throw new BusinessException("服务异常,提现失败");
        }
    }
    
    
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void updateRecordStatus2FailedInNewTrx(Long withdrawalRecordId, String failedReason) {
        WithdrawalRecord update = new WithdrawalRecord();
        update.setWithdrawalRecordId(withdrawalRecordId);
        update.setFailedReason(failedReason);
        update.setStatus(3);
        withdrawalRecordMapper.updateById(update);
    }
    
    @Transactional(rollbackFor = Exception.class)
    public WithdrawalRecord createRecordAndReduceBalance(Long userId, Long amount) {
        // 1. 生成流水
        WithdrawalRecord withdrawalRecord = new WithdrawalRecord();
        withdrawalRecord.setUserId(userId);
        withdrawalRecord.setUserId(userId);
        withdrawalRecord.setAmount(amount);
        withdrawalRecord.setStatus(1);
        withdrawalRecordMapper.insert(withdrawalRecord);
        // 2. 扣减余额
        int c = userMapper.reduceBalance(userId, amount);
        if (c < 0) {
            withdrawalRecord.setStatus(3);
            withdrawalRecord.setFailedReason("用户余额不足");
        }
        return withdrawalRecord;
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值