基于Redisson(Redis客户端)实现的分布式锁

1、Redisson简介

  Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

关于Redisson项目的详细介绍可以在官方网站找到。

2、Redisson的RLock分布式锁

  在《基于Redis实现的分布式锁》中,我们通过Redis的setNX+Lua方法实现了分布式锁,而Redisson是一个Redis客户端,是 Redis 官网推荐的 java 语言实现分布式锁的项目。

2.1、RLock层级结构

在这里插入图片描述
  在Redisson中,Redisson 分布式锁的实现是基于 RLock 接口,而 RLock 锁接口实现源码主要是 RedissonLock 这个类,而源码中加锁、释放锁等操作都是使用 Lua 脚本来完成的,并且封装的非常完善,开箱即用。

  • RedissonWriteLock 写锁
  • RedissonReadLock 读锁
  • RedissonTransactionalLock 事务锁
  • RedissonFairLock 公平锁
  • RedissonRedLock 红锁
2.2、RLock 常用接口
public interface RLock extends Lock, RLockAsync {
    
    
	//获取名称
    String getName();
    /**
     * 中断锁 表示该锁可以被中断 假如A和B同时调这个方法,A获取锁,B为获取锁,那么B线程可以通过
     * Thread.currentThread().interrupt(); 方法真正中断该线程
     */
    void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException;
	/**
     * tryLock(long waitTime, long leaseTime, TimeUnit unit)方法和tryLock()方法(在Lock接口中定义)是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,且可以自己设置锁失效时间,后者默认30s。
     * 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
     *
     * @param waitTime 获取锁的最大的等待时间
     * @param leaseTime 锁失效时间
     * @param unit 时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
	/**
     * 加锁 上面是默认30秒这里可以手动设置锁的有效时间
     *
     * @param leaseTime 锁有效时间
     * @param unit  时间单位 小时、分、秒、毫秒等
     */
    void lock(long leaseTime, TimeUnit unit);
	//强制解锁
    boolean forceUnlock();
	/**
     * 检验该锁是否被线程使用,如果被使用返回True
     */
    boolean isLocked();
	/**
     * 检查指定线程是否获得此锁
     */
    boolean isHeldByThread(long threadId);
	/**
     * 检查当前线程是否获得此锁
     */
    boolean isHeldByCurrentThread();
	//当前线程对该锁的持有数
    int getHoldCount();
	//锁的剩余有效时间
    long remainTimeToLive();
    
}

3、基于Redisson分布式锁的实践

  这里我们基于上一篇《基于Redis实现的分布式锁》中的项目进行改造。

3.1、修改pom文件

  去掉原来的redis依赖(下面依赖已经包含了),直接引入如下依赖即可。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.14.0</version>
</dependency>
3.2、修改测试类DemoController

 &esmp;我们这里使用单Redis节点的模式,在原来的基础上只需要引入redisson依赖,然后修改DemoController即可,首先通过@Autowired注解注入RedissonClient对象,然后通过该对象获取锁,再使用即可。

@RestController
public class DemoController {

    private Logger logger = LoggerFactory.getLogger(DemoController.class);

    @Autowired
    private RedissonClient redissonClient;

    @RequestMapping("redissonLock")
    public String testLock() {
        logger.debug("进入testLock()方法;");
        //“order"表示业务模块

        RLock rLock = redissonClient.getLock("order");
        try{
            if(rLock.tryLock(1,25, TimeUnit.SECONDS)){
                logger.debug("获取到锁了;");
                Thread.sleep(20 * 1000);
            }else{
                logger.debug("获取锁失败了;");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        logger.debug("方法执行完成;");
        return "方法执行完成";
    }

}
3.3、总结

  该分布式锁的测试方法和前面类似,这里不再重复。通过前面的简单使用,我们可以知道,其实就是使用了Redisson框架的分布式锁,不需要我们再自己定义锁了,方便了很多,同时很多易错的细节已经被框架考虑了。

4、看门狗Watchdog

  前面基于Redis的分布式锁实现和前面基于Redssion的简单使用,都存在一个问题:当锁的失效时间到了,但是业务逻辑还没有执行完成就会出现问题,即因为锁失效,其他线程就可以继续使用该分布式锁了。针对这类问题,在Redssion框架中,通过看门狗机制进行处理。

4.1、实现原理

  为了避免锁超期失效这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

4.2、测试

  使用不带参数的rLock.tryLock()方法时,就会自动的启用看门狗,如下所示,为了演示效果,我这里把休眠时间调成了60s(而锁的失效时间是30s)。

如果使用带参数的rLock.tryLock()方法,需要保证看门狗配置的参数lockWatchdogTimeout < leaseTime(锁失效时间),否则看门狗将失效。

 @RequestMapping("redissonLock")
 public String testLock() {
      logger.debug("进入testLock()方法;");
      //“order"表示业务模块

      RLock rLock = redissonClient.getLock("order");
      try{
          if(rLock.tryLock()){
              logger.debug("获取到锁了;");
              Thread.sleep(60 * 1000);
          }else{
              logger.debug("获取锁失败了;");
          }
      }catch (Exception e){
          e.printStackTrace();
      }
      logger.debug("方法执行完成;");
      return "方法执行完成";
  }

 &esmp;重新启动项目后(8080、8081两个端口),我们先访问http://localhost:8080/redissonLock,这个时候该线程会获取到锁,等待35s后,访问http://localhost:8081/redissonLock接口,正常已经过了锁失效时间,这个时候线程二应该可以获取到锁的,但时间情况是获取失败。打印日志如下:
在这里插入图片描述
在这里插入图片描述

5、红锁(Redlock)

  在上述单节点情况下,很难保证高可用性的。如果在主从模式、哨兵模式、集群模式模式下,又会出现异步数据丢失和脑裂等问题,那如何实现分布式锁的高可用性并且避免这些问题呢?

  为了解决这些问题,在Redis官网提出了红锁(Redlock)的概念:假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。

参考《Redis 分布式锁之红锁 Redisson java实现》《红锁的实现》

  在实际场景中,红锁是很少使用的。因为使用了红锁后会影响高并发环境下的性能,使得程序的体验更差。而且,在实际场景中,我们一般都是要保证Redis集群的可靠性。同时,使用红锁后,当加锁成功的RLock个数不超过总数的一半时,会返回加锁失败,即使在业务层面任务加锁成功了,但是红锁也会返回加锁失败的结果。另外,使用红锁时,需要提供多套Redis的主从部署架构,同时,这多套Redis主从架构中的Master节点必须都是独立的,相互之间没有任何数据交互。

  使用Redisson框架来实现红锁的用法如下:

public void testRedLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){
    
    
    RLock lock1 = redisson1.getLock("lock1");
    RLock lock2 = redisson2.getLock("lock2");
    RLock lock3 = redisson3.getLock("lock3");
    RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
    try {
    
    
        // 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
        lock.lock();
        // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
        boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    } finally {
    
    
        lock.unlock();
    }
}

猜你喜欢

转载自blog.csdn.net/hou_ge/article/details/112801568