原理+实战全面探索分布式锁之强大的Redisson【建议收藏】

目录

一、Redisson概述

什么是Redisson

Redission是一个基于Redis实现的Java分布式对象存储和缓存框架。它提供了丰富的分布式数据结构和服务。例如:分布式锁、分布式队列、分布式Rate Limiter等。

Redisson和Jedis、Lettuce、Spring Data Redis的区别

Redis是一个高性能的键值存储数据库,它支持多种数据结构。在Java生态中,与Redis交互的客户端和库有很多,其中Lettuce、Jedis、Redisson和Spring Data Redis最为常用。这些工具之间有各自的特点、优势以及适合的使用场景,而且它们可以相互协作或独立使用,以满足不同的业务需求。

  • Lettuce:一个高性能的Redis客户端。基于Netty实现,并且提供了非阻塞和事件驱动的API;Lettuce客户端完全是线程安全的,因此可以在多个线程间共享同一个连接实例。Lettuce的连接是基于Netty的连接实例,它支持多路复用,即多个命令可以在同一TCP连接上并行执行。
    由于它的异步能力,Lettuce非常适合需要处理大量并发请求的应用程序,例如微服务架构和响应式编程模型。此外,Lettuce还支持集群、Sentinel、管道和事务等高级功能。
  • Jedis:相对于Lettuce,Jedis是一个更加轻量级和直接的Redis客户端。提供了简便的方法来与Redis进行交互。Jedis主要关注于同步的命令执行方式。由于Jedis不是线程安全的,因此通常推荐在多线程环境下通过连接池来使用Jedis。
    虽然Jedis没有内置的异步支持,但它的简单性让它在小型或者中等规模的系统中非常受欢迎,并且它的直接性也使得它在性能上表现出色。
  • Redisson:一个在JedisLettuce之上构建的Redis客户端。提供了一系列分布式Java对象和服务,比如:分布式锁、原子变量、计数器等。Redisson意在通过高层次的抽象使得开发者能够更容易地利用Redis提供的各种功能。
    Redisson通过封装底层的Redis命令,使得在Java代码中操作分布式数据结构就像操作本地数据结构一样自然。如果你的应用程序需要分布式数据类型或者锁,Redisson可能是最佳选择。
  • Spring Data Redis:Spring提供的对Redis的高级抽象,它旨在简化Redis的数据访问并与Spring框架无缝集成。Spring Data Redis支持Lettuce和Jedis作为其底层连接库,并为开发者提供了一致的操作接口,比如RedisTemplate和各种Repository支持。
    Spring Data Redis允许开发者通过声明式的方式来定义交云与Redis的交互,从而避免了冗余的样板代码,并且可以非常方便地与Spring的其他项目(如Spring Cache、Spring Session)整合。

二、基本使用

基础环境搭建

为了便于进行相关代码测试,下面简单的搭建了一个Spring Boot项目,并集成了Redisson

Spring Boot版本:3.2.1

JDK:21

创建一个Spring Boot 项目

项目基本结构:

项目基本结构图

引入相关依赖

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.15.6</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.16</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建配置类连接Redis

创建Redisson配置类,配置redis地址、密码登信息,连接Redis。

RedissonConfig.java
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     *
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        // 创建配置 指定redis地址及节点信息
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(1).setPassword("123456");

        // 根据config创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

分别启动Redis服务以及项目

启动Redis服务

启动项目

到此,项目启动成功,基本环境搭建完毕。

Redisson提供的主要功能

分布式对象:

  • 分布式集合(Set、SortedSet、List)
  • 分布式映射(Map)
  • 分布式队列(Queue、Deque)
  • 分布式锁(Lock)
  • 分布式计数器(AtomicLong)

分布式限流:

  • 令牌桶算法(Rate Limiter)
  • 漏桶算法(Rate Limiter)

分布式发布订阅:

  • 发布订阅模式(Pub-Sub)
  • 消息监听器容器(Message Listener Container)

分布式锁和同步:

  • 可重入锁(ReentrantLock)
  • 公平锁(FairLock)
  • 联锁(MultiLock)
  • 红锁(RedLock)
  • 读写锁(ReadWriteLock)
  • 信号量(Semaphore)
  • 闭锁(CountDownLatch)
  • 栅栏(CyclicBarrier)

分布式服务和任务调度:

  • 远程服务(Remote Service)
  • 分布式任务调度器(Task Scheduler)
  • 分布式延迟队列(Delayed Queue)

分布式地理空间索引(Geospatial Index):

  • 地理位置存储
  • 地理位置搜索

分布式布隆过滤器(Bloom Filter)和可布隆过滤器(Bloom Filter)。

分布式缓存:

  • 对Redis进行本地缓存
  • Spring缓存注解支持

分布式连接池:

  • 支持连接池管理和维护

Redis集群和哨兵支持:

  • 支持Redis集群模式
  • 支持Redis哨兵模式
  • 对于使用Redis集群部署的场景,Redisson可以自动识别和操作集群中的多个节点,保证数据的高可用性和扩展性。而对于使用Redis哨兵模式部署的场景,Redisson可以监控并切换到可用的主从节点,实现高可靠性和容错能力。

Spring集成:

  • 与Spring框架的无缝集成
  • 支持Spring缓存注解

Redisson基本操作

下面演示了Redisson的常用操作:

package com.xxkfz.controller;


import cn.hutool.core.lang.UUID;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import com.xxkfz.message.TopicMsg;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @program: xxkfz-admin-redisson
 * @ClassName BaseController.java
 * @author: 公众号:小小开发者
 * @create: 2024-01-19 09:58
 * @description: Redisson基本操作
 * @Version 1.0
 **/
@RestController
@RequestMapping(value = "/base")
@Slf4j
public class BaseController {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 通用对象桶,我们用来存放任类型的对象
     * Redisson将Redis中的字符串数据结构封装成了RBucket,
     * 通过RedissonClient的getBucket(key)方法获取一个RBucket对象实例,
     * 通过这个实例可以设置value或设置value和有效期。
     * 测试:http://127.0.0.1:8080/base/redissonBucket?key=xk&value=xxkfz
     *
     * @return
     */
    @GetMapping("redissonBucket")
    public String redissonBucket(String key, String value) {
        RBucket<String> rBucket = redissonClient.getBucket(key);
        rBucket.set(value, 30, TimeUnit.SECONDS);
        return redissonClient.getBucket(key).get().toString();
    }


    /**
     * Redisson操作List
     * 测试:http://127.0.0.1:8080/base/redissonList?listKey=xxkfz_key_list
     */
    @GetMapping("redissonList")
    public void redissonList(String listKey) {
        RList<String> list = redissonClient.getList(listKey);
        // 使用add方法向List中添加元素
        list.add("公众号: 小小开发者-list1");
        list.add("公众号: 小小开发者-list2");
        list.add("公众号: 小小开发者-list3");
        // 获取List中的元素
        String s = list.get(0);
        System.out.println("s = " + s);
        // 获取列表长度
        int size = list.size();
        System.out.println("size = " + size);
        Object object = redissonClient.getList(listKey).get(1);
        System.out.println("object = " + object);
    }

    /**
     * Redisson操作Set
     * 测试:http://127.0.0.1:8080/base/redissonSet?listKey=xxkfz_key_set
     *
     * @param setKey
     */
    @GetMapping("redissonSet")
    public void redissonSet(String setKey) {
        RSet<Object> set = redissonClient.getSet(setKey);
        set.add("公众号: 小小开发者-set1");
        set.add("公众号: 小小开发者-set2");
        System.out.println(set);
        //通过key取value值
        RSet<Object> setValue = redissonClient.getSet(setKey);
        System.out.println("setValue = " + setValue);
    }


    /**
     * Redisson操作map
     * 测试:http://127.0.0.1:8080/base/redissonMap?mapKey=xxkfz_key_map
     *
     * @param mapKey
     */
    @GetMapping("redissonMap")
    public void redissonMap(String mapKey) {
        // 创建Map对象
        RMap<String, String> map = redissonClient.getMap(mapKey);
        // 添加键值对
        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("key3", "value3");
        // 获取值
        String value = map.get("key1");
        System.out.println(value);
        // 删除键值对
        String removedValue = map.remove("key2");
        System.out.println(removedValue);
        // 获取Map大小
        int size = map.size();
        System.out.println(size);
    }

    /**
     * Redisson操作Queue
     * 测试:http://127.0.0.1:8080/base/redissonQueue?queueKey=xxkfz_key_queue
     *
     * @param queueKey
     */
    @GetMapping("redissonQueue")
    public void redissonQueue(String queueKey) {
        RQueue<String> rQueue = redissonClient.getQueue(queueKey);
        // 向队列中添加值
        rQueue.add("公众号: 小小开发者-queue1");
        rQueue.add("公众号: 小小开发者-queue2");
        // 取值
        String value = rQueue.poll();
        System.out.println("value = " + value);

        //
        RQueue<Object> queueValue = redissonClient.getQueue(queueKey);
        System.out.println("queueValue = " + queueValue);
    }


    /**
     * Redisson消息发布订阅操作
     * 消息监听器:详见TopicListener.java
     * 测试:http://127.0.0.1:8080/base/redissonTopic?topicKey=xxkfz_key_topic
     *
     * @param topicKey
     */
    @GetMapping("redissonTopic")
    public String redissonTopic(String topicKey) {
        RTopic rTopic = redissonClient.getTopic(topicKey);
        String msgId = UUID.fastUUID().toString();
        long victory = rTopic.publish(new TopicMsg(msgId, "消息:我是小小开发者"));
        return StrUtil.toString(victory);
    }

    /**
     * Redisson的可重入锁操作
     * 测试:http://127.0.0.1:8080/base/redissonLock?lockKey=xxkfz_key_lock
     *
     * @param lockKey
     * @return
     */
    @GetMapping("redissonLock")
    public String redissonLock(String lockKey) {
        RLock rLock = redissonClient.getLock(lockKey);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                rLock.lock();
                try {
                    System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-" + "获取了锁");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    rLock.unlock();
                }
            }).start();
        }
        return "success";
    }

    /**
     * Redisson实现限流
     * 具体可以查看接口的限流应用
     * <p>
     * <p>
     * 测试:http://127.0.0.1:8080/base/redissonRateLimiter?rateKey=xxkfz_key_rate
     *
     * @param rateKey
     */
    @GetMapping("redissonRateLimiter")
    public String redissonRateLimiter(String rateKey) {
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(rateKey);
        //创建限流器,最大流速:每10秒钟产生8个令牌
        rateLimiter.trySetRate(RateType.OVERALL, 8, 10, RateIntervalUnit.SECONDS);
        String availCount = "-1";
        if (rateLimiter.tryAcquire()) {
            availCount = StrUtil.toString(rateLimiter.availablePermits());
        }
        return availCount;
    }


    /**
     * Redisson操作布隆过滤器
     * 解决方案:缓存穿透
     * 测试:http://127.0.0.1:8080/base/redissonBloomFilter?bloomKey=xxkfz_key_bloom
     *
     * @param bloomKey
     */
    @GetMapping("redissonBloomFilter")
    public void redissonBloomFilter(String bloomKey) {
        RBloomFilter<String> rBloomFilter = redissonClient.getBloomFilter(bloomKey);
        // 初始化布隆过滤器,初始化预期插入的数据量为200,期望误差率为0.01
        rBloomFilter.tryInit(200, 0.01);
        // 插入数据
        rBloomFilter.add("小小开发者");
        rBloomFilter.add("开发者小小");
        rBloomFilter.add("开发");
        // 判断是否存在
        boolean victory = rBloomFilter.contains("开发者小小");
        boolean forward = rBloomFilter.contains("小小程序");
        System.out.println(victory); //true
        System.out.println(forward); //false
    }


    /**
     * 分布式锁案详解
     */
    @GetMapping("redissonLockTest")
    public void redissonLockTest(String lock) {
        RLock rLock = redissonClient.getLock(lock);
        // 获取锁,并设置锁的自动释放时间。
        try {
            //waitTime:等待获取锁的最大时间量; leaseTime:锁的自动释放时间; unit:时间单位。
            boolean tryLock = rLock.tryLock(2, 3, TimeUnit.SECONDS);
            if (tryLock) {
                // 模拟执行业务逻辑
                log.error("开始执行业务逻辑......");
                ThreadUtil.sleep(5000);
                log.error("业务逻辑执行完成......");
            } else {
                log.error("锁已存在......");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
                log.error("释放锁完成");
            } else {
                log.error("锁已过期......");
            }
        }

    }
}

三、详解Redisson分布式锁

为什么要使用分布式锁

现在很少系统是单体架构了,基本上都是部署在多台服务器上,也就是分布式部署,那么在分布式环境中,必须要解决的一个问题就是:数据一致性的问题。

在某个资源在多个系统之间共享的时候,如何保证只有一个客户端对其进行处理,不能并发地执行,否则一个客户端写一个客户端读,就会造成数据一致性的问题。

在分布式系统中,传统线程之间的锁机制,如synchronized等就没作用了,系统会有多份并且部署在不同的服务器中,这些资源已经不是在线程之间共享了,而是属于进程(服务器)之间共享的资源。

因此,为了解决这个互斥访问的问题,我们就需要引入分布式锁。

分布式锁

分布式锁:指的是在分布式部署环境下,通过加锁来控制共享资源在多个客户端之间的互斥访问,即同一时刻只能有一个客户端对共享资源进行操作,保证数据一致性。

特点

  • 互斥性:在任意时刻,只有一个线程能够持有锁;
  • 线程安全:分布式锁可以确保在多线程和多进程环境下的数据一致性和可靠性;
  • 可重入性:同一个线程可以多次获取同一个锁,避免死锁的问题;
  • 锁超时:支持设置锁的有效期,防止锁被长时间占用而导致系统出现问题;
  • 阻塞式获取锁:当某个线程尝试获取锁时,如果锁已经被其他线程占用,则该线程可以选择等待直到锁释放;
  • 无阻塞式获取锁:当某个线程尝试获取锁时,如果锁已经被其他线程占用,则该线程不会等待,而是立即返回获取锁失败的信息。

特性

  • 高性能:Redission是基于Redis的,因此它继承了Redis的高性能和低延迟的特性。同时,它采用了Netty的NIO框架,能够并发地处理大量的请求,使得应用程序的响应速度得到了极大的提升。
  • 易用性:Redission提供了丰富的API和方法,同时还提供了文档和示例,让开发者易于上手和使用。此外,它支持自动配置和灵活的配置方式,使得开发者可以根据自己的需求进行配置和调整。
  • 可扩展性:Redission的分布式架构使得它支持水平扩展,可以将数据和请求分散到更多的节点上进行处理。这也使得它具备了更好的容错能力和可靠性。

源码分析及原理详解

使用Redisson实现分布式锁的操作三部曲:

1、获取锁。

RLock rLock = redissonClient.getLock(lockKey);

2、加锁。

关于加锁,提供了下面一系列的方法。

3、释放锁。

rLock.unlock();

RLock接口

RLock接口继承了Lock接口,以及RLockAsync接口;它是Redisson提供的用于分布式锁的核心接口,它定义了获取锁和释放锁等方法 ,并扩展了很多方法。

方法解析

方法参数返回值功能
void lock(long leaseTime, TimeUnit unit);leaseTime:锁的自动释放时间;unit:时间单位——获取锁,并设置锁的自动释放时间。
RFuture lockAsync(long leaseTime, TimeUnit unit);leaseTime:锁的自动释放时间;unit:时间单位返回RFuture对象,表示异步操作的结果。异步方式获取锁,并设置锁的自动释放时间。
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;waitTime:等待获取锁的最大时间量;leaseTime:锁的自动释放时间;unit:时间单位在等待时间内成功获取锁,则返回true;否则返回false。尝试在指定的等待时间内获取锁,并设置锁的自动释放时间。

注:除了以上的及方法外,RLock接口还提供了其他方法来支持其他类型的锁:比如:可重入锁、公平锁、联锁、红锁、读写锁、闭锁等特性,以便满足更为复杂的分布式锁需求场景。

源码分析

加锁流程

lock()方法

1、创建锁对象。

RLock rLock = redissonClient.getLock(lock);

进入父类构造方法RedissonBaseLock:

  • name:锁的名称;
  • id :随机序列号;
  • pubsub:锁订阅;
  • entryName:随机序列号+锁名称;

2、加锁,下面以lock方法为例进行分析;

rLock.lock();

到这里我们看到waitTime参数为-1、由第一步得知leaseTime参数也为-1;接着进入方法tryAcquire方法;

解析: tryAcquire方法:执行lua脚本并且根据返回的结果ttl判断获取锁是否成功;

接下来,如果获取锁成功并且leaseTime(锁释放时间)为-1则开启看门狗,刷新锁的过期时间防止锁过期失效。

最后进入tryAcquireAsync方法:

调用tryLockInnerAsync方法,如果获取锁失败,返回的结果是这个key的剩余有效期,如果获取锁成功,返回null。

Redisson获取锁的实现是通过lua脚本来实现的!

// 判断rediskey是否存在,如果不存在则表示锁没有被其它线程获取
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 创建命名为order的hash数据,并且把线程id作为key,1作为value存入hash中
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 重置redis过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
// 返回nil在java中就是null
"return nil; " +
"end; " +
// 到这一步了则表示锁已经被获取了接下来判断获取锁的线程是否是当前线程
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 如果获取锁成功,代表获取锁次数的value+1
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 重置redisKey的有效期
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 到了这一步则表示获取锁已经失败了,最后返回redisKey有效期的剩余时间
"return redis.call('pttl', KEYS[1]);"

获取锁成功后,ttlRemaining==null成立,由上得知leaseTime = -1,执行scheduleExpirationRenewal(threadId); 方法来启动看门狗机制。

关于ExpirationEntry类,一个锁就对应自己的一个ExpirationEntry类,那么EXPIRATION_RENEWAL_MAP存放的是所有的锁信息;

根据锁的名称从EXPIRATION_RENEWAL_MAP里面获取锁,如果存在这把锁则存入;如果不存在,则将这个新锁放置进EXPIRATION_RENEWAL_MAP,并且开启看门狗机制。

第一次获取锁oldEntry==null,进入上面else逻辑,进入renewExpiration方法:

关于renewExpiration方法:

首先,从EXPIRATION_RENEWAL_MAP中获取这个锁,接下来定义一个延迟任务task,这个任务的步骤如下:

  • 新创建了一个子线程去反复调用。
  • 从EXPIRATION_RENEWAL_MAP中获取这把锁,如果这把锁不存在了,说明被删除了,不在需要续期了。
  • 从锁中获取获得这把锁的线程threadId。
  • 调用renewExpirationAsync方法刷新最长等待时间。
  • 如果刷新成功,则进来递归调用这个函数renewExpiration()。
  • 这个延迟任务task,定时操作为锁超时时间/3取的,30过期,定时规则为10秒执行一次。

回到lock方法逻辑:

如果加锁成功ttl则返回null,直接返回,加锁流程结束;

如果加锁失败了,这里返回的ttl为过期时间,则会执行下面的逻辑。

源码解析:

 private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        // 1、获取锁,加锁成功:ttl为null; 加锁失败:返回的ttl为过期时间
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId); 
         // 2、表示加锁成功
        if (ttl == null) {
            return;
        }
        // 3、此时,表示加锁失败了 异步订阅当前key, threadId只有公平锁时候才有用
    
        RFuture<RedissonLockEntry> future = subscribe(threadId); 
        if (interruptibly) { //是否支持中断 下面同步执行订阅(其实是有个默认的订阅时间, 超时就会报错, 防止异常或者太久卡死在这)
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }
       //到这里, 说明key被释放了 , 可以抢锁了
        try {
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId); // 还是调用之前的方法, 抢锁
                // lock acquired
                if (ttl == null) {  // 成功, 那就中断跳出去
                    break;
                }

                // waiting for message
                if (ttl >= 0) {  // 被别人抢走了
                    try {
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else { //<0   //在Redis 2.6和之前版本,如果key不存在或者key存在且无过期时间将返回-1。
                   //  从 Redis 2.8开始,错误返回值发送了如下变化:
                  //  如果key不存在返回-2
                 //     如果key存在且无过期时间返回-1
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {  //没抢到锁 ,就一直在while true里面轮询
            // 取消订阅
            unsubscribe(future, threadId);  
        }
//        get(lockAsync(leaseTime, unit));
    }

加锁流程小结:

  • 当前线程,调用tryAcquire方法执行LUA脚本进行加锁;若没有知道锁生效的时间,设置超时时间为30秒。
    • 获取锁成功(返回的ttl == null), 直接返回;
    • 获取锁失败(返回的ttl为过期时间) 则进行如下处理:
      • 订阅当前key,并阻塞, 直到锁被释放。
      • while(true)循环, 再尝试获取锁, 如果获取成功, 跳出循环直接返回。
      • 如果获取失败, 那么继续阻塞, 等待锁释放。并重复上一步操作
      • 跳出循环后,取消订阅。

详细流程图如下:

image

解锁流程

unlock()方法
    @Override
    public void unlock() {
        try {
            get(unlockAsync(Thread.currentThread().getId()));
        } catch (RedisException e) {
            if (e.getCause() instanceof IllegalMonitorStateException) {
                throw (IllegalMonitorStateException) e.getCause();
            } else {
                throw e;
            }
        }
        
//        Future<Void> future = unlockAsync();
//        future.awaitUninterruptibly();
//        if (future.isSuccess()) {
//            return;
//        }
//        if (future.cause() instanceof IllegalMonitorStateException) {
//            throw (IllegalMonitorStateException)future.cause();
//        }
//        throw commandExecutor.convertException(future);
    }

进入unlockAsync方法:

@Override
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<>();
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
        cancelExpirationRenewal(threadId);

        if (e != null) {
            result.tryFailure(e);
            return;
        }

        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }

        result.trySuccess(null);
    });

    return result;
}

进入unlockInnerAsync方法执行LUA解锁脚本:

    /**
     * 解锁
     * @param threadId 当前线程id
     * @return
     * null: 当前线程没有锁;0: 当前线程还持有重入锁; 1: 释放完毕
     */
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + // 如果当前线程没持有锁, 或者锁过期了,返回null
                        "return nil;" +
                        "end; " +
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + //走到这,确定当前线程持有锁, 对锁减一, (可重入)
                        "if (counter > 0) then " + //  如果还持有锁, 没释放完
                        "redis.call('pexpire', KEYS[1], ARGV[2]); " +  //续期, 延长锁的时间到internalLockLeaseTime
                        "return 0; " + //返回0
                        "else " +
                        "redis.call('del', KEYS[1]); " +  //否则证明锁已经释放完毕, 删除锁
                        "redis.call('publish', KEYS[2], ARGV[1]); " + //推送消息 , 当前锁已经释放
                        "return 1; " + //释放成功,返回1
                        "end; " +
                        "return nil;",

                Arrays.asList(getRawName(), getChannelName()), //  KEYS[1] key   channel name (redisson_lock__channel+key)
                LockPubSub.UNLOCK_MESSAGE, //ARGV[1]  解锁消息  0
                internalLockLeaseTime,  //ARGV[2]  时间
                getLockName(threadId));  // ARGV[3] connectionid+threadid , 对应的是field
    }

注:如果当前线程没有持有锁,调用RLock.unlock()方法不会抛出异常,也不会影响到其他线程。

四、实战场景案例

使用注解方式优雅实现 Redisson 分布式锁

自定义分布式锁注解

RedissonLock.java
/**
 * @program: xxkfz-admin-redisson
 * @ClassName RedissonLock.java
 * @author: 公众号:小小开发者
 * @create: 2024-01-19 09:58
 * @description: 自定义实现分布式锁注解
 * @Version 1.0
 **/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RedissonLock {

    /**
     * 分布式锁的 key,必须保持唯一性,支持 spring el表达式
     *
     * @return
     */
    String lockKey() default "";

    /**
     * 锁的自动释放时间
     *
     * @return
     */
    int leaseTime() default 10;

    /**
     * 时间单位,默认:秒
     *
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;


    /**
     * 错误提示信息
     *
     * @return
     */
    String errMsg() default "系统正在处理中,请稍后再试!";

    /**
     * 等待获取锁的最大时间量
     *
     * @return
     */
    int waitTime() default 1;

}

自定义切面

RedissonLockAspect.java
/**
 * @program: xxkfz-admin-redisson
 * @ClassName RedissonLockAspect.java
 * @author: 公众号:小小开发者
 * @create: 2024-01-20 16:15
 * @description:
 * @Version 1.0
 **/
@Aspect
@RequiredArgsConstructor
@Slf4j
@Component
public class RedissonLockAspect {

    private final RedissonClient redissonClient;


    /**
     * 环绕切面
     *
     * @param joinPoint
     * @param redissonLock
     * @return
     * @throws Throwable
     */
    @Around("@annotation(redissonLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
        String lockKey = this.getLockKey(joinPoint, redissonLock);
        int leaseTime = redissonLock.leaseTime();
        String errMsg = redissonLock.errMsg();
        int waitTime = redissonLock.waitTime();
        TimeUnit timeUnit = redissonLock.timeUnit();
        RLock rLock = redissonClient.getLock(lockKey);

        Object res;
        try {
            boolean tryLock = rLock.tryLock(waitTime, leaseTime, timeUnit);
            // 获取锁失败
            if (!tryLock) {
                throw new RuntimeException(errMsg);
            }
            log.error("获取到锁,lockName = {}", lockKey);
            res = joinPoint.proceed();
        } catch (Exception ex) {
            log.error("执行业务方法异常:{}", ex);
            throw ex;
        } finally {
            if (rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }

        return res;
    }

    /**
     * 解析El表达式获取lockKey
     *
     * @param joinPoint
     * @param redissonLock
     * @return
     */
    private String getLockKey(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) {
        String key = redissonLock.lockKey();
        Object[] parameterValues = joinPoint.getArgs();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String methodName = method.getName();
        DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
        String[] parameterNames = nameDiscoverer.getParameterNames(method);
        // 没有指定key
        if (StrUtil.isEmpty(key)) {
            if (parameterNames != null && parameterNames.length > 0) {
                StringBuffer sb = new StringBuffer();
                int i = 0;

                for (int len = parameterNames.length; i < len; ++i) {
                    sb.append(parameterNames[i]).append(" = ").append(parameterValues[i]);
                }

                key = sb.toString();
            } else {
                key = methodName;
            }

            return key;
        } else {
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(key);
            if (parameterNames != null && parameterNames.length != 0) {
                EvaluationContext evaluationContext = new StandardEvaluationContext();

                for (int i = 0; i < parameterNames.length; ++i) {
                    evaluationContext.setVariable(parameterNames[i], parameterValues[i]);
                }
                try {
                    Object expressionValue = expression.getValue(evaluationContext);
                    return expressionValue != null && !"".equals(expressionValue.toString()) ? expressionValue.toString() : key;
                } catch (Exception ex) {
                    return key;
                }
            } else {
                return key;
            }
        }
    }

}

测试

@RedissonLock(lockKey = "'xxkfz'+ #param1+#param2+#param3", errMsg = "不能重复执行业务逻辑")
public String execBusinessData(String param1, String param2, Integer param3) {
    log.error("开始执行业务逻辑......");
    ThreadUtil.sleep(20000);
    log.error("结束执行业务逻辑......");
    return "success";
}

连续调用两次这个方法,我们可以看到控制台有报错信息,返回结果也是没有问题的!

使用Redisson实现接口的限流功能

对接口实现限流,主要使用了Redisson提供的限流API方法;使用很简单:

  • 第一步:声明一个限流器;
 RRateLimiter rRateLimiter = redissonClient.getRateLimiter(rateLimiterKey);
  • 第二步:设置速率;举例:5秒中产生2个令牌
rRateLimiter.trySetRate(RateType.OVERALL, 2, 5, RateIntervalUnit.SECONDS);
  • 第三步:试图获取一个令牌,获取到,返回true;否则,返回false
rateLimiter.tryAcquire();

自定义限流注解

/**
 * @program: xxkfz-admin-redisson
 * @ClassName RateLimiter.java
 * @author: 公众号:小小开发者
 * @create: 2024-01-20 09:58
 * @description: 接口限流注解
 * @Version 1.0
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * 限流key,支持使用Spring el表达式来动态获取方法上的参数值
     */
    String rateKey() default "";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;

    /**
     * 提示消息
     */
    String errMsg() default "接口请求过于频繁,请稍后再试!";
}

实现限流切面

RateLimiterAspect.java
/**
 * @program: xxkfz-admin-redisson
 * @ClassName RateLimiterAspect.java
 * @author: 公众号:小小开发者
 * @create: 2024-01-20 09:58
 * @description: 接口限流切面
 * @Version 1.0
 **/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimiterAspect {

    private final RedissonClient redissonClient;

    /**
     * 定义spel表达式解析器
     */
    private final ExpressionParser parser = new SpelExpressionParser();
    /**
     * 定义spel解析模版
     */
    private final ParserContext parserContext = new TemplateParserContext();
    /**
     * 定义spel上下文对象进行解析
     */
    private final EvaluationContext context = new StandardEvaluationContext();
    /**
     * 方法参数解析器
     */
    private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();

    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        String rateLimiterKey = getRateKey(rateLimiter, point);
        log.error("rateLimiterKey == {}", rateLimiterKey);
        try {
            RateType rateType = RateType.OVERALL;
            if (rateLimiter.limitType() == LimitType.CLUSTER) {
                rateType = RateType.PER_CLIENT;
            }

            long number = -1;
            RRateLimiter rRateLimiter = redissonClient.getRateLimiter(rateLimiterKey);
            rRateLimiter.trySetRate(rateType, count, time, RateIntervalUnit.SECONDS);
            if (rRateLimiter.tryAcquire()) {
                number = rRateLimiter.availablePermits();
            }


            if (number == -1) {
                String message = rateLimiter.errMsg();
                throw new RuntimeException(message);
            }
            log.info("限制令牌 => {}, 剩余令牌 => {}, 缓存key => '{}'", count, number, rateLimiterKey);
        } catch (Exception ex) {
            throw ex;
        }
    }

    /**
     * 解析El表达式获取lockKey
     * @param rateLimiter
     * @param joinPoint
     * @return
     */
    public String getRateKey(RateLimiter rateLimiter, JoinPoint joinPoint) {
        String key = rateLimiter.rateKey();
        // 获取方法(通过方法签名来获取)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        // 判断是否是spel格式
        if (StrUtil.containsAny(key, "#")) {
            // 获取参数值
            Object[] args = joinPoint.getArgs();
            // 获取方法上参数的名称
            String[] parameterNames = pnd.getParameterNames(method);
            if (ArrayUtil.isEmpty(parameterNames)) {
                throw new RuntimeException("限流key解析异常!请联系管理员!");
            }
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }
            // 解析返回给key
            try {
                Expression expression;
                if (StrUtil.startWith(key, parserContext.getExpressionPrefix()) && StrUtil.endWith(key, parserContext.getExpressionSuffix())) {
                    expression = parser.parseExpression(key, parserContext);
                } else {
                    expression = parser.parseExpression(key);
                }
                key = expression.getValue(context, String.class) + ":";
            } catch (Exception e) {
                throw new RuntimeException("限流key解析异常!请联系管理员!");
            }
        }
        StringBuilder stringBuffer = new StringBuilder("xk-admin:rate_limit:");

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        stringBuffer.append(request.getRequestURI()).append(":");
        if (rateLimiter.limitType() == LimitType.IP) {
            // 获取请求ip
            stringBuffer.append(HttpUtils.getIpAddr(request) + ":");
        } else if (rateLimiter.limitType() == LimitType.CLUSTER) {
            // 获取客户端实例id
            stringBuffer.append(redissonClient.getId()).append(":");
        }
        return stringBuffer.append(key).toString();
    }
}

限流测试

/**
 * 限流测试
 *
 * @param param1
 * @param param2
 */
@GetMapping("rateTest")
@RateLimiter(rateKey = "'xxkfz'+ #param1+#param2", count = 2, time = 10, limitType = LimitType.DEFAULT, errMsg = "访问超过限制,请稍后再试!")
public void rateTest(String param1, String param2) {
    log.error("开始处理业务......");
    ThreadUtil.sleep(15000);
    log.error("处理业务完成......");
}

通过使用@RateLimiter 注解配置:count = 2, time = 10;即:每10秒钟产生2个令牌。

测试接口访问地址:

http://127.0.0.1:8080/rate/rateTest?param1=param1Value&param2=param2Value

连续访问该接口模拟频繁请求接口操作:

五、代码传送门

代码传送门

源码获取方式:wx关注【小小开发者】 + 私信:【Redisson分布式锁】

如果觉得本文不错,欢迎关注、点赞、收藏、转发支持,你的关注是我坚持的动力!

发布于 2024-01-27 09:47・IP 属地浙江

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值