1. 什么是幂等性?
幂等性是指在分布式系统中,一个操作多次执行的结果与其执行一次的结果相同。设计具有幂等性的分布式系统可以有效避免数据不一致和重复处理的问题。
幂等系统的应用场景
在微服务架构下,由于分布式天然特性的时序问题, 以及网络的不可靠性(机器、机架、机房故障, 电缆被挖断等等), 重复请求很常见, 接口幂等性设计就显得尤为重要。 比如浏览器/客户端多次提交、微服务间超时重试、消息重复消费等。 以订单流程为例的幂等性场景:
1.一个订单创建接口,第一次调用超时了,然后调用方重试了一次
2.在订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次
3.当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次
4.一个订单状态更新接口,调用方连续发送了两个消息,一个是已创建,一个是已付款。但是你先接收到已付款,然后又接收到了已创建
5.在支付完成订单之后,需要发送一条短信,当一台机器接收到短信发送的消息之后,处理较慢。消息中间件又把消息投递给另外一台机器处理
为了解决以上问题,就需要保证接口的幂等性,接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。除了查询功能具有天然的幂等性之外,增加、更新、删除都要保证幂等性。
2. 分布式幂等性设计方法
2.1 利用数据库实现幂等性
数据库的唯一约束和事务特性可以用来实现幂等性。例如,在处理支付请求时,我们可以在支付记录表中插入一条带有唯一支付 ID 的记录。如果数据库已存在相同支付 ID 的记录,则认为该支付请求已处理过,从而实现幂等性。
1、去重表(唯一索引)
往数据库去重表里插入数据的时候,利用数据库的唯一索引特性,保证唯一的逻辑。唯一序列号可以是一个字段,例如订单的订单号,也可以是多字段的唯一性组合。
使用数据库防重表的方式它有个严重的缺点,那就是系统容错性不高,如果幂等表所在的数据库连接异常或所在的服务器异常,则会导致整个系统幂等性校验出问题。
2、多版本号控制之乐观锁
多版本并发控制,该策略主要使用update with condition(更新带条件来防止)来保证多次外部请求调用对系统的影响是一致的。在系统设计的过程中,合理的使用乐观锁,通过version或者updateTime(timestamp)等其他条件,来做乐观锁的判断条件,这样保证更新操作即使在并发的情况下,也不会有太大的问题。借鉴数据库的乐观锁机制。
示例:
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
3、悲观锁
使用悲观锁实现幂等性,一般是配合事务一起来实现。
使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。
select for update,整个执行过程中锁定该订单对应的记录。注意:这种在DB读大于写的情况下尽量少用。
举个更新订单的业务场景:
假设先查出订单,如果查到的是处理中状态,就处理完业务,再然后更新订单状态为完成。如果查到订单,并且是不是处理中的状态,则直接返回
4、全局唯一ID
如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、Redis等。如果存在则表示该方法已经执行。使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。
结合redis的incr自增实现全局唯一ID,是一个常用的方案。
示例代码:
2.2 使用分布式事务实现幂等性
在涉及多个服务和数据源的场景下,可以使用分布式事务来实现幂等性。例如,使用两阶段提交(2PC)或者三阶段提交(3PC)协议来保证多个服务间的数据一致性。
示例代码:
在这个示例中,我们使用 @GlobalTransactional
注解来标记需要分布式事务支持的方法。在处理预订请求时,系统首先检查预订记录是否已存在,然后依次调用酒店服务和支付服务。如果其中任何一个服务出现异常,分布式事务将回滚,确保数据的一致性和幂等性。
2.3、token机制
token机制的幂等保障的主要流程就是:
1.服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存2.到redis中。(微服务肯定是分布式了,如果单机就适用jvm缓存)。
3.然后调用业务接口请求时,把token携带过去,一般放在请求头部。
4.服务器判断token是否存在redis中,存在表示第一次请求,这时把redis中的token删除,继续执行业务。
5.如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。
缺点:
业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。(当然redis性能很好,耗时不会太明显)
2.4,分布式锁
分布式锁可以确保同一时间只有一个线程处理特定的操作。我们可以在处理关键业务逻辑之前获取分布式锁,从而保证幂等性。
示例代码:
分布式锁实现幂等性的逻辑是,在每次执行方法之前判断,是否可以获取到分布式锁,如果可以,则表示为第一次执行方法,否则直接舍弃请求即可。需要注意的是分布式锁的key必须为业务的唯一标识,通常适用redis或者zookeeper来实现分布式锁
如果是分布是系统,构建唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统,在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路;
目前主要有几种方式实现分布式锁:
1、redis setNx命令
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
优点:
(1)Redis有很高的性能;
(2)Redis命令对此支持较好,实现起来比较方便
2,基于ZooKeeper的实现方式
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。
3, 状态机
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机,就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
很多业务表,都是有状态的,比如转账流水表,就会有0-待处理,1-处理中、2-成功、3-失败状态。转账流水更新的时候,都会涉及流水状态更新,即涉及状态机 (即状态变更图)。
状态机是怎么实现幂等的呢?
第1次请求来时,如流水号是 666,该流水的状态是处理中,值是 1,要更新为2-成功的状态,所以该update语句可以正常更新数据,sql执行结果的影响行数是1,流水状态最后变成了2。
第2请求也过来了,如果它的流水号还是 666,因为该流水状态已经2-成功的状态了,所以不会再处理业务逻辑,接口直接返回。
示例: 对于不少业务是有一个业务流转状态的,每一个状态都有前置状态和后置状态,以及最后的结束状态。例如流程的待审批,审批中,驳回,从新发起,审批经过,审批拒绝。订单的待提交,待支付,已支付,取消。
3.幂等性设计的注意事项
在实现分布式幂等性时,需要考虑以下几点:
-
幂等性操作的粒度:根据业务场景和性能要求,可以选择适当的幂等性设计粒度,如方法级、服务级或全局级。
-
幂等性与性能的权衡:实现幂等性可能会增加系统的复杂性和性能开销。在设计时,需要考虑这些因素并选择合适的实现策略。
-
幂等性与可用性的关系:某些幂等性实现方法可能会影响系统的可用性,如分布式锁。在设计时,需要充分了解各种方法的优缺点,选择合适的方案。