幂等设计
什么是幂等
- 幂等性是系统服务对外一种承诺,承诺只要调用接口成功,外部多次调用对系统的影响是一致的。
- 声明为幂等的服务会认为外部调用失败是常态,并且失败之后必然会有重试。
什么情况下需要幂等
- 业务出现异常(例如:网络异常或者服务崩溃重启)时,前端有可能重复发起超时的或者处理失败的请求。例如:用户下达委托时,由于网络问题导致超时,此时客户端很可能会重试下达,且重试下达后,服务端不会被多次执行,避免出现做超的情况。
- 用户下达委托时,多次点击下达按钮,可能生成多笔委托,实际期望应该只生成一笔委托。
- 后端处理MQ或者消息中心发过来的消息,处理失败后,要支持重试处理。避免一次处理失败,就需要人工介入的问题。
同时还需要避免被重复处理,导致业务异常。
幂等的优点
- 客户端开发逻辑变得简单,处理失败后可以放心的重试,不用担心服务端重复处理的问题。
幂等的缺点
-
幂等增加了服务提供者的逻辑和成本,是否有必要,需要根据具体场景具体分析
- 增加了额外控制幂等的业务逻辑,复杂化了业务功能
- 把并行执行的功能改为串行执行,降低了执行效率
-
因此除了业务上的特殊要求外,尽量不提供幂等的接口。
业务是否需要的判断标准
- 必须实现
- 幂等不实现会对客户产生严重影响的
- 例如:指令下达、委托下达,多次处理后产生多笔指令或者委托,买入或者卖出的量与客户预期的可能会差很多,对客户来说后果是很严重的。
- 幂等实现比较简单的。
- 例如:判断一下原状态
- 幂等不实现会对客户产生严重影响的
- 无需实现
- 业务处理后,会产生多笔业务或者记录,但是对客户来说影响不大的,无需实现。
- 例如:操作日志因为不幂等多了几笔。撤单委托多了几笔,但对客户实际业务无影响。
- 业务处理后,会产生多笔业务或者记录,但是对客户来说影响不大的,无需实现。
幂等的空间与时间
- 空间维度:即幂等对象的范围,是个人还是机构,是某一次交易还是某种类型的交易
- 时间维度:即幂等的保证时间,是几秒、几分钟还是永久性的…
对于交易系统来说,幂等的空间是客户端的单笔业务请求。幂等的时间要求暂定:1秒-12小时。(小于1秒的,则要求同步串行处理。)
幂等要求的范围
对全系统接口进行分析,看是否有必要实现幂等。主要评价两个点:
- 实现幂等,增加的业务复杂度。
- 不做幂等带来的后果严重程度。
防重 VS 幂等
- 防重:防重是指在第一次请求已经成功的情况下,人为的进行多次操作,导致不满足幂等要求的服务多次改变状态。 幂等:幂等是指在第一次请求不知道结果(比如超时)或者失败的异常情况下,发起多次请求,目的是多次确认第一次请求成功,却不会因多次请求而出现多次的状态变化。
保证幂等策略
幂等需要通过唯一的业务单号来保证。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。 下面以支付为例,在不考虑并发的情况下,实现幂等很简单:①先查询一下订单是否已经支付过,②如果已经支付过,则返回支付成功;如果没有支付,进行支付流程,修改订单状态为‘已支付’。
防重复提交策略
上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变更状态操作加锁,将并行操作改为串行操作。
幂等实现思路
幂等实现的思路有以下几种:
-
业务去重
这种方法适用于在业务中有唯一标的插入场景中。比如:成交回报。成交存在唯一的成交编号,将成交编号作为唯一索引,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。方案优点:依懒于业务设计,无额外的性能付出。(推荐指数:※※※※※)
-
单独去重
这种方法适用于原业务无唯一标志,客户端或服务端生成统一的唯一编号,作为业务的唯一标志。每次请求时,带上唯一编号,业务处理的事务中,同时将唯一编号插入专门的去重表。重复处理时,数据库会抛出唯一约束异常,操作就会回滚。 方案优点:不依懒业务有唯一主键。(推荐指数:※※※※)方案缺点:
- 需要增加额外的去重表。
- 多插入一张去重表,性能会有所损耗。
-
多版本控制
这种方法适合在更新的场景中,我们就可以在更新的接口中增加一个版本号,来做幂等方案优点:实现简单,除增加一个版本字段外,无额外增加。(推荐指数:※※※※)
方案缺点:仅适用于更新操作。
-
状态机控制
方案优点:性能可以保证。(推荐指数:※※※)
方案缺点:状态必须有严格的顺序。
-
token令牌
这种方式分成两个阶段:申请token阶段和支付阶段。- 在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。
- 订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。 实际上这里的token是一个信物,支付系统根据token确认,你是你妈的孩子。
方案缺点:需要系统间交互两次,流程较上述方法复杂。(推荐指数:※※※)
-
分布式锁
这里使用的防重表可以使用分布式锁代替,比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。方案缺点: (推荐指数:※)
- 需要引入分布式锁,比较麻烦。
- 分布式锁的性能相对来说较低。
- 如果业务处理失败,分布式锁无人解除,业务无法继续执行。