服务请求幂等,简单地解释可以为同一次请求,因为各种原因重试时得到的结果一致或者可被识别,这里的结果一致指的是对于平台数据的变更影响,比如重复提交同一订单,会不会生成重复订单。从上一篇(高可用系列一:高可用问题是如何产生的)分析可以看出,请求幂等在高可用场景是非常重要的一环。
先从请求分析,一般请求可分为查询类、更新类和新增类:
- 查询类:因为不存在数据变更,不存在幂等问题;
- 更新类:更新虽然存在数据变更,但一般都通过特定条件,此时不存在单接口幂等问题,但可能存在ABA问题,不在本文讨论范围内;
- 新增类:新增类存在幂等问题。
解决方案一:使用唯一索引
新增类场景,大多数情况下,数据都需要落库,此时最简单的方案则是,使用请求的必填字段,根据业务场景需要,设置数据库的唯一索引。当触发唯一索引异常时,表示为重复提交,此时可以选择返回正常成功或者数据已保存的标识。
优缺点:简单,且无需引入任何额外解决方案或第三方服务,但适用场景有限制
解决方案二:二次确认是否重复
有些情况下,使用请求的必填字段组合,从长期上无法生成为唯一索引,但是短期存在唯一性,此时可以采取查、锁、查的方式来解决:
- 查询是否已存在满足条件记录,已存在则说明为重复请求,否则继续
- 将关键字段作为锁key,申请分布式锁,加锁失败,则返回延后重试(此时可以考虑适当等待锁,减少重试次数),申请成功则继续
- 二次查询是否已存在满足条件记录,已存在则说明为重复请求,否则进行业务处理
ps:在处理有第三方依赖,且无法确认第三方是否已经做了幂等支持时,只要将查替换为查询第三方确认实际状态,也可以避免依赖第三方时因第三方系统造成的幂等问题。
优缺点:应用端引入分布式锁,所幸可以通过锁key减小锁竞争的范围,方案相对简单,但是多了两步查询消耗。
解决方案三:通过请求id
前端在发起请求时,可以提前生成一个请求id,在碰到重试场景是,使用相同的请求id来进行。这里需要注意一个坑,即需要保证生成请求id的服务本身的幂等请求id生成问题。说得比较绕,换个说法就是相同请求生成的请求id也需要相同。
使用请求id时,也有几种方案:
- 请求id 本身或者和其他参数共同组成唯一索引,这里其实是结合方案二来做,有时候会碰到请求的参数本身不存在唯一性,但可以与请求id组合形成唯一索引,组合的好处是对于请求id本身的唯一性要求降低了,毕竟生成的请求id根据不同的算法,两次生成也存在相同的可能性。
- 方案二基础上,使用请求id 本身或者和其他参数组合,作为分布式锁的key。
优缺点:大大降低了同请求判断对于业务参数的要求,对于请求id 的生成有一定要求(通常是故障引发点),另增加了请求id 本身的存储成本
解决方案四:提前生成主键
一般我们新增数据时,都存在一个单独的唯一主键,此时可以通过提前生成该唯一主键的方式,提交时将主键传入,利用主键存储时的主键重复异常来判断是否重复提交。
为了系统主键安全,一般采用如下步骤来使用该解决方案:
- 前端进入页面时,从系统请求一个令牌
- 后端在生成令牌时,将令牌放入分布式缓存
- 前端提交数据时,需同时提交令牌,和请求id类似(重试时使用与第一次相同的令牌,通常一个页面只请求一次令牌)
- 后端从分布式缓存中查询令牌对应主键,获取到主键则直接使用,未获取到则尝试生成主键
- 生成主键后,存入缓存时使用不存在则存入模式,存入成功则使用当前主键,否则说明主键已被其他线程/实例生成,直接重新从缓存中获取主键
优缺点:处理过程相对来说会更复杂一点,可以通过封装通用服务解决,业务侵入性更低,使用场景一般局限于前后端交互。
另外,还有一种串联的请求场景,即后置业务依赖前置业务信息的情况,比如下A订单送一次B权益,此时B权益的生成幂等,可以使用A订单的唯一索引通过特定规则转换后作为B权益的主键,或者使用该信息前面的方案一、方案二。
总结
不管哪个解决方案,本质在于相同的入参数下,如何区分两次请求为一次相同请求,并且该确认过程本身不存在高可用问题。以上每种方案都有其适用场景,没有绝对的优劣。
ps:有其他的方案欢迎留言讨论。
本作品的版权所有权归作者所有,受法律保护。未经作者书面许可,任何个人或组织均不得以任何形式使用、复制、修改、传播、展示或在未获得授权的情况下进行商业利用。