在了解分布式锁的时候,看了很多博客,但是其中大部分的认识是很不够的,甚至不乏很多对RedLock的错误认识。所以本文旨在对分布式锁,Redis的分布式锁的实现原理,Redisson架构的简要分析和Redisson实现分布式锁的源码大概了解做一个分析
Distributed locks are a very useful primitive in many environments
where different processes must operate with shared resources in a
mutually exclusive way. 分布式锁是在分布式环境中保证不同的进程相互独立操作共享资源的一种方式。 —— Redis官网
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 ——百度百科
基于数据库 | 基于zookeeper | 基于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是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】
Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。——百科
Redisson架构
redission分布式锁的使用
1.引入依赖
org.redisson
redisson
3.13.5
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时,释放锁。
现在我们大概了解了一下redission的分布式锁实现,那么在集群模式下会不会有什么问题呢?
设想一个场景:
客户端A从master获取到锁
在master将锁同步到slave之前,master宕掉了
slave节点被晋级为master节点
客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!
这个问题如何处理呢?
RedLock算法
我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。
为了取到锁,客户端应该执行以下操作:
锁真正的有效时间: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节点也会收到)。