面试官:谈谈分布式锁的实现


最近小莱去大厂面试,最终挂在了分布式锁上,于是回来后认真整理了这篇文章,以期下次面试遇到同样的问题时一雪前耻......

一、什么是分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。举个通俗易懂的例子:网吧打游戏。

小莱去网吧打游戏,路上碰巧遇到了同学小王和小丁,三人同时来到网吧前台表示都想在包厢里上网。但是包厢只有一个,同一时间也只能容纳一人,前台MM很为难。突然,前台MM心生一计,将一枚硬币抛于空中,让他们三人同时争抢,谁能抢到谁去包厢。只见小莱眼疾手快最终将硬币据为己有,看着不甘的小王和小丁,哼着小曲进了包厢.....

在这个例子中,小莱、小王和小丁可以看成三个独立分布的客户端(三个独立系统),小莱在包厢上网的时间看作锁的时间,包厢可以看作同一资源。同一时刻三人都想去包厢(即都想访问同一资源),那么硬币就可以作为一把分布式锁限制同一时刻共享包厢的人员。

二、分布式锁的特点

1、互斥性

任意时刻,只有一个客户端能够持有锁。

 2、不会发送死锁

即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

 3、容错性

只要大部分的redis节点正常运行,客户端就可以加锁和解锁。

 4、解锁

加锁和解锁必须为同一个客户端,客户端不能解锁他人的锁。

三、分布式锁的实现

  • 基于redis实现

  • 基于mysql乐观锁实现

  • 基于zookeeper实现

在这篇文章中,我们重点来讲述如何通过redis来实现分布式锁。 

四、加锁实现方式

常用redis命令

  • setnx:在指定的key不存在时,为key设置指定值

  • expire:设置key过期时间,单位以秒计

  • getset:设置指定key的值,并返回key的旧值

错误示例

1、通过setnx、expire实现

 实现思路:在当前锁没有被占用的情况下,加锁成功后,给锁设置一个过期时间。

这乍看没有什么问题,但是仔细思考之后就会发现由于setnx/expire不具有原子性,某一时刻进程在执行expire前突然崩溃,就会导致该锁永久存在,后续进程在获取锁时发现锁已被占用,从而导致无法加锁。

 2、将锁的值设为过期时间,通过锁的值对比实现

 这段代码实现的缺点是:

  1. 需要分布式下每个客户端的时间保持一致;

  2. 锁快过期时,多个客户端同时执行getSet,虽然最终只有一个客户端可以加锁,但该客户端锁的过期时间可能被其他客户端覆盖;

  3. 不具备拥有者标识,任何客户端都可以解锁。

 正确示例

 参数说明:

  • nx:SET IF NOT EXIST,即当key不存在时,进行set操作,若key已经存在,则不做任何操作

  • px:给key加一个过期时间,单位ms

redis2.8版本后,set里提供了px参数,因此我们在实现分布式锁的时候就可以进行原子操作,同时加锁操作也变得简单。

通过上述示例,我们已经清楚地知道了加锁的实现方式,但是解铃还须系铃人,解锁如何实现呢?

五、解锁实现方式

常用redis命令

  • del:用于删除已存在的键

  • pttl:以毫秒为单位返回key的剩余过期时间

错误示例

1、最常见的一种错误解锁方式是直接通过删除del来进行的:

 这种方式的错误在于不具有拥有者标识,任何客户端都可以随时进行解锁。

2、有人可能会说,加锁时给每个客户端分配一个唯一的value值,每次释放锁前把锁的值与客户端传过来的值做对比,相同再删除不就行了,即:

这种方式确实在一般情况下能够解决锁被其他客户端随意释放的问题,但是这样实现会有什么问题呢?答案是当客户端A在执行del之前,锁突然过期了,此时客户端B加锁成功,然后客户端A执行del操作则会将客户端B的锁解除。这还是因为删除不具有原子性。

:在这里还有一种解决临界条件下客户端A锁被其他客户端释放的方式,只是对性能可能有一些影响:在del前,我们可以先判断锁的过期时间,如果当前时间不小于10ms(根据自己的业务而定)的话可以操作del删除,否则自然释放,即:

 

正确示例

锁的释放包含了get、判断、del三个步骤。如果不能保证三个步骤的原子性,分布式锁就会有并发问题。

通过redis里eval命令操作lua代码,这样可以确保在解锁时保持原子性,而不会因为进程的崩溃导致解锁失败。

六、思考

到这里我们就完成了分布式锁的实现,请继续思考:

1、当在集群中,某个master节点宕机后,master数据未及时同步至slave节点时,上述示例是否还能满足当前场景?此时会发生什么样的情况?又该如何来解决?

上边讲述的示例适用于单实例或对业务要求性不高的情况,当在集群上实现分布式锁的时候,master节点宕机且数据未同步至slave节点时,此时就会出现多个客户端拥有一把锁的情况,失去了锁的互斥性原则。

基于此,redis官方提出了RedLock的实现方案,核心思想是同时使用多个Redis Master来冗余,且这些节点是完全独立的,也不需要对这些节点之间的数据进行同步。获取集群中多数master节点上的锁,同时全部获取,否则全部释放。

例如下图的集群中,同时在一半以上(2个master)的master上加锁,如果其中某一个master宕机,客户端仍然可以获取到锁。

 

Redis cluster集群图

2、业务未处理完面临锁时间到期如何处理?

 还是开头那个例子,小莱在包厢里打游戏,任务做到一半,时间到了,这时怎么办呢?有经验的同学第一反应肯定是去续费。

 那么对应到锁的应用上也是这样,当占有锁的时间快到了但是此时业务未处理完,可以延长锁的过期时间,即锁支持可重入。

七、总结

1、无论加锁还是解锁,都要确保原子性操作;

2、Redis分布式锁要考虑单实例和多实例的情况;

3、正确加锁方式:

 

如果当前业务可容忍多个客户端拥有一把锁或保证master不会发生故障,在集群中也可以使用这种方式。

4、正确解锁方式:

关于作者

作者:大家好,我是莱乌,BAT搬砖工一枚。从小公司进入大厂,一路走来收获良多,想将这些经验分享给有需要的人,因此创建了公众号「IT界农民工」。定时更新,希望能帮助到你。

  

原文地址:https://www.cnblogs.com/caoyier/p/13458330.html