SpringBoot使用Redisson操作Redis及使用场景实战

news2024/9/21 20:41:29

前言

在SpringBoot使用RedisTemplate、StringRedisTemplate操作Redis中,我们介绍了RedisTemplate以及如何SpringBoot如何通过RedisTemplate、StringRedisTemplate操作Redis。
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

那既然SrpingBoot可以通过RedisTemplate操作Redis,为何又出现了Redisson呢?Rddisson 中文文档
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

一、概述

从 spring-boot 2.x 版本开始,spring-boot-data-redis 默认使用 Lettuce 客户端操作数据。

1.1 Lettuce

SpringBoot2之后,默认就采用了lettuce。
是高级Redis客户端,基于Netty框架的事件驱动的通信层,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
Lettuce的API是线程安全的,可以操作单个Lettuce连接来完成各种操作,连接实例(StatefulRedisConnection)可在多个线程间并发访问。

1.2 Reddisson

基于Netty框架的事件驱动的通信层,方法是异步的,API线程安全,可操作单个Redisson连接来完成各种操作。
实现了分布式和可扩展的Java数据结构,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。
提供很多分布式相关操作服务,如,分布式锁,分布式集合,可通过 Redis支持延迟队列。

总结:优先使用Lettuce,需要分布式锁,分布式集合等分布式的高级特性,添加Redisson结合使用。

二、Spring-Boot整合Redisson

2.1 引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>

注意:引入此依赖后,无需再引入spring-boot-starter-data-redis,其redisson-spring-boot-starter内部已经进行了引入,且排除了 Redis 的 Luttuce 以及 Jedis 客户端。因此,在 application.yaml 中 Luttuce 和 Jedis 的配置是不会生效的。
在这里插入图片描述

在项目使用 Redisson 时,我们一般会使用 RedissonClient 进行数据操作,但有朋友或许觉得 RedissonClient 操作不方便,或者更喜欢使用 RedisTemplate 进行操作,其实这两者是可以共存的,我们只需要再定义RedisTemplate的配置类即可。参考SpringBoot使用RedisTemplate、StringRedisTemplate操作Redis。

发现项目引入 Redisson 后,RedisTemplate底层所用的连接工厂也是 Redisson。
在这里插入图片描述

2.2 配置文件

在application.yaml中添加redis的配置信息。

spring:
  data:
    redis:
      mode: master
      # 地址
      host: 30.46.34.190
      # 端口,默认为6379
      port: 6379
      # 密码,没有不填
      password: ''
      # 几号库
      database: 1
      sentinel:
        master: master
        nodes: 30.46.34.190
      cluster:
        nodes: 30.46.34.190
      lettuce:
        pool:
          # 连接池的最大数据库连接数
          max-active: 200
          # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 连接池中的最大空闲连接
          max-idle: 50
          # 连接池中的最小空闲连接
          min-idle: 8

2.3 配置类

@Configuration
@EnableConfigurationProperties({RedisProperties.class})
public class RedissonConfig {

    private static final String REDIS_PROTOCOL_PREFIX = "redis://";

    @Value("${spring.data.redis.mode}")
    private String redisMode;

    private final RedisProperties redisProperties;

    public RedissonConfig(RedisProperties redisProperties) {
        this.redisProperties = redisProperties;
    }

    /**
     * 逻辑参考 RedissonAutoConfiguration#redisson()
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson(@Autowired(required = false) List<RedissonAutoConfigurationCustomizer> redissonAutoConfigurationCustomizers) throws IOException {
        Config config = new Config();
        config.setCheckLockSyncedSlaves(false);

        int max = redisProperties.getLettuce().getPool().getMaxActive();
        int min = redisProperties.getLettuce().getPool().getMinIdle();

        switch (redisMode) {
            case "master": {
                SingleServerConfig singleConfig = config.useSingleServer()
                        .setAddress(REDIS_PROTOCOL_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort())
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    singleConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                singleConfig.setConnectionPoolSize(max);
                singleConfig.setConnectionMinimumIdleSize(min);
            }
            break;
            case "sentinel": {
                String[] nodes = convert(redisProperties.getSentinel().getNodes());

                SentinelServersConfig sentinelConfig = config.useSentinelServers()
                        .setMasterName(redisProperties.getSentinel().getMaster())
                        .addSentinelAddress(nodes)
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    sentinelConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                sentinelConfig.setMasterConnectionPoolSize(max);
                sentinelConfig.setMasterConnectionMinimumIdleSize(min);
                sentinelConfig.setSlaveConnectionPoolSize(max);
                sentinelConfig.setSlaveConnectionMinimumIdleSize(min);
            }
            break;
            case "cluster": {
                String[] nodes = convert(redisProperties.getCluster().getNodes());

                ClusterServersConfig clusterConfig = config.useClusterServers()
                        .addNodeAddress(nodes)
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    clusterConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                clusterConfig.setMasterConnectionMinimumIdleSize(min);
                clusterConfig.setMasterConnectionPoolSize(max);
                clusterConfig.setSlaveConnectionMinimumIdleSize(min);
                clusterConfig.setSlaveConnectionPoolSize(max);
            }
            break;
            default:
                throw new IllegalArgumentException("无效的redis mode配置");
        }

        if (redissonAutoConfigurationCustomizers != null) {
            for (RedissonAutoConfigurationCustomizer customizer : redissonAutoConfigurationCustomizers) {
                customizer.customize(config);
            }
        }

        return Redisson.create(config);

    }

    private String[] convert(List<String> nodesObject) {
        List<String> nodes = new ArrayList<String>(nodesObject.size());
        for (String node : nodesObject) {
            if (!node.startsWith(REDIS_PROTOCOL_PREFIX)) {
                nodes.add(REDIS_PROTOCOL_PREFIX + node);
            } else {
                nodes.add(node);
            }
        }
        return nodes.toArray(new String[0]);
    }
}

2.4 使用方式

@Component
public class RedissonService {
	@Resource
    protected RedissonClient redissonClient;

	public void redissonExists(String key){
		RBucket<String> rBucketValue = redissonClient.getBucket(key, StringCodec.INSTANCE);
		if (rBucketValue.isExists()){
            String value = rBucketValue.get();
            // doSomething
        } else {
           // doElseSomething
        }
	}

}

2.5 实用场景

2.5.1 分布式锁

有点经验的同学一提到使用分布式锁便联想到了redis,那redis如何实现分布式锁呢?

分布式锁本质上要实现的目标就是在Redis中占一个坑(简单的说,就是萝卜占坑的道理),当别的进程也要来占坑时,发现那个坑里已经有一个颗大萝卜时,就只好放弃或者稍后重试。

分布式锁常用手段

1.使用setNx命令
这个命令的详细描述是(set if not exists),如果指定key不存在则设置(成功占坑),在业务执行完成后,调用del命令删该key(释放坑)。比如:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

del  distribution-lock

但这个命令存在一个问题,如果执行逻辑中出现问题,可能导致del指令无法执行,那么该锁就会成为死锁了。
可能有小伙伴贴心的想到了,我们可以给这个key再设置一个过期时间呀。比如:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

del  distribution-batch

即使这样操作后,该逻辑仍有问题,由于 setnx 与 expire 是两条命令,如果在 setnx 与 expire 之间,redis 服务器挂了,就会导致 expire 不会执行,从而过期时间设置失败,该锁仍会成为死锁。

根源是 setnx 与 expire 两条命令并不是原子命令

且redis的事物也无法解决 setnx 与 expire 的问题,因为 expire 是依赖于 setnx 的执行结果的,如果 setnx 没有成功,expire则不应该执行。事物又无法进行if else判断,故 setnx+expire 方式实现分布式锁,并不是优解。

2.使用setNx Ex 命令
上方已经说了 setNx+expire 的问题,Redis官方为了解决这个问题,在2.8版本时引入了 set指令的扩展参数,使得 setnx 与 expire命令可以一起执行。比如:

# set 锁名 值 ex 过期时间(单位:秒) nx
set distribution-lock locked ex 5 nx

// doSomthing

del distribution-lock

从逻辑上来讲,setNx Ex 已是优解了,不会使该分布式锁成为死锁。

但在我们开发中,或许仍会出现问题,为什么呢?
由于我们一开始为此锁设置了一个过期时间,那假如我们的业务逻辑执行耗时超过了设置的过期时间呢?就会出现一个线程未执行完毕,第二个线程可能持有了这个分布式锁的情况。
所以呢,如果使用 setNx Ex 组合,必须要确保自己的锁的超时时间大于占锁后的业务执行时间

3.使用lua脚本+watch dog自动延期机制
这个方案在网上一找一大堆,在此就不做详细赘述。

Redisson实现分布式锁

上方介绍的 setNx 与 setNx Ex 命令,都是Redis 服务器为我们提供的原生命令,也或多或少的存在着一部分问题,为解决setNx Ex命令存在着业务逻辑大于锁超时时间的问题,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(就是续期30s),也可以通过修改Config.lockWatchdogTimeout来另行指定,锁的初始过期时间默认也是30s。

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
@Resource
RedissonClient redissonClient;

@GetMapping("/testDistributionLock")
public BaseResponse<String> testRedission(){
    RLock lock = redissonClient.getLock("redis:distributionLock");
    try {
        boolean locked = lock.tryLock(10, 3, TimeUnit.SECONDS);
        if(locked){
            log.info("获取锁成功");
            Thread.sleep(100);
            return ResultUtils.success("ok" );
        }else{
            log.error("获取锁失败");
            return ResultUtils.error(ErrorCode.SYSTEM_ERROR);
        }
    } catch (InterruptedException e) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR,"出异常了");
    } finally {
        lock.unlock();
    }
}

2.5.2 限流

我们是有面临高并发下需要对接口或者业务逻辑限流的问题,我们可以采用Guaua依赖下的RateLimiter 实现,实际上,Redisssion也有类似的限流功能。

RateLimiter 被称为令牌桶限流,此类限流是首先定义好一个令牌桶,指明在一定时间内生成多少个令牌,每次访问时从令牌桶获取指定数量令牌,如果获取成功,则设为有效访问。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1923155.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

51单片机(STC8H8K64U/STC8051U34K64)_RA8889_8080参考代码(v1.3)

硬件&#xff1a;STC8H8K64U/STC8051U34K64 RA8889开发板 硬件跳线变更为并口8080模式&#xff0c;PS00x&#xff0c;R143&#xff0c;R142不接&#xff0c;R141无关 8080接口电路连接图&#xff1a; 实物连接图&#xff1a; RA8889开发板外接MCU连接器之引脚定义&…

防火巡查记录卡数字化平台

防火巡查记录卡数字化平台 利用凡尔码搭建防火巡查记录卡数字化平台是一个高效且实用的解决方案&#xff0c;能够显著提升防火巡查的效率和管理水平。替代纸质巡检造成的数据丢失等困扰。 一、如何注册凡尔码平台 百度搜索“凡尔码”找到平台地址即可注册开通。凡尔码平台通…

二叉树层序遍历?秒了!

废话不多说&#xff0c;直接上题&#xff0c;涉及到二叉树层序遍历的题目大部分都可以用这个方法&#xff1a; 示例&#xff1a;力扣102 二叉树的层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有…

实验8 视图创建与管理实验

一、实验目的 理解视图的概念。掌握创建、更改、删除视图的方法。掌握使用视图来访问数据的方法。 二、实验内容 在job数据库中&#xff0c;有聘任人员信息表&#xff1a;Work_lnfo表&#xff0c;其表结构如下表所示&#xff1a; 其中表中练习数据如下&#xff1a; 1.‘张明…

数据结构之单链表(赋源码)

数据结构之单链表 线性表 线性表的顺序存储结构&#xff0c;有着较大的缺陷 插入和删除操作需要移动大量元素。会耗费很多时间增容需要申请空间&#xff0c;拷贝数据&#xff0c;释放旧空间。会有不小的消耗即使是使用合理的增容策略&#xff0c;实际上还会浪费许多用不上的…

【Oracle】实验五 PL_SQL编程

【实验目的】 熟悉PL/SQL的数据类型和书写规则熟悉控制结构和游标的使用编写和运行函数、过程和触发器 【实验内容】 编写脚本文件&#xff0c;调试运行脚本文件&#xff0c;并记录结果。 本地子程序的编写及调试 1、编写一个PL/SQL块&#xff0c;功能用于打印学生信息。整…

【学习css1】flex布局-页面footer部分保持在网页底部

中间内容高度不够屏幕高度撑不开的页面时候&#xff0c;页面footer部分都能保持在网页页脚&#xff08;最底部&#xff09;的方法 1、首先上图看显示效果 2、奉上源码 2.1、html部分 <body><header>头部</header><main>主区域</main><foot…

深入解析香橙派 AIpro开发板:功能、性能与应用场景全面测评

文章目录 引言香橙派AIpro开发板介绍到手第一感觉开发板正面开发板背面 性能性能概况性能体验 应用场景移植操作系统香橙派 AIpro开发板支持哪些操作系统&#xff1f;烧写操作系统到SD卡中启动开发板的步骤查看系统提供的事例程序体验——开发的简洁性 视频播放展示ffmpeg简介f…

【Python3】自动化测试_用Playwright发送API请求

一、创建APIRequestContex实例 # 连接到 APIRequest&#xff0c;可用于 Web API 测试的 API。 myRequest myPlaywright.request# 创建APIRequestContext实例&#xff0c;该实例可用于发送 Web 请求 myRequestContext myRequest.new_context() myRequest.new_context(**kwargs…

【MySQL】8.复合查询

复合查询 一.基本查询回顾(新增子查询)二.多表查询三.自连接四.子查询1.单列单行子查询2.单列多行子查询——三个关键字3.多列子查询4.在 from 子句中使用子查询 五.合并查询六.总结 一.基本查询回顾(新增子查询) //1.查询工资高于500或岗位为MANAGER的雇员&#xff0c;同时还…

js逆向-webpack-python

网站&#xff08;base64&#xff09;&#xff1a;aHR0cHM6Ly93d3cuY29pbmdsYXNzLmNvbS96aA 案例响应解密爬取&#xff08;webpack&#xff09; 1、找到目标url 2、进行入口定位&#xff08;此案例使用 ‘decrypt(’ 关键字搜索 &#xff09; 3、找到位置进行分析 --t 为 dat…

【软件工具】VMware Workstation Pro 15.5安装

1、双击运行安装包程序 2、接受许可证协议 3、选择安装位置&#xff0c;建议非中文无空格&#xff0c;增强型键盘驱动程序可选 4、按照自身使用习惯勾选产品更新和客户体验提升计划 5、快捷方式 6、开始安装 7、稍等会儿(可以玩会儿手机) 8、可输入许可证也可直接完成&#xff…

《ElementUI/Plus 基础知识》el-tree 之修改可拖拽节点的高亮背景和线

前言 收到需求&#xff0c;PM 觉得可拖拽节点的高亮背景和线样式不明显&#xff01;CSS 样式得改&#xff01; 注意&#xff1a;下述方式适用于ElementUI el-tree 和 ElementPlus el-tree&#xff01; 修改 拖拽被叠加节点的背景色和文字 关键类名 is-drop-inner .el-tree…

几何距离与函数距离:解锁数据空间中的奥秘

几何距离&#xff1a;直观的空间度量 几何距离&#xff0c;顾名思义&#xff0c;是我们在几何学中熟悉的距离概念&#xff0c;如欧几里得距离、曼哈顿距离和切比雪夫距离等。这些距离度量直接反映了数据点在多维空间中的位置关系。 欧几里得距离&#xff1a;最为人熟知的几何距…

conda install问题记录

最近想用代码处理sar数据&#xff0c;解放双手。 看重了isce这个处理平台&#xff0c;在安装包的时候遇到了一些问题。 这一步持续了非常久&#xff0c;然后我就果断ctrlc了 后面再次进行尝试&#xff0c;出现一大串报错&#xff0c;不知道是不是依赖项的问题 后面看到说mam…

langchain-runnable底层原理

文章目录 langchainlangchain生态介绍langchainLCELrunnablerunnable基础能力介绍invokebatchstreamainvokeabatchastream__or__、__ror__pipeget_nameInputType (属性)OutputType (属性)input_schema (属性)output_schema (属性) langchain langchain生态介绍 langchain是一个…

Min P Sampling: Balancing Creativity and Coherence at High Temperature阅读笔记

上一篇文章是关于大语言模型的调参数&#xff0c;写了temperature这个参数近期的一个工作。那接下来&#xff0c;就不得不再来讲讲top-p这个参数啦。首先还是上文章&#xff0c;同样是非常新的一个工作&#xff0c;2024年7月1日submit的呢。 文章链接&#xff1a;https://arxi…

NLP任务:情感分析、看图说话

我可不向其他博主那样拖泥带水&#xff0c;我有代码就直接贴在文章里&#xff0c;或者放到gitee供你们参考下载&#xff0c;虽然写的不咋滴&#xff0c;废话少说&#xff0c;上代码。 gitee码云地址&#xff1a; 卢东艺/pytorch_cv_nlp - 码云 - 开源中国 (gitee.com)https:/…

LED显示屏中什么是亮度检测?什么是亮度调节?

在夜幕降临的城市中&#xff0c;一套套璀璨夺目的LED显示屏如同现代生活的万花筒&#xff0c;展示着五彩斑斓的信息世界。从繁华的商业街区到繁忙的交通枢纽&#xff0c;再到家庭影院&#xff0c;LED显示屏以其高亮度、长寿命和灵活性&#xff0c;在数字时代的大潮中迅速崛起&a…

Drv8434s芯片两相步进电机驱动程序+硬件解决方案

一、Drv8434s芯片介绍 PWM 微步进电机驱动器 – 具有 STEP/DIR 引脚的 SPI 接口 – 最高 1/256 的微步进分度器 • 集成电流检测功能 – 无需检测电阻 – 4% 满量程电流精度 • 智能调优、慢速和混合衰减选项 • 工作电源电压范围为 4.5V 至 48V • 低 RDS(ON)&#xff1a;24V…