解读分布式锁及其实现方式

1,976 阅读15分钟

写在前面的话

备考就像黑屋子里洗衣服,你不知道洗干净没有,只能一遍一遍去洗。

等到上了考场的那一刻,灯光亮了。

你发现只要你认真洗过,那件衣服光亮如新,而你以后每次穿上那件衣服都会想起那段岁月。

为什么需要分布式锁?

1.为了防止分布式系统中的多个进程之间相互干扰,

需要一种分布式协调技术来对这些进程进行调度。

而这个分布式协调技术的核心就是来实现这个分布式锁。

2.为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行。

分布式锁是什么?

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

分布式锁出现的背景及需要解决的问题?

为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行。

在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制(加锁)。

在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,

由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。

为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁出现的背景以及需要解决的问题!

分布式锁应该具备的条件?

1.在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行,

2.高可用的获取锁与释放锁,

3.高性能的获取锁与释放锁,

4.具备可重入特性,并发使用,不会引起数据错误,并发操作时与单线程操作获得的结果是一样的,

5.具备锁失效机制,能避免死锁,

6.具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

分布式锁在什么场景使用

在分布式系统中,常常需要协调他们的动作。

如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,

往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

分布式锁的实现方式

一、数据库方式实现分布式锁

1. 悲观锁

利用select … where … for update 排他锁

注意: 其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。

2. 乐观锁

所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。

我们的抢购、秒杀就是用了这种实现以防止超卖。

增加递增的版本号字段实现乐观锁。

当更新失败,表明锁被占用了。

3.给数据库某个字段增加唯一索引

每次插入相同的值,当值存在了,插入失败,表明已经被持有锁了。 表结构: 表数据: 插入语句及结果:

对user做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功。

缺点:

1.这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

2.这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

3.这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

4.锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决方案:

1.数据库是单点?主从同步,一旦挂掉快速切换到备库上。

2.没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

3.非阻塞的?搞一个while循环,直到insert成功再返回成功。

4.非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,

如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

二、利用redis实现分布式锁

1. 涉及命令

1.SETNX

SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

2.expire

expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

3.delete

delete key:删除key

在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

2. 实现思想:

1.获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

2.获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

流程图

三、使用Zookeeper中的临时顺序节点实现分布式锁

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,

它内部是一个分层的文件系统目录树结构,

规定同一个目录下只能有一个唯一文件名。

基于ZooKeeper实现分布式锁的步骤如下:

1.创建一个目录mylock,即创建一个持久节点

2.线程A想获取锁就在mylock目录下创建临时顺序节点LOCK1;

3.获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁,这就是加锁过程

4.线程B,创建临时顺序节点LOCK2,并获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点

注:向排序仅比它靠前的节点注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。;

5.这时候,如果又有一个客户端Client3或者线程C前来获取锁,则在mylock下载再创建一个临时顺序节点Lock3,相同的方式检查自己是否

为最小节点,发现不是,则向且仅向LOCK2节点注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态

6.线程A处理完,删除自己的节点,这就是解锁

线程B的监Watcher听到变更事件,再判断自己是不是最小的节点,如果是则获得锁,同样的线程C也是这样的方式获取锁。

流程图

客户端故障检测

正常情况下,客户端会在会话的有效期内,向服务器端发送PING 请求,来进行心跳检查,说明自己还是存活的。

服务器端接收到客户端的请求后,会进行对应的客户端的会话激活,会话激活就会延长该会话的存活期。

如果有会话一直没有激活,那么说明该客户端出问题了,服务器端的会话超时检测任务就会检查出那些一直没有被激活的与客户端的会话,

然后进行清理,清理中有一步就是删除临时会话节点(包括临时会话顺序节点)(参见《从paxos到zookeeper分布式一致性原理与实践》“会话”一节)。

这就保证了zookeeper分布锁的容错性,不会因为客户端的意外退出,导致锁一直不释放,其他客户端获取不到锁。

数据一致性

zookeeper服务器集群一般由一个leader节点和其他的follower节点组成,数据的读写都是在leader节点上进行。

当一个写请求过来时,leader节点会发起一个proposal,待大多数follower节点都返回ack之后,再发起commit,

待大多数follower节点都对这个proposal进行commit了,leader才会对客户端返回请求成功;如果之后leader挂掉了,

那么由于zookeeper集群的leader选举算法采用zab协议保证数据最新的follower节点当选为新的leader,

所以,新的leader节点上都会有原来leader节点上提交的所有数据。这样就保证了客户端请求数据的一致性了。

简单实现方式:

Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点

具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点

因为需要频繁的创建和删除节点,性能上不如Redis方式。

三种方式的比较

数据库分布式锁实现

缺点:

1.db操作性能较差,并且有锁表的风险

2.非阻塞操作失败后,需要轮询,占用cpu资源;

3.长时间不commit或者长时间轮询,可能会占用较多连接资源

Redis(缓存)分布式锁实现

缺点:

1.锁删除失败 过期时间不好控制

2.非阻塞,操作失败后,需要轮询,占用cpu资源;

##ZK分布式锁实现

缺点:性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。

总之:ZooKeeper有较好的性能和可靠性。

从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

CAP原则

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

解释

一致性(C)

在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A)

在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

分区容忍性(P)

以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

CAP原则的精髓

就是要么AP,要么CP,要么AC,但是不存在CAP。

如果在某个分布式系统中数据无副本, 那么系统必然满足强一致性条件, 因为只有独一数据,不会出现数据不一致的情况,此时C和P两要素具备,

但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了CP系统,但是CAP不可同时满足 。

因此在进行分布式架构设计时,必须做出取舍。

当前一般是通过分布式缓存中各节点的最终一致性来提高系统的性能,

通过使用多节点之间的数据异步复制技术来实现集群化的数据一致性。

通常使用类似 memcached 之类的 NOSQL 作为实现手段。

虽然 memcached 也可以是分布式集群环境的,但是对于一份数据来说,它总是存储在某一台 memcached 服务器上。

如果发生网络故障或是服务器死机,则存储在这台服务器上的所有数据都将不可访问。

由于数据是存储在内存中的,重启服务器,将导致数据全部丢失。

base理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写。

BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,

其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,

采用适当的方式来使系统达到最终一致性(Eventual consistency)。

BASE中的三要素进行详细讲解

基本可用:指分布式系统在出现不可预知故障的时候,允许损失部分可用性。

注意,这绝不等价于系统不可用,以下两个就是“基本可用”的典型例子:

响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5秒内返回给用户相应的查询结果,

但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。

功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,

由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面(消费降级)。

**弱状态:也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,**即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

**最终一致性:强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。**因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

三种情况

  1. CA: 优先保证一致性和可用性,放弃分区容错。 这也意味着放弃系统的扩展性,系统不再是分布式的,有违设计的初衷。

2.CP: 优先保证一致性和分区容错性,放弃可用性。在数据一致性要求比较高的场合(譬如:zookeeper,Hbase) 是比较常见的做法,一旦发生网络故障或者消息丢失,就会牺牲用户体验,等恢复之后用户才逐渐能访问。

3.AP: 优先保证可用性和分区容错性,放弃一致性。NoSQL中的Cassandra 就是这种架构。跟CP一样,放弃一致性不是说一致性就不保证了,而是逐渐的变得一致。

写在后面的话

每天认真洗脸,多读书,按时睡,少食多餐。

变得温柔,大度,继续善良,保持爱心。

不在人前矫情,四处诉说以求宽慰,而是学会一个人静静面对,自己把道理想通。

这样的你,单身也无所谓啊,你在那么虔诚地做更好的自己,一定会遇到最好的。

后续

TODO后面深入的理解下分布式锁的实现,包括代码方面和具体的业务场景。

下集预告

既然说了分布式锁,那么就再来一个分布式事物吧!

参考借鉴

分布式锁的三种实现方式及对比分析