代码改变世界

系统幂等设计

2019-06-06 11:18  春哥大魔王  阅读(992)  评论(6编辑  收藏  举报

前言

幂等简单的定义:

系统中的多次操作,不管多少次,都应该产生一样的效果,或返回一样的效果。

比如实际的业务请求为创建一个活动,理论上需要根据业务形态开发幂等创建活动的接口,这样在相同参数调用接口多次创建活动时,只可以创建成功一次。

由于查询天生的是幂等请求,所以针对于查询场景可以不做业务角度的幂等约束,查询幂等的约束多是针对于资源控制,安全防刷,流控来做的。

一个场景

试想有这样一个场景:
A系统传递userId和活动Id调用B系统发券,如果B系统发券成功,需要返回A系统本次发券userId和发券code。
由于B系统需要对自己发出去的券进行限制防止超发,所以会根据userId和code建立幂等拦截。
但是A系统的调用次数是不受信的,B系统会对多次重复的请求做拦截,这样造成一部分A的请求为无效请求,被直接打回。
但是A系统接受B系统的返回值中是需要code的,如果没有收到code,A系统会认为调用B系统失败,进行重试,结果就造成了A系统不停被重试,B系统拦截无效请求,返回默认值,A再重试的死循环。

解决这个场景问题有两种方法:

  • 在B系统识别到A重复请求时,需要查询流水表,返回已经发送的code,组装参数返回A系统,A系统识别到code,做本地记录,不再调用B系统发送。
  • A系统调用B系统发券这个逻辑拆分成两个接口,发券接口调用和查询发券记录。

第一种方案明显的缺点在于,针对于重复发送的请求都会转化成一次查询操作,这样无形中加大了对于B系统资源的浪费,同时由于发券接口逻辑中引入了查询逻辑,造成此接口违反了“单一职能原则”,在未来围绕这个接口的新业务逻辑造成的代码修改时,比如允许对同一个用户发送多张券,可能出现潜在的bug问题。

第二种方案则是我选择的更好的方案,也是更支持的方案,一个接口最好只做一件事,这样一个接口只做发券,同时对于多次重复发券做请求拦截,没有必要放无效请求到系统核心逻辑中,更没有必要因此引入查询逻辑消耗系统资源。
在调用B系统发券接口因为拦截重复请求,返回重复请求状态码后,系统A调用B系统的查询接口,进行已发送code的查询,这样在使用角度和后期业务迭代角度及系统资源使用和未来优化角度来说,都存在一定的空间,而不会造成代码复杂度提升引入隐患bug。

总结

针对于幂等操作还有如下几种方案:

  • 删除/修改操作,一定要带入版本号和原始修改参数,万不可直接在下游逻辑中直接i++,i--
  • 为进一步拦截真实数据罗库,需要在数据库表中创建唯一约束,防止因为分布式系统锁问题或数据不一致问题导致拦截不到,这样在DB层建立最后一次兜底策略
  • token机制,可以做类似于页面重复提交的功能,token可以放到redis中,并自带实效
  • 悲观锁/乐观锁,一般分为分布式锁,单机锁,update where
  • 有限状态机,订单系统一般都会设计一个订单状态流转的状态机,表述在不同状态下的状态变更,只有在上一个状态满足时,才会进行接下来的状态变更,这样保证了状态变更的幂等性
  • 接口调用最好引入来源source,序列号seq等信息,可以用source+seq做唯一索引,也可以将这两个值上报做好监控
  • 监控和开关,为可以更直观的观察系统幂等情况,可以建立对应的监控大盘,及告警配置,这样可以更直观的发现问题,同时配置相应的开关,在发现问题时比如被刷时,通过调控开关及时止损。