基于redis的分布式锁的优化
基于redis的setnx的分布式锁仍存在几个问题:
1.不可重入:同一个线程无法多次获取同一把锁(比如一个线程调用a方法中的b方法,先获取a中的锁,还要获取b中同一把锁就不可以,然后b就只能等待a的释放,而a又在调用b方法的锁,就陷入了死锁)
2.不可重试:获取锁只尝试一次就返回false,无法重试
3.超时释放:设置TTL虽然可以避免死锁,但很难把握一个具体的时间给业务执行(比如有的业务执行时间过长,锁提前被释放,其他线程又获取锁就会造成安全问题)
4.redis主从不一致性:因为Redis的复制是异步的,主节点在写入锁数据后,从节点可能还未复制,此时主节点宕机,从节点晋升为主,导致锁信息丢失。这时候其他客户端可能再次获取锁,造成多个客户端同时持有锁,破坏了互斥性。
redission介绍
redission是redis基础上实现的分布式系统的工具,它提供了很多分布式服务,包括各种分布式锁的实现,所以上述优化的步骤可以借助工具,而不需要亲自敲
redission入门
配置redission客户端
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
// 创建RedissonClient对象
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//创建RedissionClient客户端
return Redisson.create(config);
}
}
测试redission的分布式锁
@Resource
private RedissonClient redissonClient;
private RLock lock;
@Test
void testRedission throws InterruptedException{
//获取锁并且指定所得名称
RLock lock = redissonClient.getLock("anylock");
//尝试获取锁,参数包括获取锁的最大等待时间(期间会不断地重试),锁的自动释放时间,时间单位
//也就表明了获取锁的阻塞式的,可以在最大等待时间内重试
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
//判断释放锁获取锁的成功
if (isLock) {
try{
System.out.println("获取锁成功,执行业务!");
}finally {
lock.unlock();
}
}
}
redission可重入锁原理
关注下面这两个方法,实在一个线程里面,正常的过程是,method1获取了thread1的lock,然后调用method2,然后method2也要获取同一个线程里的thread1的lock,但获取失败,这就是不可重入
而可重入的原理就是,进入method2后做一个判断,判断两个获取锁的是不是同一个线程,是就可以重入再次获取,并且或设置一个字段记录重入的次数,每次释放锁后次数恢复。
redission中采用hash数据结构来对锁进行记录,method1获取锁,value=1,调用method2,获取锁value=2,method2执行完业务,value--,method1执行完业务,value--,在释放锁之前,要先对value值进行判断是否为0,才能释放锁。
redission可重入锁的逻辑图
像redission可重入锁的完整逻辑,很难用java代码去表示,所以采取lua脚本的方式来获取锁
获取锁的lua脚本
1.定义一个key,两个参数
2.首先,利用redis.call(‘exists’,key)判断锁是否存在,不存在就说明是第一个获取锁的,并且设置有效期,count+1
3.若锁已经存在,则判断threadID是否是自己人,不是则获取锁失败,是count++并且重置有效期
释放锁的lua脚本
redission可重试的原理
//尝试获取锁,参数包括获取锁的最大等待时间(期间会不断地重试),锁的自动释放时间,时间单位
//也就表明了获取锁的阻塞式的,可以在最大等待时间内重试
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
上面这句语法解释了可重试的原理,即在设置最大等待时间内,会不断地尝试获取锁,但也不是盲目的不断尝试,期间每当一个其他方法释放了锁,都会发从一个广播通知,tryLock方法的源码中支持订阅广播,得到其他释放锁的消息,若收到则在等待时间内不断重试,减少了CPU的负担
redission超时释放原理
大概就是如果设置了锁的自动释放时间,就不用管,如果没有设置锁的自动释放时间,就会利用看门狗机制将锁的自动释放时间设置为30s,并且在释放锁之前,不断地更新自动释放时间,防止业务执行时间过长。
总结
redission的主从一致性问题
由于redis的主从不一致问题容易产生安全问题:比如一个java应用向redis执行写的操作并获取锁,主节点在完成读写操作后,要向从节点进行数据同步,就在这时,主节点宕机,数据没有同步完成,redis的哨兵机制就会从从节点当中选择一个晋升为主节点,但这是java应用只能向新的主节点发送请求,但之前获取的锁已经失效了,就会导致一定的安全问题。
redission解决方案
取消redis中的主从关系,在发送请求时,向每一个redis节点都发送请求,只有获取所有结点的锁,才算获取锁成功,如果有一个节点宕机,获取成功其他两个结点的锁也是获取锁失败,提升了安全性。即使有一个结点在更新数据时发生宕机,但其他的节点的数据也是同步更新的,提升了可用性
主从一致性测试
创建三个不同端口不同的redission客户端作为节点
@Bean
public RedissonClient redissonClient1(){
// 配置
Config config = new Config();
// 创建RedissonClient对象
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//创建RedissionClient客户端
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient2(){
// 配置
Config config = new Config();
// 创建RedissonClient对象
config.useSingleServer().setAddress("redis://127.0.0.1:6380");
//创建RedissionClient客户端
return Redisson.create(config);
}
@Bean
public RedissonClient redissonClient3(){
// 配置
Config config = new Config();
// 创建RedissonClient对象
config.useSingleServer().setAddress("redis://127.0.0.1:6381");
//创建RedissionClient客户端
return Redisson.create(config);
}
@SpringBootTest
class RedissonTest {
@Resource
private RedissonClient redissonClient1;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;
private RLock lock;
@BeforeEach
void setUp() {
//创建3个锁,代表向三个节点发送的三个请求
RLock lock1 = redissonClient1.getLock("order");
RLock lock2 = redissonClient2.getLock("order");
RLock lock3 = redissonClient3.getLock("order");
//创建联锁 multiLock
lock = redissonClient1.getMultiLock(lock1,lock2,lock3);
}
@Test
void method1() throws InterruptedException {
// 尝试获取锁
boolean isLock = lock.tryLock(10L, TimeUnit.SECONDS);
if (!isLock) {
log.error("获取锁失败 .... 1");
return;
}
try {
log.info("获取锁成功 .... 1");
method2();
log.info("开始执行业务 ... 1");
} finally {
log.warn("准备释放锁 .... 1");
lock.unlock();
}
}
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();
}
}
}
三个节点的锁都出现了计数器的变化说明测试成功