前言
在了解分布式锁的时候,看了很多博客,但是其中大部分的认识是很不够的,甚至不乏很多对RedLock的错误认识。所以本文旨在对分布式锁,Redis的分布式锁的实现原理,Redisson架构的简要分析和Redisson实现分布式锁的源码大概了解做一个分析
目录
- 认识分布式锁
- 分布式锁特性
- 分布式锁实现原理
- Redisson架构
- Redisson实现分布式锁源码分析
- RedLock算法分析
认识分布式锁
Distributed locks are a very useful primitive in many environments
where different processes must operate with shared resources in a
mutually exclusive way. 分布式锁是在分布式环境中保证不同的进程相互独立操作共享资源的一种方式。 —— Redis官网分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 ——百度百科
分布式锁特性
- 安全性:相互独立,任何情况下,只能有一个客户端持有锁。
- 无死锁:即使持有锁的客户端宕机了,其他客户端最终总能获取锁。
- 容错性:只要大多数的Redis节点无故障,客户端总能获取锁和释放锁。
- 阻塞等待:当锁被占用时,阻塞等待锁的释放。
- 可重入性:同一个节点上的同一个线程获取了锁之后,也可以再次获取。
分布式锁的实现方式
基于数据库 | 基于zookeeper | 基于Redis | |
性能 | 一般 | 高 | 最高 |
使用 | 少 | 一般 | 最多 |
实现复杂度 | 一般 | 最高 | 高 |
可靠性 | 一般 | 最高 | 高 |
Redis分布式锁的实现原理
首先我们要想一想,锁主要需要解决的问题
- 互斥访问
- 无死锁
- 高可用
- 可重入
- 阻塞等待
Redis分布式锁是怎么做的呢?
其实就一条语句
SET resource_name my_random_value NX PX 30000
当客户端需要获取锁时,就往redis发送上面的语句,如果resource_name这个key已经存在了,就无法插入到redis中,也就无法获取锁。
如果没有,就将 resource_name -> my_random_value 设置到redis中。当其他客户端也想访问该资源时,就不能访问了,这就解决了互斥访问问题。
那客户端在处理业务的情况下宕机了怎么办,由于redis自带的失效时间处理,就很好的解决了死锁问题。
为什么要设置唯一的value?防止别人误解锁。设置唯一的value,只有加锁的人才能解锁。
那客户端获取到了锁,在执行业务的时候,又有个地方要获取该资源的锁,怎么办呢?(可重入)
那如果客户端没获取到锁,想一直等待锁呢?(阻塞等待)
我们可以看看redission是怎么处理的。
其他值得思考的问题
1.执行过程中,锁的失效时间到期了怎么办?redission设置了一个定时器,每到失效时间的1/3,如果业务还没执行完,就刷新失效时间。
2.设置过程中master节点宕机了怎么办?
Redisson
首先我们来简单认识一下Redisson。
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】
Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。——百科
Redisson架构
Redisson实现分布式锁源码分析
redission分布式锁的使用
1.引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.5</version>
</dependency>
2.配置
单机配置
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.setCodec(TypedJsonJacksonCodec.INSTANCE);
config.useSingleServer()
.setAddress(“redis://127.0.0.1:6379”)
.setTimeout(3000)
.setConnectionPoolSize(1000);
return Redisson.create(config);
}
集群配置
config.useClusterServers()
.setScanInterval(200000)//设置集群状态扫描间隔
.setMasterConnectionPoolSize(10000)//设置对于master节点的连接池中连接数最大为10000
.setSlaveConnectionPoolSize(10000)//设置对于slave节点的连接池中连接数最大为500
.setIdleConnectionTimeout(10000)//如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
.setConnectTimeout(30000)//同任何节点建立连接时的等待超时。时间单位是毫秒。
.setTimeout(3000)//等待节点回复命令的时间。该时间从命令发送成功时开始计时。
.setRetryInterval(3000)//当与某个节点的连接断开时,等待与其重新建立连接的时间间隔。时间单位是毫秒。
.addNodeAddress(
"redis://127.0.0.1:30003",
"redis://127.0.0.1:30006"
);
3.使用
RLock lock = redisson.getLock("anyLock");
// Most familiar locking method
lock.lock();
// Lock time-to-live support
// releases lock automatically after 10 seconds
// if unlock method not invoked
lock.lock(10, TimeUnit.SECONDS);
// Wait for 100 seconds and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
过程分析
加锁
加锁有lock和trylock,这两个方法都利用了发布订阅模式实现阻塞等待,然后都会调用tryAcquire方法。追溯到最后,还是tryLockInnerAsync的一段LUA脚本。
可重入的实现利用的是redis的hash,同一客户端,每次加锁,都会将hash值加1。解锁减1,当hash值为0时,释放锁。
解锁
解锁的过程最后还是一段LUA脚本。
RedLock算法分析
现在我们大概了解了一下redission的分布式锁实现,那么在集群模式下会不会有什么问题呢?
设想一个场景:
客户端A从master获取到锁
在master将锁同步到slave之前,master宕掉了
slave节点被晋级为master节点
客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!
这个问题如何处理呢?
RedLock算法
我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
- 获取当前Unix时间,以毫秒为单位。
- .依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
锁真正的有效时间:MIN_VALIDITY = TTL - (T2 - T1) - CLOCK_DRIFT .
TTL:锁的失效时间
T1:第一个获取到锁的时间
T2: 最后一个获取到锁的时间
CLOCK_DRIFT:时钟漂移(每台计算机的时间可能不同,所以集群中不同节点的通信可能会产生时钟漂移),因为基本都是以客户端的时间频率进行前进的,所以时钟漂移基本可以忽略不计。
Redission中RedLock用法
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
但是有一个问题:这种操作不能保证每个锁都是发送到了不同的实例,如果所有的锁都在一个实例上,一样会遇到问题之前的问题。
于是,Redisson废弃了RedLock。Rlock的一些操作会直接同步到所有的slave节点。
然后我就跑去测试了一下,经测试:
RLock操作会发送到每一个slave节点(Redisson只配master节点,slave节点也会收到)。