分布式工具之redisson
目录
一.Redisson的引入
- 1、不可重入
- 2、不可重试
- 3、超时释放
- 4、主从一致
二.Redisson
- 1、什么Redisson
- 2、Redisson使用手册
- 3、Redisson快速入门
三.Redisson解决可重入锁
四.Redisson解决重试、超时续约问题
五.Redisson解决主从一致问题
六.总结
1.Redisson的引入
我们先看看之前基于setnx实现的分布式锁存在的问题: 不可重入:同一个线程无法多次获取同一把锁 不可重试:获取锁只尝试一次就返回false,没有重试机制 超时释放:锁超时释放虽然避免死锁,但是业务执行耗时较长,也会导致释放存在安全隐患 主从一致:如果redis提供了主从集群,主从同步存在延迟,当主节点宕机,如果从节点同步主中的锁数据,则会出现锁失效
1、不可重入
简单的来说就是一旦setnx [key] [value]后,就不能再对这个key做任何操作了(除了删除)
假设我们在开发中有A和B两个业务,在业务A中,执行了setnx操作,然后在业务A中调用业务B。然后在业务B中也有setnx的操作(同一个KEY),此时,业务B就会阻塞在这里,等待业务A释放锁,但是,业务A肯定不会释放锁,因为业务A还没有执行完(调B)。故就会发生死锁。
2、不可重试
在我们之前业务逻辑中,尝试获取锁,如果获取不到就直接return了,没有“重来”的机会!也无法提供重试的机制!
3、超时释放
虽然我们可以通过加超时时间,可以解决当业务执行发生异常后出现死锁,但是,仍然会存在隐患!,我们这里是用TTL来控制它。业务执行时间都是未知数,TTL要咋样设置?如何处理执行业务阻塞?
3、主从一致
在主节点上获取到了锁,但是主节点突然宕机了,就会从从结点中选出一个节点,作为主节点。 但由于,因为之前的那个主节点宕机了。在新选举出来的这个主节点中是无法获取到之前的锁。 所以之前的那个锁相当于失效了!
2.Redisson
要解决上述问题并不是那么容易的,如果我们自己实现很有可能会出一些问题!所以最好的办法就是使用市面上的一些框架来解决!
1、什么Redisson?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
2、Redisson使用手册 ?
Redission使用手册https://www.bookstack.cn/read/redisson-wiki-zh/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D.md里面提到了Redisson可以实现大致如下的分布式锁
3、Redisson快速入门
org.redisson
redisson
3.13.6
/**
* 配置 Redisson
*/
@Configuration
public class RedisConfig
@Bean
public RedissonClient redissonClient()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.20:6379").setPassword("123456");
// 创建 RedissonClient 对象
return Redisson.create(config);
@Test
void testRedisson() throws Exception
RLock anyLock = redissonClient.getLock("anyLock");
boolean isLock = anyLock.tryLock(1, 10, TimeUnit.SECONDS);
if(isLock)
try
System.out.println("执行业务");
finally
anyLock.unlock();
3.Redisson解决可重入锁 4.Redisson解决重试、超时续约问题 5.Redisson解决主从一致问题
三.Redisson解决可重入锁
这里可重入的实现和java的ReentrantLock类似! 获取锁的时候,先判断是否是同一个对象,是就val+1, 是否锁的时候就val - 1, 当小于0就将key删除!(redisson帮我们做好了) [图片上传失败...(image-97ec9-1683640000804)]
核心实现主要是lua脚本: 获取锁:
-- 判断锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 不存在,获取锁
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置有效期
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 锁已经存在,判断是否是自己?!
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 自增+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 重置有效期
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
释放锁:
-- 判断当前锁是否还是被自己持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
-- 不是就就直接返回
return nil;
end;
-- 是自己,则重入次数 -1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 判断重入次数是否已经为0
if (counter > 0) then
-- 大于0,说明不能释放,重置有效期即可
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
-- 等于0,说明可以直接删除
redis.call('del', KEYS[1]);
-- 发消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
四.Redisson解决重试、超时续约问题
实现重试、超时续约问题流程 [图片上传失败...(image-e0f263-1683640000804)]
可重试:不是简单while循环,利用信号量和发布订阅功能实现等待,唤醒,获取锁失败的重试机制;好处,不浪费cpu性能 超时续约: waitTime:是最大等待时间,如果使用 tryLock() 的时候,有传参数表明是可重试的锁;反之,不是!
leaseTime:超时释放时间,默认是-1,建议不要设定,Redisson看门狗机制可以就行锁续约!
锁重试核心代码:
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
// 转成毫秒,后面都是以毫秒为单位
long time = unit.toMillis(waitTime);
// 当前时间
long current = System.currentTimeMillis();
// 线程ID-线程标识
long threadId = Thread.currentThread().getId();
// 尝试获取锁 tryAcquire() !!!
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 如果上面尝试获取锁返回的是null,表示成功;如果返回的是时间则表示失败。
if (ttl == null)
return true;
// 剩余等待时间 = 最大等待时间 -(用现在时间 - 获取锁前的时间)
time -= System.currentTimeMillis() - current;
// 剩余等待时间 < 0 失败
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
// 再次获取当前时间
current = System.currentTimeMillis();
// 重试逻辑,但不是简单的直接重试!
// subscribe是订阅的意思
RFuture subscribeFuture = subscribe(threadId);
// 如果在剩余等待时间内,收到了释放锁那边发过来的publish,则才会再次尝试获取锁
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS))
if (!subscribeFuture.cancel(false))
subscribeFuture.onComplete((res, e) ->
if (e == null)
// 取消订阅
unsubscribe(subscribeFuture, threadId);
);
// 获取锁失败
acquireFailed(waitTime, unit, threadId);
return false;
try
// 又重新计算了一下,上述的等待时间
time -= System.currentTimeMillis() - current;
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
// 重试!
while (true)
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 成功
if (ttl == null)
return true;
// 又获取锁失败,再次计算上面的耗时
time -= System.currentTimeMillis() - currentTime;
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
currentTime = System.currentTimeMillis();
// 采用信号量的方式重试!
if (ttl >= 0 && ttl < time)
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
else
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
// 重新计算时间(充足就继续循环)
time -= System.currentTimeMillis() - currentTime;
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
finally
unsubscribe(subscribeFuture, threadId);
五.Redisson解决主从一致问题
- 问题描述 为了redis可靠,我们一般会使用Redis主从模式 使用了主从模式,一般会采用读写分离策略,主节点写,从节点读! [图片上传失败...(image-1c4f09-1683640000803)]
那么,当数据被写入主节点的时候,主节点时需要向从节点去同步数据的!
这个过程一定会有延时,一致性问题也就发生在这里!
假如,在主节点中获取到了锁,在主节点向从节点同步这个锁信息的时候,主节点宕机了!那么从节点就会从中挑选一个作为主节点!
可是,此时之前的锁信息就丢失了!也就发生了锁失效的问题!!! [图片上传失败...(image-402cb5-1683640000804)]# 分布式工具之redisson
目录
一.Redisson的引入
- 1、不可重入
- 2、不可重试
- 3、超时释放
- 4、主从一致
二.Redisson
- 1、什么Redisson
- 2、Redisson使用手册
- 3、Redisson快速入门
三.Redisson解决可重入锁
四.Redisson解决重试、超时续约问题
五.Redisson解决主从一致问题
六.总结
1.Redisson的引入
我们先看看之前基于setnx实现的分布式锁存在的问题: 不可重入:同一个线程无法多次获取同一把锁 不可重试:获取锁只尝试一次就返回false,没有重试机制 超时释放:锁超时释放虽然避免死锁,但是业务执行耗时较长,也会导致释放存在安全隐患 主从一致:如果redis提供了主从集群,主从同步存在延迟,当主节点宕机,如果从节点同步主中的锁数据,则会出现锁失效
1、不可重入
简单的来说就是一旦setnx [key] [value]后,就不能再对这个key做任何操作了(除了删除)
假设我们在开发中有A和B两个业务,在业务A中,执行了setnx操作,然后在业务A中调用业务B。然后在业务B中也有setnx的操作(同一个KEY),此时,业务B就会阻塞在这里,等待业务A释放锁,但是,业务A肯定不会释放锁,因为业务A还没有执行完(调B)。故就会发生死锁。
2、不可重试
在我们之前业务逻辑中,尝试获取锁,如果获取不到就直接return了,没有“重来”的机会!也无法提供重试的机制!
3、超时释放
虽然我们可以通过加超时时间,可以解决当业务执行发生异常后出现死锁,但是,仍然会存在隐患!,我们这里是用TTL来控制它。业务执行时间都是未知数,TTL要咋样设置?如何处理执行业务阻塞?
3、主从一致
在主节点上获取到了锁,但是主节点突然宕机了,就会从从结点中选出一个节点,作为主节点。 但由于,因为之前的那个主节点宕机了。在新选举出来的这个主节点中是无法获取到之前的锁。 所以之前的那个锁相当于失效了!
2.Redisson
要解决上述问题并不是那么容易的,如果我们自己实现很有可能会出一些问题!所以最好的办法就是使用市面上的一些框架来解决!
1、什么Redisson?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
2、Redisson使用手册 ?
Redission使用手册https://www.bookstack.cn/read/redisson-wiki-zh/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D.md里面提到了Redisson可以实现大致如下的分布式锁
3、Redisson快速入门
org.redisson
redisson
3.13.6
/**
* 配置 Redisson
*/
@Configuration
public class RedisConfig
@Bean
public RedissonClient redissonClient()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.20:6379").setPassword("123456");
// 创建 RedissonClient 对象
return Redisson.create(config);
@Test
void testRedisson() throws Exception
RLock anyLock = redissonClient.getLock("anyLock");
boolean isLock = anyLock.tryLock(1, 10, TimeUnit.SECONDS);
if(isLock)
try
System.out.println("执行业务");
finally
anyLock.unlock();
3.Redisson解决可重入锁 4.Redisson解决重试、超时续约问题 5.Redisson解决主从一致问题
三.Redisson解决可重入锁
这里可重入的实现和java的ReentrantLock类似! 获取锁的时候,先判断是否是同一个对象,是就val+1, 是否锁的时候就val - 1, 当小于0就将key删除!(redisson帮我们做好了) [图片上传失败...(image-1057e7-1683640012795)]
核心实现主要是lua脚本: 获取锁:
-- 判断锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 不存在,获取锁
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置有效期
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 锁已经存在,判断是否是自己?!
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 自增+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 重置有效期
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
释放锁:
-- 判断当前锁是否还是被自己持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
-- 不是就就直接返回
return nil;
end;
-- 是自己,则重入次数 -1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 判断重入次数是否已经为0
if (counter > 0) then
-- 大于0,说明不能释放,重置有效期即可
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
-- 等于0,说明可以直接删除
redis.call('del', KEYS[1]);
-- 发消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
四.Redisson解决重试、超时续约问题
实现重试、超时续约问题流程 [图片上传失败...(image-1ed1f0-1683640012795)]
可重试:不是简单while循环,利用信号量和发布订阅功能实现等待,唤醒,获取锁失败的重试机制;好处,不浪费cpu性能 超时续约: waitTime:是最大等待时间,如果使用 tryLock() 的时候,有传参数表明是可重试的锁;反之,不是!
leaseTime:超时释放时间,默认是-1,建议不要设定,Redisson看门狗机制可以就行锁续约!
锁重试核心代码:
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
// 转成毫秒,后面都是以毫秒为单位
long time = unit.toMillis(waitTime);
// 当前时间
long current = System.currentTimeMillis();
// 线程ID-线程标识
long threadId = Thread.currentThread().getId();
// 尝试获取锁 tryAcquire() !!!
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 如果上面尝试获取锁返回的是null,表示成功;如果返回的是时间则表示失败。
if (ttl == null)
return true;
// 剩余等待时间 = 最大等待时间 -(用现在时间 - 获取锁前的时间)
time -= System.currentTimeMillis() - current;
// 剩余等待时间 < 0 失败
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
// 再次获取当前时间
current = System.currentTimeMillis();
// 重试逻辑,但不是简单的直接重试!
// subscribe是订阅的意思
RFuture subscribeFuture = subscribe(threadId);
// 如果在剩余等待时间内,收到了释放锁那边发过来的publish,则才会再次尝试获取锁
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS))
if (!subscribeFuture.cancel(false))
subscribeFuture.onComplete((res, e) ->
if (e == null)
// 取消订阅
unsubscribe(subscribeFuture, threadId);
);
// 获取锁失败
acquireFailed(waitTime, unit, threadId);
return false;
try
// 又重新计算了一下,上述的等待时间
time -= System.currentTimeMillis() - current;
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
// 重试!
while (true)
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 成功
if (ttl == null)
return true;
// 又获取锁失败,再次计算上面的耗时
time -= System.currentTimeMillis() - currentTime;
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
currentTime = System.currentTimeMillis();
// 采用信号量的方式重试!
if (ttl >= 0 && ttl < time)
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
else
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
// 重新计算时间(充足就继续循环)
time -= System.currentTimeMillis() - currentTime;
if (time <= 0)
acquireFailed(waitTime, unit, threadId);
return false;
finally
unsubscribe(subscribeFuture, threadId);
五.Redisson解决主从一致问题
- 问题描述 为了redis可靠,我们一般会使用Redis主从模式 使用了主从模式,一般会采用读写分离策略,主节点写,从节点读! [图片上传失败...(image-f89dfb-1683640012794)]
那么,当数据被写入主节点的时候,主节点时需要向从节点去同步数据的!
这个过程一定会有延时,一致性问题也就发生在这里!
假如,在主节点中获取到了锁,在主节点向从节点同步这个锁信息的时候,主节点宕机了!那么从节点就会从中挑选一个作为主节点!
可是,此时之前的锁信息就丢失了!也就发生了锁失效的问题!!! [图片上传失败...(image-a37de6-1683640012795)]
-
解决方案 之前我们分析了,主从模式是导致锁失效的原因,所以Redisson中就直接将它们视为相同的角色! 此时,我们获取锁的方式就变了,获取锁的时候,我们需要依次向全部节点获取锁,只有都获取成功时才算成功!!! [图片上传失败...(image-e93ff4-1683640012793)]
如果此时也发生了刚刚描述的问题,是不会出现锁失效的问题的! [图片上传失败...(image-ac6b02-1683640012792)]
这套方案就是Redisson中的联锁——MultiLock
代码实现
@Configuration
public class RedisConfig
@Bean
public RedissonClient redissonClient()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.89.128:6379").setPassword("888888");
// 创建 RedissonClient 对象
return Redisson.create(config);
@Bean
public RedissonClient redissonClient2()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.89.128:6380");
// 创建 RedissonClient 对象
return Redisson.create(config);
@Bean
public RedissonClient redissonClient3()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.89.128:6381");
// 创建 RedissonClient 对象
return Redisson.create(config);
@SpringBootTest
@Slf4j
public class RedissonTest
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;
private RLock lock;
@BeforeEach
void setUp()
RLock lock1 = redissonClient.getLock("order");
RLock lock2 = redissonClient.getLock("order");
RLock lock3 = redissonClient.getLock("order");
// 创建联锁
lock = redissonClient.getMultiLock(lock1, lock2, lock3);
@Test
void method1() throws InterruptedException
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
if (!isLock)
log.error("获取锁失败 ... 1");
return;
try
log.info("获取锁成功 ... 1");
method2();
log.info("开始执行业务 ... 1");
finally
log.warn("准备释放锁 ... 1");
lock.unlock();
@Test
void method2()
boolean isLock = lock.tryLock();
if (!isLock)
log.error("获取锁失败 ... 2");
return;
try
log.info("获取锁成功 ... 2");
log.info("开始执行业务 ... 2");
finally
log.warn("准备释放锁 ... 2");
lock.unlock();
六.总结
-
解决方案 之前我们分析了,主从模式是导致锁失效的原因,所以Redisson中就直接将它们视为相同的角色! 此时,我们获取锁的方式就变了,获取锁的时候,我们需要依次向全部节点获取锁,只有都获取成功时才算成功!!! [图片上传失败...(image-a332e5-1683640000802)]
如果此时也发生了刚刚描述的问题,是不会出现锁失效的问题的! [图片上传失败...(image-400e34-1683640000802)]
这套方案就是Redisson中的联锁——MultiLock
代码实现
@Configuration
public class RedisConfig
@Bean
public RedissonClient redissonClient()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.89.128:6379").setPassword("888888");
// 创建 RedissonClient 对象
return Redisson.create(config);
@Bean
public RedissonClient redissonClient2()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.89.128:6380");
// 创建 RedissonClient 对象
return Redisson.create(config);
@Bean
public RedissonClient redissonClient3()
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.89.128:6381");
// 创建 RedissonClient 对象
return Redisson.create(config);
@SpringBootTest
@Slf4j
public class RedissonTest
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;
private RLock lock;
@BeforeEach
void setUp()
RLock lock1 = redissonClient.getLock("order");
RLock lock2 = redissonClient.getLock("order");
RLock lock3 = redissonClient.getLock("order");
// 创建联锁
lock = redissonClient.getMultiLock(lock1, lock2, lock3);
@Test
void method1() throws InterruptedException
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
if (!isLock)
log.error("获取锁失败 ... 1");
return;
try
log.info("获取锁成功 ... 1");
method2();
log.info("开始执行业务 ... 1");
finally
log.warn("准备释放锁 ... 1");
lock.unlock();
@Test
void method2()
boolean isLock = lock.tryLock();
if (!isLock)
log.error("获取锁失败 ... 2");
return;
try
log.info("获取锁成功 ... 2");
log.info("开始执行业务 ... 2");
finally
log.warn("准备释放锁 ... 2");
lock.unlock();