日常工作中,很多场景需要我们保证系统操作的幂等性,先来了解下什么是幂等。
引自百度百科:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
引自维基百科:
Idempotence is the property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result beyond the initial application. The concept of idempotence arises in a number of places in abstract algebra (in particular, in the theory of projectors and closure operators) and functional programming (in which it is connected to the property of referential transparency).
幂等理解总结:
我们提取一下关键信息:一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。明确目标,在服务不出错的情况下,尽量去达成这个目标。
实战背景
外部系统对接库存操作时,所有带业务含义的操作只能生效一次,否则会因为一些流量重放等操作导致库存紊乱。由于业务场景的需要,此时就需要保证接口的幂等性。如果针对每个接口单独去做幂等,非常费力,而且需要考虑业务的边边角角。除此之外,每个业务开发同学在针对不同场景写的幂等方案可能也不尽相同,后期维护成本也较高。
在此背景之下,我们想设计一个公共的幂等组件,想达成以下几个目标,达成的目标越多越好。
目标
- 通用性,尽量满足所有幂等场景,大部分幂等场景都支持
- 易用性,使用简单,如果学习成本太高,对于开发者来说,还不如自己写一个
- 代码无侵入性,如果代码侵入太多,会导致代码不够优雅,美观,拖拖拉拉。且影响代码的可阅读性和后期维护性
- 独立性,依赖尽量少,量级要足够轻(否则因为需要引入幂等组件,项目中引入一堆无用的内容进来,对原本的业务流程造成较大的影响。例如,一个接口平均耗时100ms,引入这个组件后,变成了200ms,耗时直接double,这个是绝对不能容忍的)
- 易拓展性,方便后期迭代维护
设计思路
*设计流程图
疑难点
DB选型(mysql/mongo)
业务场景:
要保证一个请求在响应后,之后每次返回结果都一样,需要保存response数据,当同一个请求进行重复请求时,直接查DB返回结果即可,所以需要保存response结果。
相同点不做赘述,主要说下对我们来说,两者之间对我们业务影响最大的区别:
选择
业务特点:
- 需要存很多的response,数据量大。
- 不打算引入事务,如果引入了事务,通过事务去做一些rollback变得非常简单,但它是包在业务外层的。所以当事务较大时,虽然我们设计的幂等相关的内容回滚了,但业务流程的事务回滚级别都是业务制定的,无法与业务保持同步。
综上所述,最终选型mongo。
数据落DB的时机(同步提交/异步提交)
业务场景:
解决方案
这边选型是异步提交。
解决异步提交场景的问题换了个思路,看流程图便知。
详解:
请求刚进来就会查一次DB,判断是否有此次请求的记录,若有,且此时mongo中response结果为null,便认为是往库里落数据的线程还没执行完,这边的线程会retry进行等待。直到response填充进去(retry间隔时间500ms/次,次数可配)大多数业务场景无特殊情况,都不需要这么久耗时。如果等待重试后,仍旧无结果。这个时候简单判定服务可能出现了问题,或是出现了不太合理的业务场景。
两个相同请求同一时间戳并发过来,另一个线程如何正确返回
业务场景:
同一时间戳来了两个相同的请求,会因为唯一键约束报错。但更好的处理情况是,此时也能正确的返回结果(原因如引言中幂等所述:在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同)。
解决方案
这边采用spring-retry在查询发现插入报错后,在rpc框架允许的时间范围内进行重试。这种场景极少,retry会短暂的block一下线程。
被调用方,方法内部报错,产生异常,但是初始记录已经存在怎么办
业务场景:
A服务调用B服务,B服务执行到一半,产生异常,但是库里已经写入数据,response结果还没更新进去。上游发生幂等重试,会无限失败,结果也没给出。
解决方案
try-catch业务流程,一旦发生业务流程执行抛出异常,则删除mongo里面记录的初始数据。
如果DB选型选择mongo,程序跑完,落数据的时刻,是异步执行的,如果此时服务发布,进程突然没了,而response记录没写进去,后续请求会有异常
业务场景:
mongo落表是异步执行的,如果此时服务发布,进程突然没了,后续请求再进来会拿不到正确的response。此时发生幂等重复提交场景,去表里取结果就一直有问题。
解决方案
假如DB选择是mysql,通过事务rollback不需要考虑这种场景,此刻只以mongo去做讨论:
目前这个场景在设计上是不考虑的,曲线救国,通过优雅发布,去避免此场景。
理论上是不会出现进程执行到一半,进程突然被kill掉这种场景的。如果因为考虑此场景而引入事务回滚等机制,为了这一个很小很细微,目前不会出现的点,而引入很多其他技术手段来保证,会影响业务耗时。在进行取舍之后,选择了目前不一定是最好,但最适合我们的方案,不考虑进程突然被kill掉,如果实在是出现此问题,可以手动修下数据。
是否轻量级,最终引入的第三方依赖
mongoDB
最初的思想是为了保证各种各样边边角角的场景的幂等,当时想引入mysql,mongo,分布式锁,事务等一系列依赖,想做到尽善尽美。
放弃引入大批组件的原因如下:
最后瞄准目标,思量再三,结合目前绝大多数场景具体分析后,打算只引入mongo去实现,否则为了解决极少数特别细微末节的问题,引入一大批组件,损耗了很多没必要的性能,解决了几乎不可能发生的问题。反而起到了本末倒置的效果。
表字段
如何使用
学习成本:预计3分钟
主要3个步骤
- 引入pom
- 加个注释
- 加个幂等入参
三分钟教你如何保证接口幂
第一步:
第二步:
配置DB连接,涉及DB连接敏感数据,故不粘贴具体图片。
第三步:
直接在接口上添加。
第四步:
直接在接口上添加。
注意事项
requestId重复将会直接返回上一次相同requestId的处理结果, 请确保该接口是否适用幂等场景, 适用幂等的场景应该为 :requestId只能被唯一成功处理一次,相同requestId能被成功处理多次的场景,均不适用幂等场景!
未来发展
目前为了保证业务的快速发展,只是做了较为简单的一版,在自己项目内进行了使用。
后续想要做的更好,更通用,还有一些改进点:
- 比如DB支持各种选型,根据服务调用方适配,想选mysql就选mysql,想选其他DB就选其他DB(有啥选啥,做到业务方可配置)。
- 目前存储在DB里持久化的数据是需要归档的,时间一长,记录的数据量会特别大。需要业务方使用去限制时间,比如库存操作对一个月内的操作做到幂等,支付转账等场景同理。保存时间也做到可配置。
- 开源,支持各种场景,包括MQ等等。
END
文 | 平川
关注【得物技术】
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。