Springboot(五十三)SpringBoot3整合redisson

1,198 阅读9分钟

前边我们有记录Springboot2/3整合redis的方式。是基于Springboot默认集成的Lettuce客户端实现的。

 

今天我们在项目中集成的redission是在Lettuce之上构建的redis客户端。

 

Redisson:一个在Jedis和Lettuce之上构建的Redis客户端。提供了一系列分布式Java对象和服务,比如:分布式锁、原子变量、计数器等。Redisson意在通过高层次的抽象使得开发者能够更容易地利用Redis提供的各种功能。

 

Redisson通过封装底层的Redis命令,使得在Java代码中操作分布式数据结构就像操作本地数据结构一样自然。如果你的应用程序需要分布式数据类型或者锁,Redisson可能是最佳选择。

 

Redisson提供的以下的主要功能:

分布式对象:

1)      分布式集合(Set、SortedSet、List)

2)      分布式映射(Map)

3)      分布式队列(Queue、Deque)

4)      分布式锁(Lock)

5)      分布式计数器(AtomicLong)

分布式限流:

1)      令牌桶算法(Rate Limiter)

2)      漏桶算法(Rate Limiter)

分布式发布订阅:

1)      发布订阅模式(Pub-Sub)

2)      消息监听器容器(Message Listener Container)

分布式锁和同步:

1)      可重入锁(ReentrantLock)

2)      公平锁(FairLock)

3)      联锁(MultiLock)

4)      红锁(RedLock)

5)      读写锁(ReadWriteLock)

6)      信号量(Semaphore)

7)      闭锁(CountDownLatch)

8)      栅栏(CyclicBarrier)

分布式服务和任务调度:

1)      远程服务(Remote Service)

2)      分布式任务调度器(Task Scheduler)

3)      分布式延迟队列(Delayed Queue)

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

1)      地理位置存储

2)      地理位置搜索

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

分布式缓存:

1)      对Redis进行本地缓存

2)      Spring缓存注解支持

分布式连接池:

1)      支持连接池管理和维护

Redis 集群和哨兵支持:

1)      支持Redis集群模式

2)      支持Redis哨兵模式

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

Spring 集成:

1)      与Spring框架的无缝集成

2)      支持Spring缓存注解

 

功能比较多,我这里就不深入研究了,先研究在Springboot框架中集成。

 

下边我记录一下我在项目中整合redission的全过程。

 

老规矩,先放一下redission官网:

redisson.org/docs

 

一:添加POM依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- redis链接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- redission -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.24.3</version>
</dependency>

 

二:YML配置

1 :这里的配置主要是参照官方文档,我的redis是哨兵模式,官方文档如下:

redisson.org/docs/config…

 

我的配置如下所示:

(1):application.yml:

spring:
  data:
    redis:
      # 超时时间
      timeout: 10000
      # 使用的数据库索引,默认是0
      database: 0
      #host: 39.99.144.212
      #port: 6379
      # 密码
      password: camellia
      ###################以下为red1s哨兵增加的配置###########################
      sentinel:
        master: mymaster       # 哨兵主节点名称,自定义
        nodes: 39.99.144.212:26379,39.99.144.212:26380,39.99.144.212:26381
        password: camellia
      ##################以下为]ettuce连接池增加的配置###########################
      lettuce:
        pool:
          max-active: 100  #连接池最大连接数(使用负值表示没有限制)
          max-idle: 100  #连接池中的最大空闲连接
          min-idle: 50 #连接池中的最小空闲连接
          max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
    # 使用redission配置
    redisson:
      file: classpath:redisson.yml

 

(2):redisson.yml

spring:
  redis:
    redisson:
      config:
        sentinelServersConfig:
          idleConnectionTimeout: 10000
          connectTimeout: 10000
          timeout: 3000
          retryAttempts: 3
          retryInterval: 1500
          failedSlaveReconnectionInterval: 3000
          failedSlaveNodeDetector: !<org.redisson.client.FailedConnectionDetector> {}
          password: xxxxxxxxx
          lockWatchdogTimeout: 60000    # 配置对象看门狗超时时间(以毫秒为单位)
          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:26380"
          - "redis://127.0.0.1:26381"
          masterName: "master"
          database: 0
        threads: 16
        nettyThreads: 32
        codec: !<org.redisson.codec.Kryo5Codec> {}
        transportMode: "NIO"

 

2 :如果你不想使用yml配置,你也可以使用配置类对redission配置

redission.java

package com.modules.redis.config;

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;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class RedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     *
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        // 创建配置 指定redis地址及节点信息
        Config config = new Config();
        // SingleServerConfig 配置方式
        //config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(1).setPassword("123456");
        List<String> sentinelNodes = new ArrayList<>();
        sentinelNodes.add("redis:// 127.0.0.1:26379");
        sentinelNodes.add("redis:// 127.0.0.1:26380");
        sentinelNodes.add("redis:// 127.0.0.1:26381");
        config.useSentinelServers().setPassword("xxxxxxxx").setDatabase(0).setMasterName("xxxxxx").setSentinelAddresses(sentinelNodes);
        // 根据config创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }//*/

}

 

上边的两种配置方式二选一即可。

 

三:测试一下

// =============================================================
// redission 框架测试
@Autowired
private RedissonClient redissonClient;

@Resource
private ArticleDao articleDao;

/**
 * 分布式锁案详解
 * http://127.0.0.1:8001/java/redissonLockLock?lock=redissionlocklock
 */
@GetMapping("redissonLockLock")
public void redissonLockLock(String lock)
{
    RLock rLock = redissonClient.getLock(lock);
    // 使用多线程来模拟多用户并发操作
    for (int i = 0; i < 20; i++)
    {
        final int temp = i;
        new Thread(() -> {
            System.out.println("开始创建订单:"+temp);
            // 生成唯一uuid
            String uuid = UUID.randomUUID().toString();
            // 获取锁,并设置锁的自动释放时间。
            try
            {
                // 上锁
                //waitTime:等待获取锁的最大时间量; leaseTime:锁的自动释放时间(每个线程占用锁的最大时间); unit:时间单位。
                // tryLock(10, 30, TimeUnit.SECONDS):此行代码尝试在10秒内获得锁,如果成功,锁将在30秒后自动释放。
                boolean tryLock = rLock.tryLock(3000010, TimeUnit.MILLISECONDS);
                if (tryLock)
                {
                    // 开启看门狗模式,锁到期自动续期
                    rLock.lock(15, TimeUnit.MILLISECONDS);
                    // 模拟执行业务逻辑
                    log.error("开始执行业务逻辑......");
                    // 获取到锁,做对应的处理。
                    System.out.println("获取到锁!"+temp);
                    String username = "redis";
                    // 将记录写入数据库,这一步可以换成其他操作
                    String ip = "0.0.0.0";
                    Browse browse = new Browse();
                    browse.setUsername(username.toString());
                    browse.setArticleTitle("test:"+temp);
                    browse.setIp(ip);
                    browse.setIsWeixin("0");
                    int res = articleDao.addBrowse(browse);
                    System.out.println("redis写入状态:"+res+" - "+temp);
                    log.error("业务逻辑执行完成......");
                }
                else
                {
                    log.error("锁已存在......");
                }
            }
            catch (InterruptedException e)
            {
                //throw new RuntimeException(e);
                System.out.println("出错了!");
            }
            finally
            {
                // 解锁报错:attempt to unlock lock, not locked by current thread by node id: 544b01e7-babb-4223-b855-00baab04f050 thread-id: 666
                // 解决:https://segmentfault.com/a/1190000042576825?sort=newest
                if (rLock.isLocked() && rLock.isHeldByCurrentThread())
                {
                    rLock.unlock();
                    log.error("释放锁完成……");
                }
                else
                {
                    log.error("锁已过期......");
                }
            }
        }).start();
    }
}

/**
 * Redisson操作布隆过滤器
 * 解决方案:缓存穿透
 * 测试:http://127.0.0.1:8001/java/redissonBloomFilter?bloomKey=redissionFilter
 *
 * @param bloomKey
 */
@GetMapping("redissonBloomFilter")
public void redissonBloomFilter(String bloomKey) {
    RBloomFilter<String> rBloomFilter = redissonClient.getBloomFilter(bloomKey);
    // 初始化布隆过滤器,初始化预期插入的数据量为200,期望误差率为0.01
    rBloomFilter.tryInit(2000.01);
    // 插入数据
    rBloomFilter.add("guanchao");
    rBloomFilter.add("时间里的");
    rBloomFilter.add("guanchao.site");
    // 设置过期时间
    rBloomFilter.expire(60, TimeUnit.SECONDS);

    // 判断是否存在
    boolean victory = rBloomFilter.contains("guanchao");
    boolean forward = rBloomFilter.contains("site");
    System.out.println(victory); //true
    System.out.println(forward); //false
}

/**
 * Redisson实现限流
 * 具体可以查看接口的限流应用
 * 测试:http://127.0.0.1:8001/java/redissonRateLimiter?rateKey=redissionRate
 *
 * @param rateKey
 */
@GetMapping("redissonRateLimiter")
public String redissonRateLimiter(String rateKey)
{
    RRateLimiter rateLimiter = redissonClient.getRateLimiter(rateKey);
    //创建限流器,设置限流策略,例如每秒钟不超过10个请求
    rateLimiter.trySetRate(RateType.OVERALL, 11, RateIntervalUnit.SECONDS);
    // 允许请求次数
    String availCount = "-1";
    // 4. 尝试获取许可证,超时时间为0表示不等待
    if (rateLimiter.tryAcquire(01, TimeUnit.SECONDS))
    {
        availCount = StrUtil.toString(rateLimiter.availablePermits());
        // 如果获取到许可证,执行业务逻辑
        System.out.println("请求被允许:"+availCount);
    }
    else
    {
        // 如果无法获取到许可证,执行其他逻辑或者抛出异常
        System.out.println("请求被限流");
    }
    return availCount;
}


/**
 * Redisson的可重入锁操作
 * 测试:http://127.0.0.1:8001/java/redissonLock?lockKey=redissionLock
 *
 * @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消息发布订阅操作
 * 消息监听器:详见TopicListener.java
 * 测试:http://127.0.0.1:8001/java/redissonTopic?topicKey=redissionTopic
 *
 * @param topicKey
 */
@GetMapping("redissonTopic")
public String redissonTopic(String topicKey)
{
    RTopic rTopic = redissonClient.getTopic(topicKey);
    String msgId = UUID.fastUUID().toString();

    Map<ObjectObject> map = new HashMap();
    map.put("id", msgId);
    map.put("msg", "消息:发布订阅测试");

    long victory = rTopic.publish(map);
    System.out.println("victory:"+victory);
    return StrUtil.toString(victory);
}

/**
 * Redisson操作Queue
 * 测试:http://127.0.0.1:8001/java/redissonQueue?queueKey=redissionQueue
 *
 * @param queueKey
 */
@GetMapping("redissonQueue")
public void redissonQueue(String queueKey) {
    RQueue<String> rQueue = redissonClient.getQueue(queueKey);
    // 向队列中添加值
    rQueue.add("guanchao.site-queue1-111111");
    rQueue.add("guanchao.site-queue2-222222");

    // 设置过期时间
    rQueue.expire(30, TimeUnit.SECONDS);

    // 取值
    String value = rQueue.poll();
    System.out.println("value = " + value);
    // 删除数据,刚刚读取的数据
    rQueue.remove();

    // 获取队列信息
    RQueue<Object> queueValue = redissonClient.getQueue(queueKey);
    System.out.println("queueValue = " + queueValue);
}

/**
 * Redisson操作map
 * 测试:http://127.0.0.1:8001/java/redissionMap?mapKey=redissionMap
 *
 * @param mapKey
 */
@GetMapping("redissionMap")
public void redissionMap(String mapKey) {
    // 创建Map对象
    RMap<String, String> map = redissonClient.getMap(mapKey);
    // 添加键值对
    map.put("key1", "value1");
    map.put("key2", "value2");
    map.put("key3", "value3");
    // 设置过期时间
    map.expire(30, TimeUnit.SECONDS);
    // 获取值
    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操作Set
 * 测试:http://127.0.0.1:8001/java/redissonSet?setKey=redissionSet
 *
 * @param setKey
 */
@GetMapping("redissonSet")
public void redissonSet(String setKey)
{
    RSet<Object> set = redissonClient.getSet(setKey);
    // 添加数据
    set.add("guanchao.site-set1");
    set.add("guanchao.site-set2");
    // 设置过期时间
    set.expire(30, TimeUnit.SECONDS);
    System.out.println(set);
    //通过key取value值
    RSet<Object> setValue = redissonClient.getSet(setKey);
    System.out.println("setValue = " + setValue);
}

/**
 * Redisson操作List
 * 测试:http://127.0.0.1:8001/java/redissonList?listKey=redissionList
 */
@GetMapping("redissonList")
public void redissonList(String listKey) {
    RList<String> list = redissonClient.getList(listKey);
    // 使用add方法向List中添加元素
    list.add("guanchao.site-list1");
    list.add("guanchao.site-list2");
    list.add("guanchao.site-list3");
    // list 设置过期时间
    list.expire(30, TimeUnit.SECONDS);
    // 获取List中的元素
    String s = list.get(0);
    System.out.println("s = " + s);
    // 获取列表长度
    int size = list.size();
    System.out.println("size = " + size);
    // 获取List中的元素
    Object object = redissonClient.getList(listKey).get(1);
    System.out.println("object = " + object);
}

/**
 * 通用对象桶,我们用来存放任类型的对象
 * Redisson将Redis中的字符串数据结构封装成了RBucket,
 * 通过RedissonClient的getBucket(key)方法获取一个RBucket对象实例,
 * 通过这个实例可以设置value或设置value和有效期。
 * 测试:http://127.0.0.1:8001/java/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();
}

 

以上是我全部的测试代码,不太建议使用redission做发布订阅操作。我这里也只是演示了发布的代码。订阅的部分并没有演示。

 

操作起来相对来说还是比较容易的。

 

四:简单封装一个redission工具类

闲着没啥事,寻思封装一个redissionUtils工具类叭,用着更方便点。

但是,封装完了发现,还不如直接用,我TM……

写都写了,记录一下叭:

package com.modules.redis.utils;

import jakarta.annotation.Resource;
import org.redisson.api.*;
import org.redisson.client.codec.StringCodec;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

//@Component
public class RedissionUtils
{
    /**
     * 构造函数
     */
    public RedissionUtils() {}

    /**
     * 默认缓存时间
     */
    private static final Long DEFAULT_EXPIRED = 32000L;

    /**
     * 自动装配redisson client对象
     */
    @Resource
    private RedissonClient redissonClient;

    /**
     * 用于操作key
     * @return RKeys 对象
     */
    public RKeys getKeys() {
        return redissonClient.getKeys();
    }
    /**
     * 移除缓存
     *
     * @param key
     */
    public void delete(String key) {
        redissonClient.getBucket(key).delete();
    }

    /**
     * 获取getBuckets 对象
     *
     * @return RBuckets 对象
     */
    public RBuckets getBuckets() {
        return redissonClient.getBuckets();
    }

    /**
     * 读取缓存中的字符串,永久有效
     *
     * @param key 缓存key
     * @return 字符串
     */
    public String getStr(String key) {
        RBucket<String> bucket = redissonClient.getBucket(key);
        return bucket.get();
    }

    /**
     * 缓存字符串
     *
     * @param key
     * @param value
     */
    public void setStr(String key, String value) {
        RBucket<String> bucket = redissonClient.getBucket(key);
        bucket.set(value);
    }

    /**
     * 缓存带过期时间的字符串
     *
     * @param key     缓存key
     * @param value   缓存值
     * @param expired 缓存过期时间,long类型,必须传值
     */
    public void setStr(String key, String value, long expired) {
        RBucket<String> bucket = redissonClient.getBucket(key, StringCodec.INSTANCE);
        bucket.set(value, expired <= 0L ? DEFAULT_EXPIRED : expired, TimeUnit.SECONDS);
    }

    /**
     * string 操作,如果不存在则写入缓存(string方式,不带有redisson的格式信息)
     *
     * @param key     缓存key
     * @param value   缓存值
     * @param expired 缓存过期时间
     */
    public Boolean setIfAbsent(String key, String value, long expired) {
        RBucket<String> bucket = redissonClient.getBucket(key, StringCodec.INSTANCE);
        return bucket.trySet(value, expired <= 0L ? DEFAULT_EXPIRED : expired, TimeUnit.SECONDS);
    }

    /**
     * 如果不存在则写入缓存(string方式,不带有redisson的格式信息),永久保存
     *
     * @param key   缓存key
     * @param value 缓存值
     */
    public Boolean setIfAbsent(String key, String value) {
        RBucket<String> bucket = redissonClient.getBucket(key, StringCodec.INSTANCE);
        return bucket.trySet(value);
    }

    /**
     * 判断缓存是否存在
     *
     * @param key
     * @return true 存在
     */
    public Boolean isExists(String key) {
        return redissonClient.getBucket(key).isExists();
    }

    /**
     * 获取RList对象
     *
     * @param key RList的key
     * @return RList对象
     */
    public <T> RList<T> getList(String key) {
        return redissonClient.getList(key);
    }

    /**
     * 获取RMapCache对象
     *
     * @param key
     * @return RMapCache对象
     */
    public <K, V> RMapCache<K, V> getMap(String key) {
        return redissonClient.getMapCache(key);
    }

    /**
     * 获取RSET对象
     *
     * @param key
     * @return RSET对象
     */
    public <T> RSet<T> getSet(String key) {
        return redissonClient.getSet(key);
    }

    /**
     * 获取RScoredSortedSet对象
     *
     * @param key
     * @param <T>
     * @return RScoredSortedSet对象
     */
    public <T> RScoredSortedSet<T> getScoredSortedSet(String key)
    {
        return redissonClient.getScoredSortedSet(key);
    }

}

 

以上大概就是Springboot3整合redission的基本过程以及简单使用。

 

详细的使用,后边会记录。

 

有好的建议,请在下方输入你的评论。