对于分布式应用场景下,当多个线程对公共资源访问时,需要实现排他性,传统的多线程锁已经不能满足(独立于某个进程之外),这时候就需要分布式锁的实现。

分布式锁主要是要求将锁的管理交由外部公共的第三方管理,主要有数据库、Redis、zookeeper三种:

1. 数据库

 

 

对于数据库实现分布式锁,即将锁的数据存储在数据库中,通过lock方法获取锁(至于如何获取暂不举例)。这个做法下呢会存在很多弊端。

第一点就是对于一个用户的业务操作,锁的超时时间的设置(额外进程监控控制,也存在宕机风险)。如果这个用户在操作的时候,锁超时,进行强制锁删除,第二个用户获取锁,进行业务操作,同样会出现一个多卖的现象,所以超时时间很难定义。

第二点是当数据库操作的时候,对于for update的表锁或者行锁未进行释放时,这种排他锁是不允许其他用户操作的,如果对于当前操作用户崩溃或者宕机,就会造成在不考虑超时时间的时间外,造成死锁。

2. Reids

       

 

 

 

 redis分布式锁实现原理:

 

(1)加锁机制

咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。

这里注意,仅仅只是选择一台机器!这点很关键!

紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:

 

 

为啥要用lua脚本呢?

因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。

那么,这段lua脚本是什么意思呢?

KEYS[1]代表的是你加锁的那个key,比如说:

RLock lock = redisson.getLock("myLock");

这里你自己设置了加锁的那个锁key就是“myLock”。

ARGV[1]代表的就是锁key的默认生存时间,默认30秒。

ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:

8743c9c0-0795-4907-87fd-6c719a6b4586:1

给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。

如何加锁呢?很简单,用下面的命令:

hset myLock

8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:

 

 

上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。

接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。

好了,到此为止,ok,加锁完成了。

 

(2)锁互斥机制

那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?

很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。

接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。

此时客户端2会进入一个while循环,不停的尝试加锁。

 

(3)watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

 

(4)可重入加锁机制

那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?

比如下面这种代码:

 

 

这时我们来分析一下上面那段lua脚本。

第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。

第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

此时就会执行可重入加锁的逻辑,他会用:

incrby myLock

8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

通过这个命令,对客户端1的加锁次数,累加1。

此时myLock数据结构变为下面这样:

 

大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

(5)释放锁机制

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。

如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:

“del myLock”命令,从redis里删除这个key。

然后呢,另外的客户端2就可以尝试完成加锁了。

这就是所谓的分布式锁的开源Redisson框架的实现机制。

一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。

(6)上述Redis分布式锁的缺点

其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。

但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。

接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。

此时就会导致多个客户端对一个分布式锁完成了加锁。

这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。

所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

 

 redis分布式锁的简单使用:

redis在锁的实现上呢,同样体现了一个三方持有锁,设置自身超时时间的一个锁    机制获取的模型。这在功能上其实要面对和数据库操作一样的问题。Redis可以通过   SETNX和Expire来设置超时时间(Lua脚本实现),这里对Redis的其他几种锁的方式进         行介绍:

1.redlock算法出现

不做讲解

2.Redisson实现

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例:

 

// 1.构造redisson实现分布式锁必要的Config

Config config = new Config();

config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);

// 2.构造RedissonClient

RedissonClient redissonClient = Redisson.create(config);

// 3.获取锁对象实例(无法保证是按线程的顺序获取到)

RLock rLock = redissonClient.getLock(lockKey);

try {

    /**

     * 4.尝试获取锁

     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败

     * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)

     */

    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);

    if (res) {

        //成功获得锁,在这里处理业务

    }

} catch (Exception e) {

    throw new RuntimeException("aquire lock fail");

}finally{

    //无论如何, 最后都要解锁

    rLock.unlock();

}

这种方式因为最大超时时间leaseTime的存在,依然会出现业务未执行完毕,超时时间后删除当前线程持有锁,第二个线程进入,出现一个多卖的现象。

3. Zookeeper

   通过以上的总结,不难发现,分布式锁在三方持有锁的这个概念中,最大的冲突就是都是在怕用户崩溃或宕机的情况下设置了锁的最大持有时间来避免死锁,而这个时间却总与用户正常处理业务大的时间有冲突。持有时间大于业务时间,程序正常,却会在其他用户不断轮询锁,额外消耗系统资源。持有时间小于业务时间,提前结束锁的持有,就会导致一个多卖的现象,这个时候,我们利用zookeeper中临时文件这一机制,来替换超时时间这个设计。

 

 

 

首先讲解一下临时文件的概念。

 

 

 

在客户端连接zookeeper的某个节点的时候,在根目录下有一个/msd文件,在创建文件的时候,加上如图 -e 的指令,就是创建此客户端的临时节点,这个节点是临时,有序的。他有一个特性,就是在对应客户端断开连接的时候,此临时文件在zookeeper进行心跳检测后,进行移除,此时自然的就进行了锁的释放。并且通知在此文件中含有的其他node节点下的锁获取(node2在node1中也存有数据,进行通知,同步,这是zookeeper的特性)

 

 

 

查看当前临时文件,有当前客户端的唯一标识。

 

通过DistributeLock获取zookeeper中的分布式锁,进行正常操作(锁获取、业务处理、锁释放)。

 

部分内容来自:https://www.cnblogs.com/williamjie/p/11250679.html