从储值卡(会员卡)充值业务看分布式事务的设计
看起来是一项很简单的业务,最初我们储值卡团队的实现也确实很简单。我们看看最初的实现:
相信聪明的你一眼就能看出问题:
- 压根没有考虑分布式事务一致性,比如第 12 步根本没有考虑卡系统充值失败的情况该如何处理,而是默认其一定能成功;
看到这里你可能会大呼开发人员是不是没长脑子?
实际情况是,这个版本的开发是几年前的事情了,那时候公司还是创业早期,第一目标是尽快上线能用,而且客户量没有那么大,虽然中间也出现过一些数据不一致的情况,也都通过人工处理了事了。
随着公司业务的发展,用户量越来越大,而且还要和第三方合作(储值卡作为一种支付方式提供给第三方使用),问题出现得也越来越频繁,不得不将这块提上重构议程。
那么,针对上面提的几点问题,我们大体能想到如下重构项:
- 将充值业务逻辑从前端系统剥离,做成单独的服务;
- 在下单前,先调一下卡系统接口,检查用户的充值行为是否合法,避免后面不必要的麻烦;
- 在支付回调中,处理充值失败的场景;
初步设计如下:
这里我们重点讨论下对第 14 步(卡充值接口返回结果)的处理:
- 如果返回充值成功,那万事大吉,该干嘛干嘛;
骚年你等等! 你说什么?重试失败了就去退款?
实践中,远程调用失败的一个很大原因是网络超时(而超时的很大原因又是对方负载过高),而面对超时,我们是不知道对方到底有没有处理成功的,万一这边把钱退掉了,那边又充值成功咋办?(我们是 SaaS 服务商,这时真正的损失方是我们的商户,而商户无疑会找我们索赔的)
该方案在实际中也基本行不通,因为如果那段时间网络有问题或者对方服务器负载高,查询也有很大概率失败,或者就算查成功了并返回充值记录不存在,也有可能之前调的充值接口还在跑(比如处于锁等待状态)。
有人可能会说,没关系啊,就算退款后充值成功了,那后面通过人工或者系统发现数据问题再处理掉不就行了吗?
问题在于,如果在发现问题之前,用户已经从卡上消费掉了呢(比如用户当场冲1000 然后立马消费掉,这在我们实际场景中是经常发生的,因为很多商户会搞充值活动,比如冲1000 送 200)?把卡余额扣成负数?(这不是我杜撰的,在我们老储值卡系统就出现过几次这种情况,当时是直接由公司给商户赔钱)
因此,关键在于,当充值中心不知道卡系统有无充值成功的情况下,需要内部假定充值成功了。
为了防止钱退了后卡又充值成功,定时任务中只处理 1 小时前的数据。
总结:
- 任何涉及到分布式事务的地方都是复杂的,必须小心设计;
- 远程过程处理不具有时序性,设计时必须考虑进去(如退款后最终又充值成功的情况);
- 现实中的设计很多时候做不到完美,我们要做的是保证出现异常的概率最小化并设置最终检查哨兵(上面的定时任务);
- 就算增设了哨兵,也不排除需要人工干预的可能性,因而在设计上尽量保证需要人工干预时有迹可循、方便处理;
- 远程调用需要有重试机制(上面只说了对充值接口的重试,其实其他接口也一样需要有重试机制);
- 记住一句话:网络总是不可靠的;