之前写过一篇《系统设计的幂等性》科普文章。
幂等性原本是数学上的概念,用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。
对于本文讨论的资金类服务类,如果幂等设计不合理,则会出现一笔交易重复打款的情况,这就出现了资损。
01/ 为什么会产生接口幂等性问题?
其实可以分两类:
一类是受不可抗且非常规操作导致的重复请求,例如网络波动引起的重复请求;用户重复操作,用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易应用。
另一类则是请求失败需要重复发起请求的场景,例如用户请求受理成功,需要异步通过定时任务重复执行的情况。
02/ 如何保证接口幂等性?
幂等的核心是确保唯一性。常规的方法有:
唯一索引:在数据库中建立唯一索引,用作幂等键,可以防止插入重复的数据。
状态机约束: 在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
Token机制:其实本质和唯一索引类似。
摆了这么多,下面引出本文讨论的重点:资金类服务如何做幂等设计与测试。
当然回答问题前,还是要聊一下创作来源,这个话题的来源同样来源于对线上问题的思考。下面我娓娓道来。
03/ 问题背景
**退款服务的场景如下:**第一次请求:如果用户首次请求应用B失败,则应用A落失败的单据A-1,状态=FAIL;应用B则落受理成功的单据B-1,状态=INIT。初次请求报文如下:
{
"requestId": String,
"amount":Moeny,
"refundDetialList":[
refundDetial-1,
refundDetial-2
]
}
第二次幂等请求:后台重试发起幂等请求应用B失败,则应用A落失败的单据A-2,状态=FAIL;应用B则不对单据B-1做更新,状态=INIT。此时请求报文如下:
{
"requestId": String,
"amount":Moeny,
"refundDetialList":[
refundDetial-1,
refundDetial-2
]
}
第三次幂等请求:后台重试发起发起幂等请求应用B成功,则应用A落成功的单据A-3,状态=SUCCESS;应用B更新单据B-1状态=SUCCESS。(但是应用A与应用B之间的核对不一致。)
- 但是此时应用A调用应用B的请求报文里refundDetialList的size数量已经包含了A-1、A-2的单据(其实这时候报文是错误的)。
此时请求报文如下:
{
"requestId": String,
"amount":Moeny,
"refundDetialList":[
refundDetial-1,
refundDetial-2,
refundDetial-3,
refundDetial-4
]
}
应用A和B针对幂等请求的处理逻辑:
应用A:幂等字段为requestId,同时校验amount的一致性。捞起DB里根据requestId+status=FAIL关联到的refundDetial组装 对应用B调用的报文。
应用B:幂等字段为requestId,同时校验amount的一致性。捞起DB里根据requestId关联到refundDetial组装报文对下游发起调用。
背景应该描述清楚了,那么下面分析一下问题:
-
**应用A为什么组装报文错误?**原因:如果幂等重试在2次以上,组装请求报文由于DB里已经有两条FAIL出现重复组装refundDetialList的情况,导致传给下游错误的报文信息。
-
应用B面对错误的报文,为什么能处理成 **功?**原因:应用B会将首次请求失败的数据落DB,幂等请求时候,如果requestId不变,且amount不变,则会捞起首次请求落DB的refundDetial,组装对下游的请求报文,因此不会报错。
分析问题与原因后,乍一看感觉应用A和B都有问题,应用A组装refundDetialList的逻辑错误,忽略了2次幂等以上的场景,如果出现这种场景,则组装refundDetialList时候最起码要getFirst()才行。
应用B没有感知到应用A的错误报文,最起码应该拦截到,而不是向下调用才行。
当然,本文的目的不是争个谁对谁错,而是讨论针对这种资金类服务应该如何设计幂等?
04/ 幂等检验的范围有哪些?
资金类服务的特点就是很容易发生资损。因此在设计幂等逻辑的时候,需要分析请求报文中哪些字段需要严格"幂等"(就是重复请求中哪些字段需要严格保持一致)。
如上文说到的服务,是以requestId作为幂等字段,且对amount做了金额一致性校验,通过后才能继续向下调用。那么refundDetialList是否应该纳入幂等校验的范畴之内的,我觉得这个需要By业务分析,单纯从平台角度无法给出特定的结论。因为平台要提供的是通用能力,理论上是不吃业务的,但是如果针对业务诉求强,那就需要做业务定制的处理逻辑,这显然违背平台设计的初衷。但有时候确实是这样,平台建设没法完全脱离业务。
05/ 幂等测试场景分析
对于测试来说,通常情况下,我们分析幂等场景一般幂等字段(也可能是多个字段联合作为幂等条件)、关键资金字段如金额作为场景因子设计幂等测试用例:
CASE001:同号发起(幂等条件不变)+不换金额,预期幂等REPEAT_REQUEST;
CASE002:同号发起(幂等条件不变)+换金额,预期幂等校验不通过报错;
CASE003:换号发起(幂等条件变化)+不换金额,预期作为新请求处理;
此外,幂等发起的次数也应该引起关注,作为一个场景因子来对待:
幂等请求次数建议考虑2次以上的场景。
当然,我们设计幂等场景的时候,最好要review下开发的实现思路,不要完全采取黑盒方法,结合白盒方法设计的测试用例才更有效。
- END -
下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!
新年送福
关注「软件质量保障」微信公众号
菜单栏对话框回复:2024
点击「抽奖」小程序,即可参与新年抽奖活动
我们将从中抽取25位幸运读者,分别为大家送上新年礼物。
温馨提醒:
-
以上两个活动的截止日期均为:2024年1月22日23:59。
-
中奖的同学,请填写好邮寄地址,我们会核实后邮寄礼品。如果两周内没有收到礼品,请在后台留言或者私信我们。
好文推荐
往期推荐