跟着梁老师(梁桂钊,微微科技架构师)学习了一下分布式场景下的幂等解决方案,个人理解幂等是指多次执行和一次执行的结果一样,如果不涉及数据修改的接口,则不需要保证幂等。简要整理记录一下吧。
在许多业务场景中,我们需要允许甚至要保证客户端重复提交,或者服务端的多次重试,这时需要确保资源只会产生一份最终结果,否则就可能出现多次执行扣款、交割等生产故障
因此需要幂等机制,来确保资源的唯一性
有这么几种方式保证幂等机制
假设有这样两个服务:退款服务,和支付服务
退款服务需要调用支付服务进行退款,数据库方面需要针对我们的约束资源字段,创建唯一的索引
比如,这个例子中约束资源字段是”退款编码“
当支付服务收到出账调用时,先判断这笔退款是否已存在出账流水记录中。存在的话直接拒绝当前请求,不存在的话,则实现后续的业务操作
数据库的唯一索引可以防止插入重复的数据,但当我们遇到分库分表的情况时,唯一索引的方案就不太好用了
这个时候,我需要先查询一次数据库,然后判断我们的约束资源字段是否重复,当不存在重复时我们再进行插入操作
然而这种先”select“后”insert“的方式,在高并发的情况下,可能具有并发安全问题
比如当服务 A 和服务 B 同时向服务 C 发起调用请求时,可能会同时进入业务代码块的 if 逻辑
if(约束的资源字段不存在){
执行业务操作
}
这时就出现了并发安全的问题,进而导致了重复的数据写入
为了避免并发安全问题,引入分布式锁来解决
目前业界对于分布式锁的实现有很多种方案,比较常见的是 Redis 和 Zookeeper,我们后面以 Redis 为例进行讨论~
分布式锁的核心原理,就是通过获取锁令牌,来🈲止别人同时对约束资源进行写操作,而这个获取锁令牌的任务就交给 Redis 进行集中管理
当支付服务(图中有三个)接收到退款服务的出账调用时,会向集中式缓存 Redis 申请锁令牌
支付服务们会使用到 Redis 的 setnx 命令,setnx 命令会在 ” 当且仅当 Key 不存在时 “,将 Key 对应的值设为 Value;如果给定的 Key 已经存在,那么 setnx 不需要做任何动作。这样就实现了多个进程并发获取锁令牌时,只有一个进程能设置成功,其它进程就只好放弃或稍后再试
需要注意的是,为了防止出现死锁,需要设置一个合理的过期时间,进行锁的自动销毁
分布式锁是不是解决并发幂等的方式呢
由于分布式锁都存在一个存活时间,也就是过期时间
那么当一个退款服务向支付服务集群发起出账调用,假设发起了 5 个重复的请求,并且具有相同的退款编号,第一个请求成功了并获取到一个分布式锁,过期时间为 10 分钟,这时网络不稳定,导致大量接口调用失败
一般我们会进行失败重试,采用消息队列机制,这些任务就会在消息中间件中不断排队与重试,如果这个过程比较耗时,重试了 30 分钟后发现才成功,而当超过 10 分钟时,这个分布式锁其实就已经失效了,那队列中的其它任务就会正常调用支付服务的出账接口,导致重复支付
所以仅使用分布式锁不能保证这种异常场景下的并发幂等,需要确保分布式锁过期时间要大于业务的最大重试时间,以此保证业务层面的合法性
通过分布式锁获取锁令牌,实现当前只有一个请求执行业务操作,然后通过数据持久化进行数据落盘,针对我们需要的约束资源字段做二次校验
也可以引入状态机,判断当前状态是否达到预期,如果没有达到则拒绝操作,如果达到预期则执行相应逻辑
通过状态机进行状态约束和状态流转,状态机确保同一个业务的流程化执行,从而实现数据幂等