分布式系统中的幂等性

一、什么是幂等?

在分布式系统中有时会出现网络延时等原因导致的重复调用统一服务的情况,而我们往往期望此数据只产生一个结果,无论后面执行多少次所产生的影响都和第一次执行的影响相同。通俗的说,幂等就是针对一个操作,不管做多少次,产生效果或返回的结果都是一样的。

二、幂等的使用场景

1、查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;

2、删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;

3、更新操作:只是更新值的话,更新操作也是幂等的,累加操作需要注意设计接口,返回值应该是true或false,具体值应该另外提供接口

4、插入操作:需要控制实现其幂等性,因为多次插入会连续插入多条

三、如何实现幂等性

1.使用唯一索引
比如在创建订单的时候,每个单号只能对应一个订单,那么给订单号字段加上一个唯一索引,在新增订单的时候,只能有一个请求成功,剩下的都会抛出唯一索引重复异常。
2.token机制,防止页面重复提交
为了防止因为重复点击或者网络重发。或者nginx重发等情况导致页面重复提交的情况,可以使用token机制使页面的数据只能被点击提交一次
采用token+redis
处理流程:
数据提交前要向服务申请token,token放到Redis或JVM内存,token有效时间
提交后后台校验token,同时删除token,生成新的token返回
token特点:要申请,一次有效性,可以限流
注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,
存在并发问题,不建议使用

3.悲观锁
在一些新增的场景中,我们首先要查一下数据库中这个数据是否已经存在,比如,只有当goods status为1时才能对该商品下单,上面第一步操作中,查询出来的商品status为1。但是当我们执行第三步Update操作的时候,有可能出现其他人先一步对商品下单把goods status修改为2了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单2次,使得数据不一致。所以说这种方式是不安全的。
在库存占用的场景中,我们先要根据skuid和仓库查一下库存是否存在,然后再去调用update操作更新库存数量,但是当我们执行update操作的时候,有可能出现他人先一步对库存更新,但是我们不知道数据已经被修改了,这样就可能导致库存数量不一致,所以,为了保证这种类型的操作中数据的准确性,可以使用悲观锁。
实现:
在上面的场景中,库存信息从查询出来到修改,中间有一个处理库存数量的过程,使用悲观锁的原理就是,当我们在查询出库存信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为此条sku被锁定了,就不会出现有第三者来对其进行修改了。
获取数据的时候加锁获取
select * from table_xxx where sku_id=’xxx’ for update;
需要注意的是,使用此语句对数据锁定的时候,筛选的字段也应该是主键或者唯一索引,一定要确定一条数据,mysql才会使用行级锁,如果无法确定一条数据,那mysql会把整个表都锁住这点一定要避免。
4.乐观锁
以上场景的解决方案还有乐观锁的方案
乐观锁就是在数据字段中设置一个状态码或者版本号,在查询的同时更新,只有版本号或者状态码是期望的值才做更新操作
可以把当前库存量当做这个状态码,当update的时候要与原库存量对比,只有老库存与当前库存量一致时,才可更新成功
). 通过版本号实现
update table_xxx set stockNum=#newStockNum# where stockNum=#oldNum#
如下图:
分布式系统中的幂等性
5.分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。分布式锁是一个在很多环境中非常有用的原语,它是不同的系统或是同一个系统的不同主机之间互斥操作共享资源的有效方法。如在电商系统中,需要保证整个分布式系统内,对一个重要事物(订单,账户等)的有效操作线程 ,同一时间内有且只有一个。比如交易中心有N台服务器,订单中心有M台服务器,如何保证一个订单的同一笔支付处理,一个账户的同一笔充值操作是原子性的。
(1)基于zookeeper实现分布式锁的流程

  • 在zookeeper指定节点(locks)下创建临时顺序节点node_n
  • 获取locks下所有子节点children
  • 对子节点按节点自增序号从小到大排序
  • 判断本节点是不是第一个子节点,若是,则获取锁;若不是,则监听比该节点小的那个节点的删除事件
  • 若监听事件生效,则回到第二步重新进行判断,直到获取到锁

(2)基于redis实现分布式锁的流程
使用redis的setnx()用于分布式锁。(原子性)
SETNX是将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
• 返回1,说明该进程获得锁,SETNX将键 lock.id 的值设置为锁的超时时间,当前时间 +加上锁的有效时间。
• 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。
复制代码

存在死锁的问题

SETNX实现分布式锁,可能会存在死锁的情况。与单机模式下的锁相比,分布式环境下不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。某个线程获取了锁之后,断开了与Redis 的连接,锁没有及时释放,竞争该锁的其他线程都会hung,产生死锁的情况。所以在这种情况下需要对获取的锁进行超时时间设置,即setExpire,超时自动释放锁