架构误区系列 16:不可靠的幂等
最近有这么一个 case:通过 API 的方式给商户提供外汇锁价的能力。需要给商户提供下单接口,其中单据类型包括支付、退款、拒付、拒付反转;同时考虑到应急的需求,还提供了第五类的单据类型:Cancel,是对前四类的 Void 操作。
开发的同学处理 Cancel 的逻辑是这样的:
先做字段有效性的校验。
再通过 orignRequestId 查询出原单据。
如果查询到原单据,落 Cancel 单并撤销原单。
如果查询不到原单据,返回「原单据不存在」错误。
这里面有一个非常严重的 bug,如果由于商户或者系统的原因,导致 Cancel 单和原单有乱序,同时 Cancel 单商户有重发的场景,商户可能会拿到两个不同的结果:
第一次处理 Cancel 时,原单未到,返回「原单据不存在」;
第二次处理 Cancel 时间,原单已到,返回「处理成功」。
这个缺陷,很大程度上是开发的同学对幂等的理解不深以及对于异常场景的考虑不足导致的。
幂等是和外部系统交互非常重要的一个特性。对于资金支付类的服务,更是产品的生命保证。
幂等逻辑的要求很简单,就是同一个商户,用同一个单据进行交互,需要拿到相同的终态结果。
在处理过程中,可以返回商户非终态结果,比如未知、已受理、处理中,等等。这些不需要一致性。
处理完成,给到用户确定的失败和成功结果后,未来无论用户如何重试,都应该返回相同的结果。
这里面,我们需要充分考虑到各种运维情况,对于一系列特殊场景进行兼容性测试。包括:
机房迁移和扩容,两次请求分别请求到不同的机房。
数据库扩容或者所容(促后),两次请求数据到不同分片。
容灾场景。
缓存和缓存击穿场景。
正确的幂等设计,应该是这样的:
首先应对跨机房场景,需要在一个确定的机房有一份路由表,能明确的路由请求到一个确定的机房处理。可选的方式可以同一个商户在同一个机房处理,或者用机房间通过选举机制保证的一致性存储。
其次在校验之前,就应该需要落单。这里单据可以保存在缓存中,在一定时间段内,通知调用方这个单据号是参数异常的。
通过校验后,这时候单据应该可以正常保存到订单表中。这时候需要先落单,单据处于处理中状态。此时如果商户重试,可以返回处理中结果或者未知异常,不应该返回终态。
接着就进行正常的业务流程处理,可以包括调用外部系统,同时推进单据状态。保证处理和状态的一致性。处理未完成,如果重试,还是返回处理中或者未知异常。
处理到终态,记录最终状态。此时的重试,返回终态。
对于 Cancel 这种需要原单据的场景,也一样遵循先最大可能性落单,再查询原单进行处理的模式。这里可以有两个处理方式:
查询不到原单一直保持在未知异常状态,待原单据后续到达时推进处理。
查询不到直接返回终态的错误码,要求调用方换单再 Cancel。
总结下来,对于幂等逻辑,一定要考虑到各种场景下都可以重试拿到唯一的结果,具体的处理流程就是先受理落单,再异步处理;处理完成前返回非终态结果,处理完成后返回终态结果;调用方对于非终态需要重试或者查询,对终态结果失败重试要换单。
版权声明: 本文为 InfoQ 作者【agnostic】的原创文章。
原文链接:【http://xie.infoq.cn/article/c1a6b8f8ddc41eedc4f102c52】。文章转载请联系作者。
评论