Redis7整合Springboot

999 阅读9分钟

前言

  1. 本文着着重讲解使用RedisTemplate整合Redis。

  2. 阅读前保证自己有扎实的Redis基础

  3. 保证自己对Springboot有基本了解,会使用log4j,swagger相关第三方插件

  4. 本文的前篇是 尚硅谷Redis7个人学习笔记 - 掘金 (juejin.cn),步骤详细,欢迎学习。

Springboot整合Redis相关工具的发展流程

  1. 最早期使用jedis。(功能基础)
  2. 后来使用lettuce。(功能改进)
  3. 如今我们使用基于lettuce改进的RedisTemplate。(功能完善)

Jedis简介

简单来说,Jedis就是Redis官方推荐的Java连接开发工具

在Java中,Redis对应于Jedis就相当于MySQL对应于JDBC。

jedis解决了java对于redis数据库客户端的基本操作,数据库的相关crud操作一应俱全。

jedis的问题在于我们每次使用jedis都需要重新连接数据库,要知道Redis常用来处理并发操作,如此一来就会不可避免的造成性能影响。

弊端归根结底就是缺少连接池技术。

Lettuce简介

Lettuce解决了jedis的连接池问题,相较于jedis更为成熟高效,但lettuce有一个问题, 它没有解决对集群的操作问题

RedisTemplate简介

RedisTemplate是Lettuce被纳入到Springboot大家庭的产物(本质上还是lettuce),它在lettuce的基础上解决了集群相关问题,而且保留了lettuce对于单机Redis的操作,我们在工作中主要使用RedisTemplate

RedisTemplate 实操

作者废话

本人非常不推荐在jedis,lettuce上面浪费时间去练习,笔记。各位要做的就是了解前世今生即可,就像一个java程序员不需要了解二极管为什么是半导体一样。

  1. 面试压根不问,
  2. 工作压根不用,
  3. 微服务中间件实在太多,
  4. 头发实在太少。

本文将通过一个实际的案例,我们用Springboot以及RedisTemplate来写一个可用的服务,带大家了解RedisTemplate

实操环节

大家首先保证以下几点才能开始

  1. Linux防火墙中Redis服务的端口处于打开状态。

  2. redis.conf中的bind注释掉。

  3. redis保护模式关掉

  4. 使用IDE创建一个新的Springboot项目

如果你集群已经配好了,那肯定没问题。不过单机Redis也不影响连接

pom文件的相关配置

需要在pom文件中引入以下几条依赖

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

他就是Springboot整合出来的对于Redis连接所需要的所有工具的合集,一条
依赖搞定!(内部包含了RedisTemplate,RedisTemplate内部又整合lettuce)



<!-- apache的连接池技术 底层连接池需要-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- swagger -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>

<!-- swaggerUI -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

上文中我并没有引入日志相关的依赖,请大家自行引入自己常用的日志即可。

连接单机Redis

在项目properties中配置RedisTemplate(如果使用的是Springboot3则需要在Spring.的后面加上data)

spring.redis.database=0  //连接的数据库
spring.redis.host=Redis服务ip
spring.redis.port=端口
spring.redis.password=连接密码
spring.redis.lettuce.pool.max-active=8 //最大连接保持数
spring.redis.lettuce.pool.min-idle=0 //最小连接数
spring.redis.lettuce.pool.max-idle=8 //最大连接数
spring.redis.lettuce.pool.max-wait=-1ms //最大等待时长,-1ms表示无限等待

实战环节

RedisTemplate的常用API

@Service
@Log4j2
public class testRedisService {
    @Resource
    RedisTemplate redisTemplate;

    //订单key的前缀
        public static final String ORDER_HEAD = "ord:";

    //添加订单
    public void setOrder(){
        //ThreadLocalRandom.current.nextInt   保证多线程时数字的唯一性
        //获得一个随机数作为订单key的后缀
        int orderInt = ThreadLocalRandom.current().nextInt(1000)+1;
        
        //组装完整的订单key
        String key = ORDER_HEAD+orderInt;
        
        // 订单编号信息
        String orderValue = "京东"+UUID.randomUUID().toString();

        
        //执行写入数据库操作
        redisTemplate.opsForValue().set(key,orderValue);
        
        //打印信息
        log.info("key: {} value: {}",key,orderValue);

    }

    //获取订单信息的Service
    public String getOrder(String orderId){
        return (String)redisTemplate.opsForValue().get(ORDER_HEAD+orderId);
    }
}

在上面的代码中,我简单的写了2个Service,我通过RedisTemplate对象的opsForValue方法再调用set方法,向我们配置好的Redis数据库设置了一条String类型的订单号,该订单号的key我使用一个常量作为前缀,后面跟上一个随机数。value则采用uuid。

opsForValue方法是专门给String类型使用的,通过后面跟上setget方法来定义获取或者设置。

至于其他方法我就不再赘述了,因为RedisTemplate对象所提供的的方法除了上面所讲的opsForValue方法外,简直是一目了然,用法也基本相同。

为保证代码的完整性,我把Controller层的代码也贴出来,这一层怎么写并不影响RedisTemplate的使用。

import com.atguigu.redistemplatetest.service.testRedisService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Controller
@Log4j2
@RestController
@Api(tags = "订单API")
public class testRedisController {

    @Resource
    testRedisService testRedisService;

    @ApiOperation("新增订单")
    @PostMapping("/Order")
    public void setOrder(){
        testRedisService.setOrder();
    }

    @ApiOperation("查询订单")
    @GetMapping("/Order/{orderId}")
    public String getOrder(@PathVariable String orderId){
        return testRedisService.getOrder(orderId);
    }
}

RedisTemplate其他相关方法我也贴在下面。

RedisTemplateAPI.png


RedisTemplate的乱码问题

如果你已经按照配置正确写好了相关的Controller,并使用swagger或postman测试接口,你会发现。当不使用中文作为key或者value时,没有任何问题,但如果有中文,则会出现乱码

这是因为RedisTemplate在向Redis传输我们的值时,使用了默认的序列化策略, 点开RedisTemplate方法,就能看到

defaultSerializable.png

从上面可以看到,它在enableDefaultSerializer(使用默认序列化)这一行设置为true 而在下一行的defaultSerializer上,他却没有赋值!

useDefaultSerializable.png 根据上文所说,defaultSerializer没有赋值,那么他在上图中的多个if中,自然就会进入第一个if,这就导致了它使用了jdk自带的序列化,我们不需要了解这个序列化,我们只需要知道,这个策略导致了给Redis服务器的数据出现了编码问题.

所以,我们这里就会产生2种解决方案

  1. 给默认的defaultSerializer赋值,给他一个不会乱码的策略。

  2. 使用RedisTemplate的子类StringRedisTemplate


1. StringRedisTemplate 解决乱码问题

StringRedisTemplate的用法非常简单,只需要把代码中的RedisTemplate换成StringRedisTemplate就行了,别的地方全部一样,而且StringRedisTemplate中opsForValue的get方法返回值直接就是String,无需类型转换!

这种形式的存储方法其实是将任何对象都以String类型存入

StringRedisTemplate.png


2. 手动设置默认序列化

其实我们一般希望以JSON的格式存入数据,其实我们的后端Spring项目最终也是给了前端JSON格式,而JSON格式往往被各种协议所接受

所以我们需要一个新的JSON格式转换器来替换掉RedisTemplate中自动配置的jdk转换器。因为RedisTemplate是Spring官方提供的,那么我们直接看Spring配置各种AutoConfiguration的配置文件就行了。

打开搜索一下关于redis的AutoConfiguration

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration

我们点击第一个RedisAutoConfiguration,果然找到了RedisTemplate的Bean对象

@Bean
@ConditionalOnMissingBean(
    name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<Object, Object> template = new RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

我们看一下它new的这个RedisTemplate()源码, 我们发现他是使用的默认的构造器,并没有对RedisTemplate做任何的处理。但是我们可以找到一个方法

public void setDefaultSerializer(RedisSerializer<?> serializer) {
    this.defaultSerializer = serializer;
}

翻译一下,设置默认序列化,要知道因为它用的默认构造器,所以这个所谓的defaultSerializer应该是空的,但是因为它有这个方法,说明我们是可以设置它的!

搞清楚了这一点,那我们如何才能给这个对象设置属于我们的转换器呢?

让我们重新来看这个RedisTemplate的前缀的源码

@ConditionalOnMissingBean(
    name = {"redisTemplate"}
)

这段代码说的很明显了,当IOC容器中没有名为redisTemplate的对象时这个断代码才生效,也就是说,如果我们放了自己的redisTemplate进IOC中,就能替换掉这段代码:所以我们要做的是自己写一个redisTemplate,并且设置好能转换JSON的转换器,最后把他放进ioc中

@Configuration
public class MyRedisTemplateConfig {



    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Object> template = new RedisTemplate();
        //TODO 别的地方全是照抄源码,只有这里放入我们的JSON转换类
        template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

讲一下上面的GenericJackson2JsonRedisSerializer(),这个类是redisTemplate自带的转换器之一。

为什么你自己在redis客户端写入的数据取不出来?

因为redis-cli手动写入其实相当于使用StringRedisTemplate,大家使用StringRedisTemplate来取数据就能取出来了

Springboot连接集群

我们在上文中讲解lettuce时曾经说过,我们的RedisTemplate在lettuce的基础上弥补了lettuce无法操作集群的短板

其实并不是说配置了集群后,lettuce就无法连接了,而是说,一旦有master发生故障(这时集群会自发进行主从切换),而lettuce无法感知。

也就是说,lettuce并不知道原来的master已经被替换,所以lettuce会依旧向故障的master主机地址发送数据,进而导致服务不可用。

故障测试方法

我们可以在集群启动后手动使一台master下线(shutdown)。然后再次发送get请求向Redis查找订单数据。(结果是没有应答,一直转圈,最终超时!)

解决方案

我们需要在RedisTemplate的配置文件中加上新的配置,让RedisTemplate通过刷新Redis集群节点来监测集群状态

下面贴出Redis集群的property配置。

spring.redis.password=密码
# 如果不使用集群,则我们需要定义redis的host与port
spring.redis.host=ip地址
spring.redis.port=端口
#因为我用的是集群,所以下面使用了nodes标签来定义节点,不用的话就删掉就ok了

#获取失败最大重定向次数 这个带有cluster的也是集群相关的,不用可以删掉
spring.redis.cluster.max-redirects=3

spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

\#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认fāLse关闭 集群相关
spring.redis.lettuce.cluster.refresh.adaptive=true

\#定时刷新  集群相关
spring.redis.lettuce.cluster.refresh.period=2000

spr1ng.redis.c1uster.nodes=例如192.168.111.185:6381,192.168.111.185:6382,192.168.111.172:6383,192.168.111.172:6384,
注意,上面的ip只是示例,排列规则为,不用集群不用加这条

主从,主从,主从  格式为ip:端口。

不要无脑复制粘贴,哪一组在前不重要,但一定是 主机+从机 为一组。

集群的操作按照API指示 基于Redis客户端的操作逻辑 正常操作即可!