分布式锁与幂等性问题

分布式系统由独立的服务器通过网络松散耦合组成。在这个系统中每个服务器都是一台独立的主机,服务器之间通过内部网络连接。分布式系统有以下几个特点:

  • 可扩展性:可通过横向水平扩展提高系统的性能和吞吐量。
  • 高可靠性:高容错,即使系统中一台或几台故障,系统仍可提供服务。
  • 高并发性:各机器并行独立处理和计算。
  • 廉价高效:多台小型机而非单台高性能机。

互斥性(锁)

分布式锁条件

(1):存储空间
锁是一个抽象的概念,锁的实现,需要依存于一个可以存储锁的空间。在多线程中是内存,在多进程中是内存或者磁盘。更重要的是,这个空间是可以被访问到的。多线程中,不同的线程都可以访问到堆中的成员变量;在多进程中,不同的进程可以访问到共享内存中的数据或者存储在磁盘中的文件。但是在分布式环境中,不同的主机很难访问对方的内存或磁盘。这就需要一个都能访问到的外部空间来作为存储空间

  • 基于数据库做分布式锁(行锁、version乐观锁),如quartz集群架构中就有所使用
数据库表,字段为锁的ID(唯一标识),锁的状态(0表示没有被锁,1表示被锁)。

伪代码为:
      lock = mysql.get(id);
      while(lock.status == 1) 
       {
         sleep(100);
        }
        mysql.update(lock.status = 1);
        doSomething();
        mysql.update(lock.status = 0);


存在问题:
问题1:锁状态判断原子性无法保证 
  从读取锁的状态,到判断该状态是否为被锁,需要经历两步操作。如果不能保证这两步的原子性,就可能导致不止一个请求获取到了锁,这显然是不行的。因此,我们需要保证锁状态判断的原子性。
问题2:网络断开或主机宕机,锁状态无法清除 
问题3:无法保证释放的是自己上锁的那把锁 
假设持有锁的主机A在临界区遇到网络抖动导致网络断开,分布式锁及时的释放掉了这把锁。之后,另一个主机B占有了这把锁,但是此时主机A网络恢复,退出临界区时解锁。由于都是同一把锁,所以A就会将B的锁解开。此时如果有第三个主机尝试抢占这把锁,也将会成功获得。因此,我们需要在解锁时,确定自己解的这个锁正是自己锁上的。

进阶条件,在实际的系统环境中,还会对分布式锁有更高级的要求。

1:可重入:线程中的可重入,指的是外层函数获得锁之后,内层也可以获得锁,ReentrantLock和synchronized都是可重入锁;衍生到分布式环境中,一般仍然指的是线程的可重入,在绝大多数分布式环境中,都要求分布式锁是可重入的。
2:惊群效应(Herd Effect):在分布式锁中,惊群效应指的是,在有多个请求等待获取锁的时候,一旦占有锁的线程释放之后,如果所有等待的方都同时被唤醒,尝试抢占锁。但是这样的情况会造成比较大的开销,那么在实现分布式锁的时候,应该尽量避免惊群效应的产生。:
3:公平锁和非公平锁:不同的需求,可能需要不同的分布式锁。非公平锁普遍比公平锁开销小。但是业务需求如果必须要锁的竞争者按顺序获得锁,那么就需要实现公平锁。:
4:阻塞锁和自旋锁:针对不同的使用场景,阻塞锁和自旋锁的效率也会有所不同。阻塞锁会有上下文切换,如果并发量比较高且临界区的操作耗时比较短,那么造成的性能开销就比较大了。但是如果临界区操作耗时比较长,一直保持自旋,也会对CPU造成更大的负荷。
  • 缓存如Redis、Tair、Memcached、MongoDB
Redis的分布式缓存特性使其成为了分布式锁的一种基础实现。通过Redis中是否存在某个锁ID,则可以判断是否上锁。为了保证判断锁是否存在的原子性,保证只有一个线程获取同一把锁,Redis有SETNX(即SET if Not eXists)和GETSET(先写新值,返回旧值,原子性操作,可以用于分辨是不是首次操作)操作

为了防止主机宕机或网络断开之后的死锁,Redis没有ZK那种天然的实现方式,只能依赖设置超时时间来规避

除此以外还可以用SETNX+EXPIRE来实现。Redisson是一个官方推荐的Redis客户端并且实现了很多分布式的功能。它的分布式锁就提供了一种更完善的解决方案
  • 专门的分布式协调服务Zookeeper
ZooKeeper中有一种节点叫做顺序节点,假如我们在/lock/目录下创建3个节点,ZK集群会按照发起创建的顺序来创建节点,节点分别为/lock/0000000001/lock/0000000002/lock/0000000003。

ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。

根据ZK中节点是否存在,可以作为分布式锁的锁状态,以此来实现一个分布式锁:
1:客户端调用create()方法创建名为“/dlm-locks/lockname/lock-”的临时顺序节点。
2:客户端调用getChildren(“lockname”)方法来获取所有已经创建的子节点。
3:客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。
4:如果创建的节点不是所有节点中需要最小的,那么则监视比自己创建节点的序列号小的最大的节点,进入等待。直到下次监视的子节点变更的时候,再进行子节点的获取,判断是否获取锁。

释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可,不过也仍需要考虑删除节点失败等异常情况。

(2):唯一标识
不同的共享资源,必然需要用不同的锁进行保护,因此相应的锁必须有唯一的标识。在多线程环境中,锁可以是一个对象,那么对这个对象的引用便是这个唯一标识。多进程环境中,信号量在共享内存中也是由引用来作为唯一的标识。因此,在分布式环境中,只要给这个锁设定一个名称,并且保证这个名称是全局唯一的,那么就可以作为唯一标识。
(3):至少两种状态
为了给临界区加锁和解锁,需要存储两种不同的状态。如ReentrantLock中的status,0表示没有线程竞争,大于0表示有线程竞争;信号量大于0表示可以进入临界区,小于等于0则表示需要被阻塞。因此只要在分布式环境中,锁的状态有两种或以上:如有锁、没锁;存在、不存在等,均可以实现

幂等性问题

所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的。
那么我们为什么需要接口具有幂等性呢?设想一下以下情形:

1:在App中下订单的时候,点击确认之后,没反应,就又点击了几次。在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单问题。
2:在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大。

GTIS

它是一个轻量的重复操作关卡系统,它能够确保在分布式环境中操作的唯一性。我们可以用它来间接保证每个操作的幂等性。它具有如下特点:

  • 高效:低延时,单个方法平均响应时间在2ms内,几乎不会对业务造成影响;
  • 可靠:提供降级策略,以应对外部存储引擎故障所造成的影响;提供应用鉴权,提供集群配置自定义,降低不同业务之间的干扰;
  • 简单:接入简捷方便,学习成本低。只需简单的配置,在代码中进行两个方法的调用即可完成所有的接入工作;
  • 灵活:提供多种接口参数、使用策略,以满足不同的业务需求。
实现原理
  • 基本原理
    GTIS的实现思路是将每一个不同的业务操作赋予其唯一性。这个唯一性是通过对不同操作所对应的唯一的内容特性生成一个唯一的全局ID来实现的。基本原则为:相同的操作生成相同的全局ID;不同的操作生成不同的全局ID。
    生成的全局ID需要存储在外部存储引擎中,数据库、Redis亦或是Tair等均可实现。考虑到Tair天生分布式和持久化的优势,目前的GTIS存储在Tair中。其相应的key和value如下:
key:将对于不同的业务,采用APP_KEY+业务操作内容特性生成一个唯一标识trans_contents。然后对唯一标识进行加密生成全局ID作为Keyvaluecurrent_timestamp + trans_contents,current_timestamp用于标识当前的操作线程。

判断是否重复,主要利用Tair的SETNX方法,如果原来没有值则set且返回成功,如果已经有值则返回失败。

  • 内部流程
    GTIS的内部实现流程为:

1:业务方在业务操作之前,生成一个能够唯一标识该操作的transContents,传入GTIS;
2:GTIS根据传入的transContents,用MD5生成全局ID;
3:GTIS将全局ID作为key,current_timestamp+transContents作为value放入Tair进行setNx,将结果返回给业务方;
4:业务方根据返回结果确定能否开始进行业务操作;
5:若能,开始进行操作;若不能,则结束当前操作;
6:业务方将操作结果和请求结果传入GTIS,系统进行一次请求结果的检验;
7:若该次操作成功,GTIS根据key取出value值,跟传入的返回结果进行比对,如果两者相等,则将该全局ID的过期时间改为较长时间;
8:GTIS返回最终结果。

  • 实现难点
    GTIS的实现难点在于如何保证其判断重复的可靠性。由于分布式环境的复杂度和业务操作的不确定性,在上一章节分布式锁的实现中考虑的网络断开或主机宕机等问题,同样需要在GTIS中设法解决。这里列出几个典型的场景:
    1:如果操作执行失败,理想的情况应该是另一个相同的操作可以立即进行。因此,需要对业务方的操作结果进行判断,如果操作失败,那么就需要立即删除该全局ID;
    2:如果操作超时或主机宕机,当前的操作无法告知GTIS操作是否成功。那么我们必须引入超时机制,一旦长时间获取不到业务方的操作反馈,那么也需要该全局ID失效;
    3:结合上两个场景,既然全局ID会失效并且可能会被删除,那就需要保证删除的不是另一个相同操作的全局ID。这就需要将特殊的标识记录下来,并由此来判断。这里所用的标识为当前时间戳。
  • 使用说明
    使用时,业务方只需要在操作的前后调用GTIS的前置方法和后置方法,如下图所示。如果前置方法返回可进行操作,则说明此时无重复操作,可以进行。否则则直接结束操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用分布式实现和不使用分布式实现是两种不同的方式,它们各自有一些优缺点。 使用分布式实现的优点是: 1.保证了接口的。使用分布式可以避免并发请求重复操作,确保接口只会被调用一次,从而保证了接口的。 2.操作简单方便。通过使用Redis等分布式工具,可以很方便地实现分布式,代码实现也相对简单。 使用分布式实现的缺点是: 1.能开销较大。使用分布式需要对的实现方式、的粒度等进行优化,否则会对系统的能产生影响。 2.的超时时间设置问题。如果设置的超时时间过短,可能会导致请求无法执行,如果设置的过长,则可能会导致请求等待的时间过长,影响系统的能。 不使用分布式实现的优点是: 1.能开销较小。不使用分布式实现,可以避免分布式带来的额外开销,提高系统的能。 2.实现灵活方便。不使用分布式实现,可以通过代码实现,实现灵活方便。 不使用分布式实现的缺点是: 1.实现难度较大。在不使用分布式的情况下,需要通过代码实现,实现难度较大。 2.容易出现逻辑错误。在不使用分布式的情况下,容易出现逻辑错误,导致接口无法实现。 综上所述,使用分布式实现和不使用分布式实现各有优缺点,需要根据具体的业务场景和能需求进行选择。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值