分布式工具之redisson

分布式工具之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解决主从一致问题

  1. 问题描述 为了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解决主从一致问题

  1. 问题描述 为了redis可靠,我们一般会使用Redis主从模式 使用了主从模式,一般会采用读写分离策略,主节点写,从节点读! [图片上传失败...(image-f89dfb-1683640012794)]

那么,当数据被写入主节点的时候,主节点时需要向从节点去同步数据的!

这个过程一定会有延时,一致性问题也就发生在这里!

假如,在主节点中获取到了锁,在主节点向从节点同步这个锁信息的时候,主节点宕机了!那么从节点就会从中挑选一个作为主节点!

可是,此时之前的锁信息就丢失了!也就发生了锁失效的问题!!! [图片上传失败...(image-a37de6-1683640012795)]

  1. 解决方案 之前我们分析了,主从模式是导致锁失效的原因,所以Redisson中就直接将它们视为相同的角色! 此时,我们获取锁的方式就变了,获取锁的时候,我们需要依次向全部节点获取锁,只有都获取成功时才算成功!!! [图片上传失败...(image-e93ff4-1683640012793)]

    如果此时也发生了刚刚描述的问题,是不会出现锁失效的问题的! [图片上传失败...(image-ac6b02-1683640012792)]

    这套方案就是Redisson中的联锁——MultiLock

  2. 代码实现

@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();

六.总结

  1. 解决方案 之前我们分析了,主从模式是导致锁失效的原因,所以Redisson中就直接将它们视为相同的角色! 此时,我们获取锁的方式就变了,获取锁的时候,我们需要依次向全部节点获取锁,只有都获取成功时才算成功!!! [图片上传失败...(image-a332e5-1683640000802)]

    如果此时也发生了刚刚描述的问题,是不会出现锁失效的问题的! [图片上传失败...(image-400e34-1683640000802)]

    这套方案就是Redisson中的联锁——MultiLock

  2. 代码实现

@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)