Redisson实现分布式锁从入门到应用

Posted 结构化思维wz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redisson实现分布式锁从入门到应用相关的知识,希望对你有一定的参考价值。

分布式锁

随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。

  • 在单机环境中,应用是在同一进程下的,通过Java并发包提供的API即可保证线程的安全性。
  • 在集群多机部署的环境中,应用在不同的进程中,也就引出了分布式锁的问题。

白话讲分布式锁:所有请求的线程都去同一个地方占坑,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放坑位。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库

分布式锁的基本原理:

Redis实现分布式锁

青铜->钻石(分布式锁) - 掘金 (juejin.cn)

Redisson

用Redisson实现分布式锁,是目前最流行的分布式锁解决方案

**官方概念:**Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

Redisson的基础概念和使用

Netty 框架:Redisson采用了基于NIO的Netty框架,不仅能作为Redis底层驱动客户端,具备提供对Redis各种组态形式的连接功能,对Redis命令能以同步发送、异步形式发送异步流形式发送管道形式发送的功能,LUA脚本执行处理,以及处理返回结果的功能

基础数据结构:将原生的Redis HashListSetStringGeoHyperLogLog等数据结构封装为Java里大家最熟悉的映射(Map)列表(List)集(Set)通用对象桶(Object Bucket)地理空间对象桶(Geospatial Bucket)基数估计算法(HyperLogLog)等结构。

分布式数据结构:这基础上还提供了分布式的多值映射(Multimap)本地缓存映射(LocalCachedMap)有序集(SortedSet)计分排序集(ScoredSortedSet)字典排序集(LexSortedSet)列队(Queue)阻塞队列(Blocking Queue)有界阻塞列队(Bounded Blocking Queue)双端队列(Deque)阻塞双端列队(Blocking Deque)阻塞公平列队(Blocking Fair Queue)延迟列队(Delayed Queue)布隆过滤器(Bloom Filter)原子整长形(AtomicLong)原子双精度浮点数(AtomicDouble)BitSet等Redis原本没有的分布式数据结构。

分布式锁:Redisson还实现了Redis文档中提到像分布式锁Lock这样的更高阶应用场景。事实上Redisson并没有不止步于此,在分布式锁的基础上还提供了联锁(MultiLock)读写锁(ReadWriteLock)公平锁(Fair Lock)红锁(RedLock)信号量(Semaphore)可过期性信号量(PermitExpirableSemaphore)闭锁(CountDownLatch)这些实际当中对多线程高并发应用至关重要的基本部件。正是通过实现基于Redis的高阶应用方案,使Redisson成为构建分布式系统的重要工具。

节点:Redisson作为独立节点可以用于独立执行其他节点发布到分布式执行服务分布式调度服务里的远程任务。

  1. 引入Maven

            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.6.0</version>
            </dependency>
    
  2. 定义配置类

    @Configuration
    public class RedissonConfig 
        @Value("$spring.redis.host")
        private String host;
    
        @Value("$spring.redis.port")
        private String port;
    
        @Value("$spring.redis.password")
        private String password;
    
        /**
         * RedissonClient,单机模式
         */
        @Bean(destroyMethod = "shutdown")
        public RedissonClient redisson() throws IOException 
            Config config = new Config();
            config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
            return Redisson.create(config);
        
    
    
  3. 测试配置类

        @Autowired
        RedissonClient redissonClient;
    
        @Test
        void contextLoads() 
            System.out.println(redissonClient);
        
    
    输出信息:
    org.redisson.Redisson@25bc65ab
    

Redisson分布式锁

Redisson分布式锁都与JUC采用了相似的接口和用法。

可重入锁(Reentrant Lock)

基于Redis的Redisson分布式可重入锁RLockJava 对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

案例:

// 1.设置分布式锁
RLock lock = redisson.getLock("WZ_LOCK");

//2.获取锁
lock.lock();

//3.执行业务逻辑
....
    
//4.释放锁
lock.unlock();    



//另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
//还可以通过 trylock()方法自定义获取锁,类似juc中的trylock()方法。

可重入锁的两点思考:

  • 多个线程抢占锁,后面的锁是否需要等待?

    # Redisson 的可重入锁(lock)是阻塞其他线程的,需要等待其他线程释放的。
    
  • 如果抢占到锁的服务停了,锁会不会自动释放?

    # 会释放,默认看门狗检查锁的超时时间是30s。    Redisson看门狗原理
    

看门狗原理

一个线程获取到Redis锁后,锁的有效时间是多少??

如果这个服务挂掉了,锁会自动释放吗??

模拟业务执行时间为60s,观察redis中WZ_LOCK键的超时时间:

 // 1.获取锁,只要锁的名字一样,获取到的锁就是同一把锁。
        RLock lock = redisson.getLock("WZ_LOCK");

        try 
            // 2.加锁
            lock.lock();
            System.out.println("加锁成功,执行后续代码。线程 ID:" + Thread.currentThread().getId());
            Thread.sleep(60000);
         catch (Exception e) 
            //TODO
         finally 
            // 3.解锁
            lock.unlock();
            System.out.println("Finally,释放锁成功。线程 ID:" + Thread.currentThread().getId());
        

现象:

超时时间默认为30s,当时间降为20s的时候超时时间又被设置到了30s。

如果中途服务挂掉了,那么超时时间到20s后也不会重新设置,而是继续变小,知道过期删除key

看门狗原理:

如果负责储存这个分布式锁的 Redisson 节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。

默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

如果我们未指定 lock 的超时时间,就使用 30 秒作为看门狗的默认时间。只要占锁成功,就会启动一个定时任务:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。

当服务器宕机后,因为锁的有效期是 30 秒,所以会在 30 秒内自动解锁。(30秒等于宕机之前的锁占用时间+后续锁占用的时间)。

图片引自:王者方案 - 分布式锁 Redisson - 掘金 (juejin.cn)

公平锁(Fair Lock)

它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

有看门狗、支持指定加锁时间。

分布式读写锁(ReadWriteLock)

类似于JUC中的读写锁,写写互斥,其中读锁和写锁都继承了RLock接口。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

有看门狗、支持指定加锁时间。

信号量(Semaphore)

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

关于信号量的使用大家可以想象一下这个场景,有三个停车位,当三个停车位满了后,其他车就不停了。可以把车位比作信号,现在有三个信号,停一次车,用掉一个信号,车离开就是释放一个信号。

我们用 Redisson 来演示上述停车位的场景。

先定义一个占用停车位的方法:

/**
* 停车,占用停车位
* 总共 3 个车位
*/
@ResponseBody
@RequestMapping("park")
public String park() throws InterruptedException 
  // 获取信号量(停车场)
  RSemaphore park = redisson.getSemaphore("park");
  // 获取一个信号(停车位)
  park.acquire();

  return "OK";

复制代码

再定义一个离开车位的方法:

/**
 * 释放车位
 * 总共 3 个车位
 */
@ResponseBody
@RequestMapping("leave")
public String leave() throws InterruptedException 
    // 获取信号量(停车场)
    RSemaphore park = redisson.getSemaphore("park");
    // 释放一个信号(停车位)
    park.release();

    return "OK";

在redis中创建 key park ,value 设置为3,发起请求观察,发现每次获取信号量,值减一,当value为0时,再发起请求获取停车位,那么接口就会阻塞。

当发起释放停车位接口时:

刚才阻塞的请求可以获取到车位。如果想要不阻塞,可以用 tryAcquire 或 tryAcquireAsync。

注意多次执行释放信号量操作,剩余信号量会一直增加,而不是到 3 后就封顶了。

联锁(MultiLock)

基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();

有看门狗、支持指定加锁时间。

适用场景:某个业务与多个业务互斥,并且都需要锁。

红锁(RedLock)

与联锁类似,红锁是如果从大于等于n/2+1个实例获取锁成功,则获取分布式所就成功。

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

有看门狗、支持指定加锁时间。

项目应用方案

AOP+Redisson

项目中许多需要用到分布式锁的地方,防止并发修改对数据一致性的影响。获取分布式锁的过程有可以抽象为几个步骤,发现步骤基本相似,所以可以采用AOP的方式,自定义注解来设置某个方法是否需要获取分布式锁。

根据业务场景复杂度,以及注解的功能复杂度,实现方案也不同。

开源项目方案:

这里举例用AOP实现一个最简单的单机分布式锁。

  1. 定义注解

    /**
     * RedissonLock
     * @author ZeWang2
     */
    
    @Target(ElementType.METHOD,ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    public @interface RedissonLock 
    
        //key值,锁名称
        String lockName();
    
    
    
  2. 定义AOP切面

    /**
     * @ClassName: RedissonAop
     * @Author: Ze WANG
     * @Date: 2022/8/22
     **/
    
    @Order(0)
    @Aspect
    @Component
    @Slf4j
    public class RedissonAop 
        @Resource
        private RedissonClient redisson;
    
        @Around("@annotation(redissonLock)")
        public Object setRedissonLock(ProceedingJoinPoint joinPoint,RedissonLock redissonLock) throws Throwable 
            //获取锁
            String lockName = redissonLock.lockName();
            RLock rLock = redisson.getLock(lockName);
            log.info(Thread.currentThread().getName()+"===尝试获取分布式锁===",lockName);
            //加锁
            boolean tryLock = rLock.tryLock();
    
            //执行业务
            if(!tryLock)
                throw new IllegalAccessError("当前线程获取分布式锁失败");
            
            log.info(Thread.currentThread().getName()+"===获取分布式锁成功===");
            Object ret = joinPoint.proceed();
            //释放锁
            rLock.unlock();
            log.info(Thread.currentThread().getName()+"===释放分布式锁===",lockName);
            return ret;
    
        
    
    
    
  3. 使用注解

    @Service
    public class TestAop 
        
        @RedissonLock(lockName = "WZ_LOCK")
        public void testA()
            System.out.println(Thread.currentThread().getName()+"业务代码执行中.....");
            try 
                Thread.sleep(30000);
             catch (InterruptedException e) 
                throw new RuntimeException(e);
            
            System.out.println(Thread.currentThread().getName()+"业务代码执行结束....");
        
    
    
        @Autowired
        private TestAop testAop;
    
        @GetMapping("trylock")
        public  void  testAop()
            testAop.testA();
        
    
  4. 测试

    首先发起一个Http请求:

    可以发现,当前exex-3获取到了分布式锁,正在执行业务…

    再发起一个http请求:

    exex-2获取分布式锁失败,抛出异常。达到了预期效果。

用不用RedLock?

参考文章:

Redis Redlock 的争论

Redisson 分布式锁源码 09:RedLock 红锁的故事 - 掘金 (juejin.cn)

官方文档中,RedLock已经被弃用:

以上是关于Redisson实现分布式锁从入门到应用的主要内容,如果未能解决你的问题,请参考以下文章

Redis实战——Redisson分布式锁

停车场反向寻车实现方法和算法

Redisson入门教程

高并发环境下,Redisson实现redis分布式锁

Redisson 实现分布式锁

Springboot基于Redisson实现Redis分布式可重入锁案例到源码分析