结论
不可能实现 100% 的数据一致性。因为调用三方接口 与 数据库操作都不是100%成功的操作。如果一方是可以保证 100% 成功的,那么就可以先执行非 100% 成功的操作,确定执行成功后,在执行 100% 成功的操作,进而达到数据的一致性。
比如:事务内调用接口,先调用接口,根据接口的返回状态来提交或回滚事务。看起来这样做一定不会出现问题,但极端情况下,commit 与 rollback 是不一定会执行成功的。所以还是会出现数据不一致的问题。那么需要做的就是尽可能的减少这种概率。
可以知道,接口影响的三方系统数据是不可进行回滚的,但系统自身但数据是可控的。所以,可以将接口调用这一步放在事务方法的最后一步。
- 若接口调用成功:提交事务,若提交成功,则数据保证了一致性。虽然仍存在 commit 失败的场景(数据库挂了,谁都没办法),但排除了业务代码发生异常导致了事务回滚,但接口却调用成功的情况。
- 若接口调用失败:回滚事务即可,数据保证了一致性。
代码案例
@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;
}
}