分布式锁原理篇-Redisson、Zookeeper、Database

1.分布式锁起源

在分布式出现之前,一般多线程操作,为了防止高并发产生的问题,使用Synchronize和ReentrantLock等加锁方式解决。因为项目服务采用的是单进程的多线程操作。而如今,集群和微服务化兴起的时代,单进程锁已经无法满足我们的业务需求。于是,也就出现了今天我们要讲到的分布式锁的概念。

对于分布式锁的设计,主要需要满足一下几个点:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
     

2.分布式锁分类

分布式锁目前大体可以分为三类:Redisson、Zookeeper、Database

2.1 Redisson分布式锁

说到Redisson,大家应该都不陌生。Redisson是基于Redis内存数据库封装而来的框架。在集成Redis大部分功能的同时,且实现了类似ReentrantLock的锁功能。

原理:Redisson的锁,其实是通过Redis的setnx和del等方法结合lua脚本语言来实现的。其中获取锁和释放锁的流程如下:

分布式锁原理篇-Redisson、Zookeeper、Database

而在加锁和解锁的关键,又在于lua脚本代码(使用lua代码执行可保证操作的原子性)。加锁时的lua脚本说明如下:

-- 1.  没被锁{key不存在}
eval "return redis.call('exists', KEYS[1])" 1 myLock
-- (1) 设置Lock为key,uuid:threadId为filed, filed值为1
eval "return redis.call('hset', KEYS[1], ARGV[2], 1)" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (2) 设置key过期时间{防止获取锁后线程挂掉导致死锁}
eval "return redis.call('pexpire', KEYS[1], ARGV[1])" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 2. 已经被同线程获得锁{key存在并且field存在}
eval "return redis.call('hexists', KEYS[1], ARGV[2])" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (1) 可重入,但filed字段+1
eval "return redis.call('hincrby', KEYS[1], ARGV[2],1)" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (2) 刷新过去时间
eval "return redis.call('pexpire', KEYS[1], ARGV[1])" 1 myLock 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 3. 已经被其他线程锁住{key存在,但是field不存在}:以毫秒为单位返回 key 的剩余超时时间
eval "return redis.call('pttl', KEYS[1])" 1 myLock

解锁的lua代码说明:

-- 1. key不存在
eval "return redis.call('exists', KEYS[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- (1) 发送释放锁的消息,返回1,释放成功
eval "return redis.call('publish', KEYS[2], ARGV[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 2. key存在,但field不存在,说明自己不是锁持有者,无权释放,直接return nil
eval "return redis.call('hexists', KEYS[1], ARGV[3])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
eval "return nil"

-- 3. filed存在,说明是本线程在锁,但有可能其他地方重入锁,不能直接释放,应该-1
eval "return redis.call('hincrby', KEYS[1], ARGV[3],-1)" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 4. 如果减1后大于0,说明还有其他重入锁,刷新过期时间,返回0。
eval "return redis.call('pexpire', KEYS[1], ARGV[2])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110

-- 5. 如果不大于0,说明最后一把锁,需要释放
-- 删除key
eval "return redis.call('del', KEYS[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- 发释放消息
eval "return redis.call('publish', KEYS[2], ARGV[1])" 2 myLock redisson_lock__channel_lock 0 3000 3d7b5418-a86d-48c5-ae15-7fe13ef0034c:110
-- 返回1,释放成功

Redisson特点:

  • 可以使用可重入锁,公平锁,联锁,红锁等多种方式
  • 具有WatchDog作为缓存续期监听机制

Redisson具体使用方式请参考官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

2.2 Zookeeper

对于zookeeper,之前很多业务是结合Dubbo作为分布式服务调用框架来设计的。在此过程中,zookeeper作为注册中心的角色负责各个服务接口的注册与调度。但其实zookeeper还有很多其他用途,比如配置管理,集群管理,队列管理以及我们今天要说到的分布式锁。(zookeeper其他用途了解参考:https://blog.****.net/lingbo229/article/details/81052078

zookeeper实现分布式锁的原理:

分布式锁原理篇-Redisson、Zookeeper、Database

原理就是多个节点同时在一个指定的节点下面创建临时会话顺序节点,谁创建的节点序号最小,谁就获得了锁,并且其他节点就会监听序号比自己小的节点,一旦序号比自己小的节点被删除了,其他节点就会得到相应的事件,然后查看自己是否为序号最小的节点,如果是,则获取锁。

具体实现可参考:https://my.oschina.net/yangjianzhou/blog/1930493

2.3 Database实现分布式锁

实现原理:采用数据库表的唯一键的形式。如果同一个时刻,多个线程同时向一个表中插入同样的记录,由于唯一键的原因,只能有一个线程插入成功。待业务逻辑代码执行完后,删除这条记录,来释放锁。(因为此种方式不合适实际应用场景,对DB的开销较大,且无法达到高可用,所以对于实现方式不做过多说明)

 

参考链接:

https://blog.****.net/qq_26620259/article/details/84292965

https://blog.****.net/loushuiyifan/article/details/82497455