Easy Redis Java 客户端:Redisson

Easy Redis Java 客户端:Redisson

前言

Redission 是一个基于RedisJava 框架。它提供了许多分布式的数据结构和功能,如分布式锁、MapQueueTopic 等。Redission可以帮助简化并发编程的复杂性,并提供可重入锁、公平锁等常用的分布式锁,支持异步执行、锁的自动续期、锁的等待,等特性。

Redisson 的宗旨是促进使用者对Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

你可能会有点蒙,RedissonJedisLettuce有什么区别?

总结下来,Jedis 把 Redis 命令封装的非常全面,Lettuce 则进一步丰富了 Api,支持 Redis 各种高级特性。

但是两者并没有进一步深化,只给了你操作 Redis 数据库的工具,而 Redisson 则是基于 Redis、Lua 和 Netty 建立起了一套的分布式解决方案,比如分布式锁的实现,分布式对象的操作等等。

引入官方依赖:

xml 复制代码
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.29.0</version>
</dependency>  

SpringBoot提供的依赖:

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.29.0</version>
</dependency>

配置文件

有多种方式来集成Redisson配置:编程式配置、声明式配置

声明式配置

  • 通用配置
perl 复制代码
#redis地址
spring.redis.host=127.0.0.1
#redis端口
spring.redis.port=6379
#redis密码(非必须)
spring.redis.password=123456

使用通用的 Spring Boot 2.7.x 及以下设置:

java 复制代码
spring:
  redis:
    database: 
    host:
    port:
    password:
    ssl: 
    timeout:
    connectTimeout:
    clientName:
    cluster:
      nodes:
    sentinel:
      master:
      nodes:

示例如下(yaml文件修改格式即可):

java 复制代码
#数据库编码
spring.redis.database= 0
##是否开启ssl
spring.redis.ssl=false
# 读取超时
spring.redis.timeout=5000
# 连接超时
spring.redis.connect-timeout=10000
##客户端名称
spring.redis.client-name=myClientName
##集群部署 ip:port,...
spring.redis.cluster.nodes=192.168.1.1:6379,192.168.1.2:6379,192.168.1.3:6379
##哨兵模式 主节点名称
#spring.redis.sentinel.master=mymaster
##哨兵模式 节点列表 ip:port,...
spring.redis.sentinel.nodes=192.168.1.1:7001,192.168.1.1:7002,192.168.1.1:7003
  • Redisson引入文件

另外也可以使用 Redisson 配置文件,再application. properties添加

perl 复制代码
spring.redis.redisson.file=classpath:redisson.yml

resources目录下创建redisson.yml文件

yml 复制代码
# 单节点配置
singleServerConfig:
  # 数据库编号
  database: 0
  # 节点地址
  address: redis://127.0.0.1:6379
  # 密码
  password: 123456
  • 使用竖杠符号|

竖杠符号(|)是YAML语法中的一种特殊表示方式,称为"折叠文本块"标记。它用于指示多行文本块,在保留换行符的同时,将所有行连接成单个字符串。在这种情况下,文本块的缩进会被忽略,而每行的缩进将被认为是相对于最短缩进的偏移量。这在配置文件中特别有用,因为它允许你在不失去可读性的情况下编写多行文本。

可以直接再application. properties添加(注意|格式)

yml 复制代码
  redis:
    redisson:
      config: |
        singleServerConfig:
          address: redis://127.0.0.1:6379
          password: 123456

编程式配置

perl 复制代码
#redis地址
spring.redis.host=127.0.0.1
#redis端口
spring.redis.port=6379
#redis密码(非必须)
spring.redis.password=123456

对应Java配置如下:

java 复制代码
@Configuration
public class MyConfig {

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        config.useSingleServer().setPassword("123456");
        return Redisson.create(config);
    }
}
  • 集群模式

单点故障问题主要是由于Redis 是单机运行的,该机器出现故障后,会间接导致使用Redis的程序出现错误。因此我们可以使用集群模式来避免这个问题。

yml 复制代码
---
clusterServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  failedSlaveReconnectionInterval: 3000
  failedSlaveNodeDetector: !<org.redisson.client.FailedConnectionDetector> {}
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 24
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 24
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  subscriptionMode: "SLAVE"
  nodeAddresses:
  - "redis://127.0.0.1:7004"
  - "redis://127.0.0.1:7001"
  - "redis://127.0.0.1:7000"
  scanInterval: 1000
  pingConnectionInterval: 30000
  keepAlive: false
  tcpNoDelay: true
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.Kryo5Codec> {}
transportMode: "NIO"
  • idleConnectionTimeout: 如果池连接一段时间未使用,并且当前连接数量大于最小空闲连接池大小,则将关闭并从池中删除。单位为毫秒,默认:10000。

  • connectTimeout: 连接到任何Redis服务器时超时,默认:10000。

  • timeoutRedis 服务器响应超时。Redis命令发送成功后开始倒计时。单位为毫秒,默认:3000。

  • retryAttempts: 如果在retryAttempts之后Redis 命令不能被发送到Redis服务器,将会抛出错误。但如果发送成功,则会启动超时。

  • retryInterval:在此时间间隔之后,将执行另一次发送Redis命令的尝试。单位为毫秒,默认:1500。

  • failedSlaveReconnectionIntervalRedis Slave重新连接尝试的时间间隔。单位为毫秒,默认:3000。

  • failedSlaveNodeDetector:定义失败的Redis Slave 节点检测器对象。

    (1)FailedConnectionDetector :如果Redis 节点在定义的毫秒间隔内有持续的连接错误,则标记为失败。默认值是180000毫秒。

    (2)FailedCommandsDetector :如果Redis 节点有一定数量的命令执行错误,则将其标记为失败,该错误由定义的时间间隔定义,以毫秒为单位。

    (3)FailedCommandsTimeoutDetector :如果Redis节点有一定数量的命令执行超时错误,则标记为失败,该错误由在定义的间隔中定义,以毫秒为单位。

  • passwordRedis服务器鉴权密码。

  • subscriptionsPerConnection:每个订阅连接限制的订阅。

  • clientName:客户端连接名称。

  • loadBalancer:连接为多台Redis服务器的负载均衡器。

  • subscriptionConnectionMinimumIdleSize:订阅(发布/订阅)通道的最小空闲连接池大小。

  • subscriptionConnectionPoolSize:订阅(发布/订阅)通道的最大连接池大小。

  • slaveConnectionMinimumIdleSizeRedis 'slave'节点每个从节点的最小空闲连接数

  • slaveConnectionPoolSizeRedis 'slave'节点每个slave节点最大连接池大小。

  • masterConnectionMinimumIdleSize:每个Redis主节点的最小空闲连接数。

  • masterConnectionPoolSizeRedis主节点最大连接池大小。

  • readMode:设置读操作使用的节点类型。可用值:

    (1)SLAVE -从从节点读取,如果没有SLAVE 可用,使用MASTER

    (2)MASTER -从主节点读取,

    (3)MASTER_SLAVE -从主节点和从节点读取

  • subscriptionMode:设置订阅操作使用的节点类型。可用值:

    (1)SLAVE -订阅从节点,

    (2)MASTER -订阅主节点,

  • nodeAddresses:格式中添加Redis节点地址。可以一次添加多个节点。应该定义所有节点(主节点和从节点)。

  • scanInterval:复制节点扫描间隔(毫秒)。

  • pingConnectionIntervalPING 命令发送间隔每连接到Redis。以毫秒为单位定义。

  • keepAlive:使能TCP 连接keepAlive

  • tcpNoDelay:使能TCP noDelay连接。

  • threads:线程用于执行对象的侦听器逻辑、对象和任务的调用处理程序。

  • nettyThreadsreddisson 使用的所有内部redis 客户端之间共享的线程数。Netty 线程用于Redis响应解码和命令发送。

  • codecRedis数据编解码器。读写Redis数据时使用。

  • transportMode:传输方式

    (1)TransportMode.NIO,

    (2)TransportMode.EPOLL -需要classpathnetty-transport-native-epoll中的库

    (3)TransportMode.KQUEUE - -需要classpathnetty-transport-native-kqueue中的库

转换为Java配置,部分示例代码如下:

java 复制代码
@Configuration
public class MyConfig {

    @Bean
    public RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        config.useClusterServers()
                .setScanInterval(2000) // 集群状态扫描间隔(毫秒)
                // 使用"rediss://"进行SSL连接
                .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
                .addNodeAddress("redis://127.0.0.1:7002");
        config.useClusterServers().setIdleConnectionTimeout(50000);
        config.useSingleServer().setConnectTimeout(10000);
        config.useClusterServers().setTimeout(3000);
        config.setThreads(16);
        config.setNettyThreads(32);
        config.setTransportMode(TransportMode.NIO);
        return Redisson.create(config);
    }
  • 复制模式

在复制模式下,将轮询每个节点的角色,以确定是否发生了故障转移,从而产生了新的主节点。

yml 复制代码
---
replicatedServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  failedSlaveReconnectionInterval: 3000
  failedSlaveNodeDetector: !<org.redisson.client.FailedConnectionDetector> {}
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 24
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 24
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  subscriptionMode: "SLAVE"
  nodeAddresses:
  - "redis://redishost1:2812"
  - "redis://redishost2:2815"
  - "redis://redishost3:2813"
  scanInterval: 1000
  monitorIPChanges: false
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.Kryo5Codec> {}
transportMode: "NIO"
  • monitorIPChanges:用于监控Redis 节点的IP地址变化。

转换为Java配置,部分示例代码如下:

java 复制代码
@Configuration
public class MyConfig {

    @Bean
    public RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        config.useReplicatedServers()
                .setScanInterval(2000) // 集群状态扫描间隔(毫秒)
                // 使用"rediss://"进行SSL连接
                .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
                .addNodeAddress("redis://127.0.0.1:7002");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}    
  • 单实例模式

所有的数据都存储在一个Redis实例中,不提供高可用性和故障转移机制。

yml 复制代码
---
singleServerConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  address: "redis://127.0.0.1:6379"
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 24
  connectionPoolSize: 64
  database: 0
  dnsMonitoringInterval: 5000
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.Kryo5Codec> {}
transportMode: "NIO"
  • database:用于 Redis 连接的数据库索引
  • dnsMonitoringInterval:以毫秒为单位的时间间隔检查终结点的 DNS 。应用程序必须确保 JVM DNS 缓存 TTL 足够低以支持此功能。设置为禁用:-1。

转换为Java配置,部分示例代码如下:

java 复制代码
@Configuration
public class MyConfig {
    @Bean
    public RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        config.useSingleServer().setDatabase(0);
        config.useSingleServer().setDnsMonitoringInterval(3000);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}
  • 哨兵模式

当主节点出现故障时,由Redis Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。

yml 复制代码
---
sentinelServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  failedSlaveReconnectionInterval: 3000
  failedSlaveNodeDetector: !<org.redisson.client.FailedConnectionDetector> {}
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 24
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 24
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  subscriptionMode: "SLAVE"
  sentinelAddresses:
  - "redis://127.0.0.1:26379"
  - "redis://127.0.0.1:26389"
  masterName: "mymaster"
  database: 0
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.Kryo5Codec> {}
transportMode: "NIO"
  • sentinelAddresses:以host:port格式添加Redis Sentinel节点地址。一次可以添加多个节点。
  • masterNameRedis Sentinel服务器和主变更监控任务使用的主服务器名称。

转换为Java配置,部分示例代码如下:

java 复制代码
@Configuration
public class MyConfig {

    @Bean
    public RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        config.useSentinelServers()
                .setMasterName("myMasterName")
                .setScanInterval(2000) //集群状态扫描间隔(毫秒)
                //使用"rediss://"进行SSL连接
                .addSentinelAddress("redis://127.0.0.1:26389", "redis://127.0.0.1:26379");

        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}
  • 主从模式

一主多从,一个主节点,多个从节点,其中主节点拥有读写操作权限,从节点只能拥有读权限,由此可以降低主节点的读取压力,降低损坏风险。

yml 复制代码
---
masterSlaveServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  failedSlaveReconnectionInterval: 3000
  failedSlaveNodeDetector: !<org.redisson.client.FailedConnectionDetector> {}
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 24
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 24
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  subscriptionMode: "SLAVE"
  slaveAddresses:
  - "redis://127.0.0.1:6381"
  - "redis://127.0.0.1:6380"
  masterAddress: "redis://127.0.0.1:6379"
  database: 0
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.Kryo5Codec> {}
transportMode: "NIO"
  • slaveAddresses:在host:portrediss://格式中添加Redis 从节点地址。可以一次添加多个节点。使用SSL连接协议。
  • masterAddress:在host:portrediss://格式的Redis 主节点地址。使用SSL连接协议。

转换为Java配置,部分示例代码如下:

java 复制代码
@Configuration
public class MyConfig {

    @Bean
    public RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        config.useMasterSlaveServers()
                //使用"rediss://"进行SSL连接
                .setMasterAddress("redis://127.0.0.1:6379")
                .addSlaveAddress("redis://127.0.0.1:6389", "redis://127.0.0.1:6332", "redis://127.0.0.1:6419")
                .addSlaveAddress("redis://127.0.0.1:6399");

        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

基本使用

我们再以Redisson 方式在代码中操作redis

(1)查看所有key

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/fetch")
    public void fetch(){
        RKeys rKeys = redissonClient.getKeys();
        rKeys.getKeysStream().forEach(System.out::println);
        /** Output
         *  test
         *  x
         */
    }
}

(2)判断key是否存在

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/fetch")
    public void fetch(){
        //检查提供的密钥是否存在,返回现有密钥的数量
        long exists = redissonClient.getKeys().countExists("test");
        System.out.println(exists);
        /** Output
         *  1
         */
    }
}

(3)删除key

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/fetch")
    public void fetch(){
    	//删除指定密钥,返回现有密钥的数量
        long exists = redissonClient.getKeys().delete("x");
        System.out.println(exists);
        /** Output
         *  1
         */
    }
}

(4)设置key有效期

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
    	//返回true表示设置成功,false则失败
        boolean time = redissonClient.getKeys().expire("test",10,TimeUnit.SECONDS);
        System.out.println(time);
        /** Output
         *  true
         */
    }
}

(5)获取key有效期

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        //时间单位为毫秒,如果密钥不存在,则为-2。-1表示密钥存在,但没有相关的过期时间
        long time = redissonClient.getKeys().remainTimeToLive("test");
        System.out.println(time);
        /** Output
         *  3784
         */
    }
}

(6)查询key类型

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        //一共5种类型:OBJECT(string), MAP, LIST, SET, ZSET
        RType test = redissonClient.getKeys().getType("test");
        System.out.println(test.name());
        /** Output
         *  OBJECT
         */
    }
}

(7)重命名key

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
    	//返回true表示设置成功,false则失败
        boolean test = redissonClient.getKeys().renamenx("test","test2");
        System.out.println(test);
        /** Output
         *  true
         */
    }
}

(8)移除key存活时间

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
    	//返回true表示设置清除成功,false则失败
        boolean test = redissonClient.getKeys().clearExpire("test");
        System.out.println(test);
        /** Output
         *  true
         */
    }
}
  • string

(1)设置字符串值。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        RBucket<Object> test = redissonClient.getBucket("testStr");
        //test.set("hello");
        test.set("hello",1,TimeUnit.SECONDS);
    }
}

(2)获取值。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        Object test = redissonClient.getBucket("testStr").get();
        System.out.println(test);
        /** Output
         *  hello
         */
    }
}

(3)获取字符串值的长度。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        RBucket<Object> testStr = redissonClient.getBucket("testStr");
        System.out.println(testStr.size());
        /** Output
         *  5
         */
    }
}

(4)自加、自减

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        RAtomicLong atomicLong = redissonClient.getAtomicLong("myAtomicLong");
        //设置初始值
        atomicLong.set(10);
        //自增
        long l = atomicLong.incrementAndGet();
        System.out.println(l);
        //自减
        long l1 = atomicLong.decrementAndGet();
        System.out.println(l1);
        /** Output
         *  11
         *  10
         */
    }
}
  • hash

(1)设置值,获取值。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        //设置值
        RMap<Object, Object> rMap = redissonClient.getMap("maps");
        rMap.put("one",1);
        rMap.put("two",2);
        rMap.put("three",3);
        //设置过期时间
        rMap.expire(Duration.ofMinutes(1));
        //获取值
        RMap<Object, Object> maps = redissonClient.getMap("maps");
        System.out.println(maps);
    }
}

(2)获取所有keysvalues

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        RMap<Object, Object> rMap = redissonClient.getMap("maps");
        //所有key
        Set<Object> objects = rMap.keySet();
        objects.stream().forEach(item-> System.out.println(item));
        Collection<Object> values = rMap.values();
        //所有value
        values.stream().forEach(item-> System.out.println(item));
    }
}

很多方式和Map使用基本一致。

  • list

(1)插入元素,获取元素,删除元素。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        //插入元素
        RList<Object> rList = redissonClient.getList("lists");
        rList.add("one");
        rList.add("two");
        rList.add("three");
        //获取元素
//        List<Object> lists = redissonClient.getList("lists").range(0, -1);
        List<Object> lists = redissonClient.getList("lists").readAll();
        lists.stream().forEach(item-> System.out.println(item));
        //删除元素
        redissonClient.getList("lists").remove("one");//value删除
        redissonClient.getList("lists").remove(1);//下标删除
    }
}

很多方式和List使用基本一致。

  • set

(1)插入元素,获取元素,删除元素。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        //插入元素
        RSet<Object> rSet = redissonClient.getSet("sets");
        rSet.add("one");
        rSet.add("two");
        rSet.add("three");
        //获取元素
        Set<Object> objects = redissonClient.getSet("sets").readAll();
        objects.stream().forEach(item-> System.out.println(item));
        //删除元素
        redissonClient.getSet("sets").remove("one");
    }
}

很多方式和Set使用基本一致。

  • zset

(1)添加元素,获取元素,删除元素。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        //插入元素
        RScoredSortedSet<Object> zsets = redissonClient.getScoredSortedSet("zsets");
        zsets.add(2,"two");
        zsets.add(3,"three");
        zsets.add(1,"one");
        //获取元素
        Collection<ScoredEntry<Object>> zsets1 = redissonClient.getScoredSortedSet("zsets").entryRange(0, -1);
        zsets1.stream().forEach(item-> System.out.println(item.getValue()));//正序
        Collection<ScoredEntry<Object>> zsets2 = redissonClient.getScoredSortedSet("zsets").entryRangeReversed(0, -1);
        zsets2.stream().forEach(item-> System.out.println(item.getValue()));//倒序
		//删除元素
        redissonClient.getScoredSortedSet("zsets").remove("one");
    }
}
  • 队列

Redisson 中,提供了RBlockingQueue , RDeque先进先出的队列结构。

(1)RDeque双端队列

java 复制代码
@RestController
public class MyController {

    @Autowired
    RedissonClient redissonClient;

    @GetMapping("/test")
    public void test() {
        RQueue<Object> queue = redissonClient.getQueue("test");
        //插入队列
        queue.add("one");
        queue.add("two");
        queue.add("three");
        //获取队列并删除
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        //删除指定队列
        queue.remove("three");
    }
}

(2)RBlockingQueue阻塞队列

java 复制代码
@RestController
public class MyController {

    @Autowired
    RedissonClient redissonClient;

    @GetMapping("/test")
    public void test() throws InterruptedException {
        RBlockingQueue<Object> queue = redissonClient.getBlockingQueue("test");
        //放入元素,如果队列满,当前线程将会被阻塞
        queue.put("one");
        queue.put("two");
        queue.put("three");
        //获取队列,队列中没有元素将阻塞
        System.out.println(queue.take());
    }
}
  • 布隆过滤器

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

java 复制代码
@Controller
public class MyController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test(){
        RBloomFilter rBloomFilter = redissonClient.getBloomFilter("blooms");
        // 初始化预期插入的数据量为10000和期望误差率为0.01
        rBloomFilter.tryInit(10000, 0.01);
        // 插入部分数据
        rBloomFilter.add("100");
        rBloomFilter.add("200");
        rBloomFilter.add("300");
        //设置过期时间
        rBloomFilter.expire(30, TimeUnit.SECONDS);
        // 判断是否存在
        System.out.println(rBloomFilter.contains("300"));
        System.out.println(rBloomFilter.contains("200"));
        System.out.println(rBloomFilter.contains("999"));
    }
}

除了这些,还有集群模式、主从复制、哨兵模式等等,以后有空再单独发布文章。

分布式锁

Redisson 提供了多种分布式锁的实现,其中包括可重入锁、公平锁、读写锁等,这些锁都是基于 Redis 实现的,具有高性能和可靠性。

可重入锁

可重入锁(Reentrant Lock )是一种同步机制,它允许线程在持有锁的情况下可以再次获取该锁,而不会造成死锁。具体来说,可重入锁允许一个线程在执行期间多次获得同一个锁,而不会被自己持有的锁所阻塞( Java 的内置锁(如 synchronizedReentrantLock)都是支持重入的)。

如果用以前的jedis,会导致第二次直接获取不到锁,示例代码如下:

java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/test")
    public void test() {
        String lockKey = "request_key";
        String clientId = "pod_1";
        try {
            if (!tryLock(lockKey,clientId)){
                System.out.println("访问人数太多,稍后重试");
                return;
            }
            System.out.println(Thread.currentThread().getName());
            reLock(lockKey,clientId);
        } finally {
            unLock(lockKey);
        }

    }
    /**
     * 测试锁的重入
     */
    private void reLock(String lockKey,String clientId) {
        try {
            if (!tryLock(lockKey,clientId)){
                System.out.println("访问人数太多,稍后重试");
                return;
            }
            System.out.println(Thread.currentThread().getName());
        } finally {
            unLock(lockKey);
        }
    }
    public boolean tryLock(String key, Object value) {
        return redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    public Object unLock(String key) {
        return redisTemplate.delete(key);
    }


}

执行结果如图:

如果要解决这个问题,就需要进行更多的操作,比如:lua脚本或者存储获取锁的次数,来达到可重入锁的效果。

Redisson 可重入锁实现了java.util.concurrent.locks.Lock接口,示例代码如下:

powershell 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";

    @GetMapping("/test")
    public void test() {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        try {
            if (!lock.tryLock()){
                System.out.println("访问人数太多,稍后重试");
                return;
            }
            System.out.println("当前线程:"+Thread.currentThread().getName()+",获取锁次数"+lock.getHoldCount());
            reLock();
        } finally {
            lock.unlock();
        }

    }
    /**
     * 测试锁的重入
     */
    private void reLock() {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        try {
            if (!lock.tryLock()){
                System.out.println("访问人数太多,稍后重试");
                return;
            }
            System.out.println("当前线程:"+Thread.currentThread().getName()+",获取锁次数"+lock.getHoldCount());
        } finally {
            lock.unlock();
        }
    }

}

执行结果如图:

Redis 中是通过HASH 来存储锁,可重入锁的实现通常会使用Value 值来跟踪锁的持有次数。每次成功获取锁时,Value 值会递增;每次释放锁时,Value值递减。这样设计可以确保同一个线程可以多次获取锁,而不会引发死锁或其他同步问题。


Redisson 提供了多种方法获取锁,除了tryLock()方法,下面简单来介绍他们的用法:

  • lock()方法:和tryLock()方法一样,属于Lock类。无参方法,默认30秒有效期,10秒种进行续期,会阻塞。
java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() throws InterruptedException {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        lock.lock();
    }
}

执行结果如图:

  • tryLock(long waitTime, TimeUnit unit)方法:属于Lock 类。有参方法,设置获取锁的时间,超过时间自动释放,返回false
java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() throws InterruptedException {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        //设置获取锁的等待时间5秒
        if (!lock.tryLock(5,TimeUnit.SECONDS)){
            System.out.println("访问人数太多,稍后重试2");
        }
    }
}

执行结果如图:

  • tryLock(long waitTime, long leaseTime, TimeUnit unit)方法:属于RLock类。有参方法,设置获取锁的时间,锁的释放时间,未获取锁超过时间自动释放,获取锁后在指定时间内释放。
java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() throws InterruptedException {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        //设置获取锁的等待时间5秒,锁的过期时间10秒
        if (!lock.tryLock(5,10,TimeUnit.SECONDS)){
            System.out.println("访问人数太多,稍后重试2");
        }
    }
}

执行结果如图:

  • lock(long leaseTime, TimeUnit unit)方法:属于RLock类。有参方法,设置锁的释放时间,超过时间自动释放。
java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() throws InterruptedException {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        //10秒后自动释放锁
        lock.lock(10,TimeUnit.SECONDS);
    }
}

执行结果如图:

公平锁

可重入锁是非公平锁。公平锁是一种按照请求顺序获取锁的机制,即先到先得,能够避免饥饿现象。在 Redisson 中,公平锁的实现是基于 Redis 的原子操作和监听机制来实现的。

java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() throws InterruptedException {
        RLock lock = redissonClient.getFairLock(LOCK_KEY);
        lock.lock();
    }
}

Redisson 中通过ListzSet去实现公平锁,把lua脚本复制出来整理,如图:

其中List 维护了一个等待的线程队列redisson_lock_queue:[xxx}zSet 维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},在List 存放等待线程IDzSet 中存放线程ID所对应的过期时间,通过排序来确认他们的优先级,每当上个线程执行完毕后释放锁,通知下一个线程。

读写锁

Redisson中,读写锁是一种支持读写分离的锁机制,可以实现多个线程对数据的并发读取,以及写操作的互斥访问。

  • 读锁(共享锁):多个线程可以同时获取读锁,用于并发读取共享资源,不阻塞其他读线程,但会阻塞写线程。
  • 写锁(排它锁):写锁是互斥的,即同一时刻只允许一个线程获取写锁进行写操作,阻塞其他读写线程。
java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test()  {
        RReadWriteLock lock = redissonClient.getReadWriteLock(LOCK_KEY);
        //读锁
        RLock rLock = lock.readLock();
        try {
            logger.info("获取读锁:"+Thread.currentThread().getName());
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("释放读锁:"+Thread.currentThread().getName());
        rLock.unlock();
		//写锁
        rLock = lock.writeLock();
        try {
            logger.info("获取写锁:"+Thread.currentThread().getName());
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("释放写锁:"+Thread.currentThread().getName());
        rLock.unlock();
    }
}

执行结果如图:

信号量

信号量允许定义一个计数器,该计数器表示当前可用的许可证数量。线程或客户端可以通过获取许可证来访问受信号量保护的资源,当许可证数量为0时,请求许可证的线程将被阻塞,直到有许可证可用。

java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() {
        RSemaphore lock = redissonClient.getSemaphore(LOCK_KEY);
        //设置初始许可证数量为10
        lock.trySetPermits(10);
        try {
            //获取一个信号量
            lock.acquire();
//            lock.tryAcquire();
//            lock.tryAcquireAsync();
//            lock.tryAcquire(23, TimeUnit.SECONDS);
//            lock.tryAcquireAsync(23, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //释放一个信号量
//            lock.release();
//            lock.release(10);
//            lock.releaseAsync();
//            lock.releaseAsync(10);
        }
    }
}

如果不释放信号量,则会导致阻塞,执行结果如图:

联锁

Redisson 的联锁(RedLock )是一种基于Redis 实现的分布式锁方案,专门用于处理多节点、多实例环境下的分布式锁管理需求。它通过在多个独立的Redis实例上进行协调,确保在分布式系统中的不同节点上能够安全地获取和释放锁。

java 复制代码
@Configuration
public class MyConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");
        return Redisson.create(config);
    }
    @Bean
    public RedissonClient redissonClient2(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6380").setPassword("123456");
        return Redisson.create(config);
    }
    @Bean
    public RedissonClient redissonClient3(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6381").setPassword("123456");
        return Redisson.create(config);
    }
}
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RedissonClient redissonClient2;
    @Autowired
    private RedissonClient redissonClient3;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() {
        RLock lock1 = redissonClient.getLock(LOCK_KEY);
        RLock lock2 = redissonClient2.getLock(LOCK_KEY);
        RLock lock3 = redissonClient3.getLock(LOCK_KEY);

        RLock lock = redissonClient.getMultiLock(lock1,lock2,lock3);
        try {
            //同时加锁:lock1 lock2 lock3, 联锁通过在多个节点上加锁,需要大多数节点(超过半数)成功加锁才算整体加锁成功。
            lock.tryLock();
        } finally {
            lock.unlock();
        }
    }
}

红锁

Redisson 的红锁(RedLock )是一种分布式锁方案,旨在提供跨多个Redis 节点的高可用性和安全性。它是基于Redis实现的一种锁机制,特别适用于需要高可用性和分布式环境下的锁管理需求。

java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RedissonClient redissonClient2;
    @Autowired
    private RedissonClient redissonClient3;
    
    private final static String LOCK_KEY = "myLock";
    
    @GetMapping("/test")
    public void test() {
        RLock lock1 = redissonClient.getLock(LOCK_KEY);
        RLock lock2 = redissonClient2.getLock(LOCK_KEY);
        RLock lock3 = redissonClient3.getLock(LOCK_KEY);

        RLock lock = redissonClient.getRedLock(lock1,lock2,lock3);
        try {
            // 同时加锁:lock1 lock2 lock3, 需要大部分(大多数)的锁节点成功加锁才能认为获取锁成功。例如,如果有5个Redis节点,需要至少3个节点成功加锁才算成功。
            lock.tryLock();
        } finally {
            lock.unlock();
        }
    }
}

加锁机制

通过HASH 来存储锁,key表示锁的名称,field表示持有者的线程ID,value重入次数。

基本流程如下:

底层是lua脚本,示例代码如下:

java 复制代码
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, 
                        "if (redis.call('exists', KEYS[1]) == 0) " +//锁是否存在,O 表示不存在。
                        	"then " +
                        	"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +//唯一标识的值+1
                        	"redis.call('pexpire', KEYS[1], ARGV[1]); " +//设置超时时间
                        	"return nil; " +
                        "end; " +
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) " +//判断hash结构中 fieldKey与客户端的唯一标识是否相等。相等表示当前加锁请求是锁重入。
                        	"then " +
                        	"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        	"redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        	"return nil; " +
                        "end; " +
                        "return redis.call('pttl', KEYS[1]);", //唯一标识不匹配,返回剩余过期时间
                        Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
    }
  • KEYS[1]:锁的名称。
  • ARGV[1]:锁的有效时间(单位毫秒)。
  • ARGV[2]:客户端唯一标识,线程ID。

释放锁机制

释放锁的时候要确保锁的重入次数为0,并且还需要保证不能删除别人的锁。

基本流程如下:

底层是lua脚本,示例代码如下:

java 复制代码
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) " +//判断key是否存在,0表示不存在
                        "then " +
                        "return nil;" + 
                "end; " + 
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +//字段值-1
                "if (counter > 0) " +//判断字段值-1,后大于0
                        "then " +
                        "redis.call('pexpire', KEYS[1], ARGV[2]); " +//设置过期时间
                        "return 0;" + 
               " else " +//判断字段值-1后,小于等于0
                        "redis.call('del', KEYS[1]); " +//删除key
                        "redis.call('publish', KEYS[2], ARGV[1]); " +//发布通知
                        "return 1; " +
               "end; " +
               "return nil;", Arrays.asList(this.getRawName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)});
    }
  • KEYS[1]:锁的名称。
  • KEYS[2]:消息发布的目标频道。
  • ARGV[1]:发布到频道的消息内容。
  • ARGV[2]:锁的有效时间(单位毫秒)。
  • ARGV[3]:客户端唯一标识,线程ID。

看门狗

Redisson 中有一个WatchDog机制,也就是看门狗机制,主要用于给锁续期,在上面的可重入锁的执行结果中有介绍过,每10秒钟判断锁是否释放,如果没有释放则续期30秒钟。

一般用于解决业务执行时间大于锁的过期时间,导致锁被释放的情况。

一般是通过估算业务执行时间,设置锁的过期时间,但是会有很多意想不到的情况,并不可靠;另一种方法就是开辟一个线程去监听锁释是否被释放,也就是WatchDog机制,给锁续期。

一般在不设置过期时间,也就是leaseTiem 为-1的情况下,才有WatchDog机制。

进入tryAcquireAsync()方法,判断是否设置了过期时间,没设置过期时间则leaseTime 为-1,进入else代码块。

java 复制代码
    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture ttlRemainingFuture;
        //如果指定了加锁时间,会直接去加锁
        if (leaseTime > 0L) {
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {//没有设置过期时间,默认
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
		//异步操作
        CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
            if (ttlRemaining == null) {
                if (leaseTime > 0L) {
                    this.internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                	//锁续期操作
                    this.scheduleExpirationRenewal(threadId);
                }
            }

            return ttlRemaining;
        });
        return new CompletableFutureWrapper(f);
    }

往下走进入scheduleExpirationRenewal()方法,第一次进来ExpirationEntryMap 结构存放,记录获取锁的次数)中为null ,进入else 代码块,添加线程id,然后执行renewExpiration()方法,实现刷新。

java 复制代码
    protected void scheduleExpirationRenewal(long threadId) {
        ExpirationEntry entry = new ExpirationEntry();
        ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
        	//记录锁次数
            entry.addThreadId(threadId);

            try {
            	//实现刷新
                this.renewExpiration();
            } finally {
                if (Thread.currentThread().isInterrupted()) {
                    this.cancelExpirationRenewal(threadId);
                }

            }
        }

    }
    public static class ExpirationEntry {
        private final Map<Long, Integer> threadIds = new LinkedHashMap();
        private volatile Timeout timeout;

        public ExpirationEntry() {
        }

        public synchronized void addThreadId(long threadId) {
            this.threadIds.compute(threadId, (t, counter) -> {
                counter = (Integer)Optional.ofNullable(counter).orElse(0);
                counter = counter + 1;
                return counter;
            });
        }
}

进入renewExpiration()方法,创建任务,延迟时间是过期时间的三分之一,也就是说,30/3=10s,每10秒钟刷新过期时间

java 复制代码
    private void renewExpiration() {
        ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
        	//创建延时任务
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                        	//具体续期脚本
                            CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
                            future.whenComplete((res, e) -> {
                                if (e != null) {
                                    RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
                                    RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
                                } else {
                                    if (res) {//重新续期
                                        RedissonBaseLock.this.renewExpiration();
                                    } else {
                                        RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
                                    }

                                }
                            });
                        }
                    }
                }
                //internalLockLeaseTime 的1/3时间去执行续期任务
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

通过lua脚本,重新设置有效期

java 复制代码
    protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) " +
                        "then " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1;" +
                " end;" +
                " return 0;", Collections.singletonList(this.getRawName()), this.internalLockLeaseTime, this.getLockName(threadId));
    }
  • KEYS[1]:锁的名称。
  • ARGV[1]:锁的有效时间(单位毫秒)。
  • ARGV[2]:客户端唯一标识,线程ID。

释放锁的时候,同时移除锁的续期任务

java 复制代码
    protected void cancelExpirationRenewal(Long threadId) {
    	//获取锁对象
        ExpirationEntry task = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (task != null) {
            if (threadId != null) {
                task.removeThreadId(threadId);//移除任务
            }

            if (threadId == null || task.hasNoThreads()) {
                Timeout timeout = task.getTimeout();
                if (timeout != null) {
                    timeout.cancel();//关闭时间
                }
				//删除实例
                EXPIRATION_RENEWAL_MAP.remove(this.getEntryName());
            }

        }
    }

项目案例

延迟队列

如果你的项目不想过于集成太多中间件,又想实现定期通知的效果,比如:生日祝福、未支付的订单失效、异步处理等,项目也不是过于复杂,那么使用Redisson进行延迟任务是一个不错的选择。

java 复制代码
@RestController
public class MyController {

    private static final Logger log = LoggerFactory.getLogger(MyController.class);

    @Autowired
    RedissonClient redissonClient;

    @GetMapping("/test")
    public void test() throws InterruptedException {
        RBlockingQueue<Object> queue = redissonClient.getBlockingQueue("test");
        RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(queue);
        delayedQueue.offer("hello world",5,TimeUnit.SECONDS);
        log.info("添加任务成功");

        while (true){
            Object take = queue.take();
            log.info("收到任务:{}",take);
        }
    }
}

执行结果如下

然后我们将代码封装一下,示例代码如下:

封装工具类

java 复制代码
@Component
public class RedisDelayQueueUtil {
    @Autowired
    private RedissonClient redissonClient;

    private static final Logger log = LoggerFactory.getLogger(RedisDelayQueueUtil.class);
    /**
     * 添加延迟队列
     *
     * @param value     队列值
     * @param delay     延迟时间
     * @param timeUnit  时间单位
     * @param queueCode 队列键
     * @param <T>
     */
    public <T> boolean addDelayQueue(@NonNull T value, @NonNull long delay, @NonNull TimeUnit timeUnit, @NonNull String queueCode) {
        if (StringUtils.isBlank(queueCode) || Objects.isNull(value)) {
            return false;
        }
        try {
            RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
            RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
            delayedQueue.offer(value, delay, timeUnit);
            log.info("(添加延时队列成功) 队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒");
        } catch (Exception e) {
            log.error("(添加延时队列失败) {}", e.getMessage());
            throw new RuntimeException("(添加延时队列失败)");
        }
        return true;
    }

    /**
     * 获取延迟队列
     *
     * @param queueCode
     * @param <T>
     */
    public <T> T getDelayQueue(@NonNull String queueCode) throws InterruptedException {
        if (StringUtils.isBlank(queueCode)) {
            return null;
        }
        RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueCode);
        T value = (T) blockingDeque.take();
        return value;
    }
    /**
     * 删除指定队列中的消息
     *
     * @param o 指定删除的消息对象队列值(同队列需保证唯一性)
     * @param queueCode 指定队列键
     */
    public boolean removeDelayedQueue(@NonNull Object o, @NonNull String queueCode) {
        if (StringUtils.isBlank(queueCode) || Objects.isNull(o)) {
            return false;
        }
        RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
        RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
        boolean flag = delayedQueue.remove(o);
        //delayedQueue.destroy();
        return flag;
    }
}

监听类

java 复制代码
public class DelayJob {
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
@Component
public class RedisDelayQueueMatch implements InitializingBean {
    @Autowired
    private List<RedisDelayQueueHandle> list;
    private static final Map<String,RedisDelayQueueHandle> maps = new HashMap<>();


    @Override
    public void afterPropertiesSet() throws Exception {
        list.stream().forEach(item ->{
            maps.put(item.code(),item);
        });
    }
    
    public RedisDelayQueueHandle match(String code){
        if(code == null){
            return null;
        }
        return maps.get(code);
    }
}
@Component
public class FileRedisDelayQueueRunner implements ApplicationRunner {
    private static final Logger log = LoggerFactory.getLogger(FileRedisDelayQueueRunner.class);
    @Autowired
    private RedisDelayQueueUtil redisDelayQueueUtil;

    @Autowired
    private RedisDelayQueueMatch redisDelayQueueMatch;

    ExecutorService executorService = Executors.newSingleThreadExecutor();

    @Override
    public void run(ApplicationArguments args) throws Exception {
        executorService.execute(() -> {
            while (true) {
                for (RedisDelayQueueEnum redisDelayQueueEnum : RedisDelayQueueEnum.values()) {
                    try {
                        Object delayQueue = redisDelayQueueUtil.getDelayQueue(redisDelayQueueEnum.getCode());
                        if (delayQueue != null) {
                            //通过code匹配出具体实现类
                            RedisDelayQueueHandle match = redisDelayQueueMatch.match(redisDelayQueueEnum.getCode());
                            DelayJob delayJob = new DelayJob();
                            delayJob.setMessage(delayJob.toString());
                            match.execute(delayJob);
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
    }
}

控制层代码

java 复制代码
@RestController
public class MyController {
    @Autowired
    RedisDelayQueueUtil redisDelayQueueUtil;
    @GetMapping("/test")
    public void test() throws InterruptedException {
        redisDelayQueueUtil.addDelayQueue("hello world",10,TimeUnit.SECONDS, RedisDelayQueueEnum.payment_order.getCode());
    }
}
//处理类
public interface RedisDelayQueueHandle {
    void execute(DelayJob delayJob);
    String code();
}
//订单超时的具体处理类
@Component
public class PaymentOrderQueue implements RedisDelayQueueHandle{
    private static final Logger log = LoggerFactory.getLogger(PaymentOrderQueue.class);
    @Override
    public void execute(DelayJob delayJob) {
        log.info("订单超时,任务处理");
        //todo
    }

    @Override
    public String code() {
        return RedisDelayQueueEnum.payment_order.getCode();
    }
}

执行结果如图

发布订阅

Redisson 提供了RTopic 接口来实现发布/订阅模式。以下是一个使用Redisson进行发布订阅的简单示例:

java 复制代码
@RestController
public class MyController {
    @Autowired
    MyPublisher myPublisher;
    @Autowired
    private RedissonClient redissonClient;
    @GetMapping("/publish")
    public void test() throws InterruptedException {
        //向频道发布消息
        myPublisher.publish("sendMessage","hello world");

    }
}
@Component
public class MyPublisher {
    Logger logger = LoggerFactory.getLogger(MyPublisher.class);

    @Autowired
    private RedissonClient redissonClient;

    public void publish(String name,String msg) {
        RTopic topic = redissonClient.getTopic(name);
        topic.publish(msg);
        logger.info("发送消息成功");
    }
}
@Component
public class MyListener implements MessageListener<String> {
    private static final Logger log = LoggerFactory.getLogger(MyListener.class);
    @Autowired
    private RedissonClient redissonClient;
    //开启监听
    @PostConstruct
    public void init(){
        //确保监听器已经准备好接收消息
        RTopic topic = redissonClient.getTopic("sendMessage");
        topic.addListener(String.class,this::onMessage);
    }
    @Override
    public void onMessage(CharSequence channel, String msg) {
        log.info("收到通知:{}",msg);
    }

}

执行结果如图

限流

Redisson 使用RateLimiter进行限流操作,底层使用lua脚本,限流器每秒会产生X个令牌放入令牌桶,调用接口需要去令牌桶里面拿令牌。如果令牌被其它请求拿完了,那么自然而然,当前请求就调用不到指定的接口。

示例代码如下:

java 复制代码
@RestController
public class MyController {

    private static final Logger log = LoggerFactory.getLogger(MyController.class);

    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/test")
    public void test() throws InterruptedException {
        // 2. 获取RRateLimiter对象
        RRateLimiter rateLimiter = redissonClient.getRateLimiter("myRateLimiter");
        // 3. 设置限流策略,例如每2分钟产生3个令牌
        rateLimiter.trySetRate(RateType.OVERALL, 3, 2, RateIntervalUnit.MINUTES);
        long l = rateLimiter.availablePermits();
        log.info("当前令牌桶可用数量:{}", l);
        // 4. 尝试获取许可证
//        rateLimiter.tryAcquire(1,1, TimeUnit.SECONDS)等待许可证的最长时间10毫秒,获取1个许可证
        if (rateLimiter.tryAcquire(1)) {
            // 如果获取到许可证,执行业务逻辑
            log.info("获取到许可证,执行业务逻辑");
        } else {
            // 如果未获取到许可证,执行拒绝策略
            log.info("未获取到许可证,执行拒绝策略");
        }
    }
}

我们可以看到第一次请求完成后,再请求获取不到令牌,进入拒绝策略,间隔两分钟后生产了3个令牌,再次请求,执行结果如图

我们可以打开Redis 客户端查看是如何管理的:rate令牌数,interval生产令牌桶间隔时间,type单机还是集群,示例如图所示:

{yourname}:permits使用zset存储,记录获取令牌的数据,示例如图所示:

{yourname}:value使用string存储,可以查看令牌数量,示例如图所示:

我们可以将他封装成一个注解,方便多业务场景的限流通用。示例代码如下:

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GlobalRateLimiter {
    String key();
    long rate() default 5L;
    long rateInterval() default 1L;
    RateIntervalUnit rateIntervalUnit() default RateIntervalUnit.SECONDS;
}
@Aspect
@Component
public class GlobalRateLimiterAspect {
    @Autowired
    private RedissonClient redissonClient;

    @Pointcut(value = "@annotation(com.example.mybatisstudy.annotation.GlobalRateLimiter)")
    public void pointcut(){

    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        GlobalRateLimiter annotation = method.getAnnotation(GlobalRateLimiter.class);
        String key = annotation.key();
        long rate = annotation.rate();
        long rateInterval = annotation.rateInterval();
        RateIntervalUnit rateIntervalUnit = annotation.rateIntervalUnit();

        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        if (!rateLimiter.isExists()){
            rateLimiter.trySetRate(RateType.OVERALL,rate,rateInterval,rateIntervalUnit);
            //todo 可以设置一个过期时间
        }
        if(!rateLimiter.tryAcquire(1)){
            throw new RuntimeException("请求过于频繁,请休息一下");
        }
        return point.proceed();
    }
}
@RestController
public class MyController {
    @GetMapping("/test")
    @GlobalRateLimiter(key = "test",rate = 2,rateInterval = 60)
    public ResponseEntity test() {
        return ResponseEntity.ok("成功");
    }
}

执行结果如图

分布式自增ID

分布式系统中生成自增ID 是一个常见的需求,特别是在需要保证全局唯一性和并发性的场景下。Redis 可以作为生成分布式自增ID的工具,我们可以通过计数器的实现方式:

java 复制代码
@RestController
public class MyController {
    @Autowired
    private RedissonClient redissonClient;
    
    @GetMapping("/test")
    public void test() {
        System.out.println(id("memberId"));
        System.out.println(seqNo("memberNo"));
        /** OutPut:
         *  1721645124953002
         *  20240722184524000000000000000002
         */
    }

    /**
     * 生成16位的自增id
     * String.format("%0"+len+"d",atomicLong.incrementAndGet())用于格式化整型,补零到指定长度
     * 比如:
     * String.format("%0" + 3 + "d", id); 将生成字符串 "045"。
     * @param idName 唯一key值
     * @return 时间戳+补零自增
     */
    private String id(String idName){
        RAtomicLong atomicLong = redissonClient.getAtomicLong(idName);
        return System.currentTimeMillis() + String.format("%0"+3+"d",atomicLong.incrementAndGet());
    }

    /**
     * 生成32位的自增no,
     * String.format("%0"+len+"d",atomicLong.incrementAndGet())用于格式化整型,补零到指定长度
     * 比如:
     * String.format("%0" + 20 + "d", id); 将生成字符串 "000000000000000012345"。
     * @param seqName 唯一key值
     * @return 日期格式++补零自增
     */
    private String seqNo(String seqName){
        RAtomicLong atomicLong = redissonClient.getAtomicLong(seqName);
        //转换日期格式
        DateTimeFormatter dateTimeFormatters = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
        String localDateStr = LocalDateTime.now().format(dateTimeFormatters);
        //计算剩余补零长度
        int len = 32-localDateStr.length();
        return localDateStr + String.format("%0"+len+"d",atomicLong.incrementAndGet());

    }
}

封装

Spring 提供了CacheManager接口,用于管理应用程序中的缓存。主要负责创建、配置、管理和销毁缓存对象。

Cache 是实际存储数据的对象或接口,它负责存储和检索缓存的数据。它是CacheManager的一部分,用于实际存储缓存项并提供相关的操作接口。

我们可以基于Spring提供这两个类,封装成自己的代码(以前公司这样用的,感觉很优雅),示例代码如下:

先创建一个CacheManagerBean

java 复制代码
@Configuration
public class MyConfig {
    @Bean
    public CacheManager cacheManager(RedissonClient redissonClient){
        RedissonSpringCacheManager redissonSpringCacheManager = new RedissonSpringCacheManager(redissonClient);
        return redissonSpringCacheManager;
    }
}

然后定义一个接口,实现它,编写基本功能操作

java 复制代码
public interface CacheManage {
    Cache getCache(String name);
    String id16(String idName);
    String seqNo32(String seqName);
    RLock getLock(String key);
}
@Component
public class RedisCacheManage implements CacheManage {
    private CacheManager cacheManager;

    private RedissonClient redissonClient;

    public RedisCacheManage(CacheManager cacheManager, RedissonClient redissonClient) {
        this.cacheManager = cacheManager;
        this.redissonClient = redissonClient;
    }

    @Override
    public Cache getCache(String name) {
        return cacheManager.getCache(name);
    }

    /**
     * 生成16位的自增id
     * String.format("%0"+len+"d",atomicLong.incrementAndGet())用于格式化整型,补零到指定长度
     * 比如:
     * String.format("%0" + 3 + "d", id); 将生成字符串 "045"。
     * @param idName 唯一key值
     * @return 时间戳+补零自增
     */
    public String id16(String idName){
        RAtomicLong atomicLong = redissonClient.getAtomicLong(idName);
        return System.currentTimeMillis() + String.format("%0"+3+"d",atomicLong.incrementAndGet());
    }


    /**
     * 生成32位的自增no,
     * String.format("%0"+len+"d",atomicLong.incrementAndGet())用于格式化整型,补零到指定长度
     * 比如:
     * String.format("%0" + 20 + "d", id); 将生成字符串 "000000000000000012345"。
     * @param seqName 唯一key值
     * @return 日期格式++补零自增
     */
    public String seqNo32(String seqName){
        RAtomicLong atomicLong = redissonClient.getAtomicLong(seqName);
        //转换日期格式
        DateTimeFormatter dateTimeFormatters = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
        String localDateStr = LocalDateTime.now().format(dateTimeFormatters);
        //计算剩余补零长度
        int len = 32-localDateStr.length();
        return localDateStr + String.format("%0"+len+"d",atomicLong.incrementAndGet());

    }

    @Override
    public RLock getLock(String key) {
        return redissonClient.getLock(key);
    }
}

我们在Spring 初始化的时候获取Cache ,并定义为全局变量,往后都基于此Map 进行put()get()操作,便于管理,我们可以看下底层是如何存储的,如图所示,存储在Map结构中。

测试用例如下:

java 复制代码
@RestController
public class MyController {
    @Autowired
    CacheManage cacheManage;
    
    Cache cache;

    @PostConstruct
    public void init(){
        cache = cacheManage.getCache("test_cache");
    }


    @GetMapping("/test")
    public void test() {
        cache.put("test_key","hello world");
        String s = cache.get("test_key", String.class);
        System.out.println(s);
        /** Output:
         *  hello world
         */
    }
}

执行结果如图

有一些场景可能需要设置缓存过期时间,防止长时间占用,浪费内存。

我们可以自定义一个CacheManage ,然后继承RedissonSpringCacheManager,示例代码如下:

java 复制代码
public class MyRedisCacheManage extends RedissonSpringCacheManager {

    public MyRedisCacheManage(RedissonClient redisson) {
        super(redisson);
    }

    @Override
    protected CacheConfig createDefaultConfig() {
    	//设置默认缓存过期时间,ttl过期时间为30分钟、最大空闲时间为5分钟
        return new CacheConfig(30 * 60 * 1000,5 * 60 * 1000);
    }
}

然后配置Bean的时候更换为自定义的类

java 复制代码
@Configuration
public class MyConfig {
    @Bean
    public CacheManager cacheManager(RedissonClient redissonClient){
        MyRedisCacheManage redissonSpringCacheManager = new MyRedisCacheManage(redissonClient);
        return redissonSpringCacheManager;
    }
}

然后开启一个线程倒计时,如果达到空闲时间就会清除,否则ttl过期清除(经过测试hash里面过期清除的是最先插入的key值,不会全部清除)。

可能有些时候,你需要单独设置不同的过期时间,你可以这样处理

java 复制代码
@Configuration
public class MyConfig {
    @Bean
    public CacheManager cacheManager(RedissonClient redissonClient){
        MyRedisCacheManage redissonSpringCacheManager = new MyRedisCacheManage(redissonClient);
        //单独设置缓存过期时间
        Map<String,CacheConfig> map = new HashMap<>();
        map.put("mycache1",new CacheConfig(5 * 60 * 1000,2 * 60 * 1000));
        map.put("mycache2",new CacheConfig(15 * 60 * 1000,7 * 60 * 1000));
        redissonSpringCacheManager.setConfig(map);
        return redissonSpringCacheManager;
    }
}
@RestController
public class MyController {
    @Autowired
    CacheManage cacheManage;
    Cache cache;
    Cache cache1;
    Cache cache2;

    @PostConstruct
    public void init(){
        cache = cacheManage.getCache("test_cache");
        cache1 = cacheManage.getCache("mycache1");
        cache2 = cacheManage.getCache("mycache2");
    }


    @GetMapping("/test")
    public void test() {
        cache.put("test_key","hello world");
        cache1.put("mycache1","hello world1");
        cache2.put("mycache2","hello world2");
    }
}
相关推荐
张张张3124 分钟前
4.2学习总结 Java:list系列集合
java·学习
KATA~6 分钟前
解决MyBatis-Plus枚举映射错误:No enum constant问题
java·数据库·mybatis
xyliiiiiL22 分钟前
一文总结常见项目排查
java·服务器·数据库
shaoing24 分钟前
MySQL 错误 报错:Table ‘performance_schema.session_variables’ Doesn’t Exist
java·开发语言·数据库
腥臭腐朽的日子熠熠生辉1 小时前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
ejinxian1 小时前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之1 小时前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码2 小时前
Spring Task 定时任务
java·前端·spring
俏布斯2 小时前
算法日常记录
java·算法·leetcode
27669582922 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿