文章目录

  • 一、 操作 Redis的3种实现对比
  • 不同点说明
  • 1.1、Jedis
  • 1.2、Lettuce
  • 1.3、Redisson
  • 1.4、spring再次封装redisTemplete源码
  • 二、Lettuce概要
  • 1.1、bug总结
  • 1.1.1、OOM堆外内存溢出问题与方案
  • 1.1.2、Connection断连问题与方案
  • 1.1.3、Netty防止内存泄露常识
  • 三、整合redis(多数据源)
  • 1.1、工程结构
  • 1.2、pom依赖
  • 1.3、application.properties
  • 1.4、redis的配置类
  • 1.4.1、公共配置抽取
  • 1.4.2、第一个redis数据源
  • 1.4.2、第二个redis数据源
  • 1.5、controller
  • 1.6、注入说明与测试效果
  • 1.6.1、注入方式说明
  • 1.6.2、测试效果



一、 操作 Redis的3种实现对比

  1. Jedis
  2. Lettuce
  3. Redisson

共同点:都提供了基于Redis操作的Java API,只是封装程度,具体实现稍有不同。

不同点说明

1.1、Jedis

  1. 是Redis的Java实现的客户端。
  2. 支持基本的数据类型5种:String、Hash、List、Set、Sorted Set。

特点:使用阻塞的I/O,方法调用同步,程序流需要等到socket处理完I/O才能执行,不支持异步操作。Jedis客户端实例不是线程安全的,需要通过连接池来使用Jedis。

1.2、Lettuce

  1. 用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
  2. 基于Netty框架的事件驱动的通信层,其方法调用是异步的。
  3. Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作。
  4. 有2个缺陷就是高并发下没有及时回收导致OOM和间接性断连问题`

1.3、Redisson

优点:分布式锁,分布式集合,可通过Redis支持延迟队列。

可以整合其他实现如redis、springcache、等…


1.4、spring再次封装redisTemplete源码

  1. redisTemplete:lettuce、jedid操作redis的底层客户端。
  2. spring再次封装redisTemplete 在 (RedisAutoConfiguration)自动配置里面能看见
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})

二、Lettuce概要

SpringBoot2.0以后默认使用 Lettuce作为操作redis的客户端

  1. springboot2.0以后默认使用lettuce作为操作redis的客户端,他使用netty进行网络通讯
  2. Lettuce新,使用netty,吞吐量大

1.1、bug总结

1.1.1、OOM堆外内存溢出问题与方案

lettuce 操作netty的时候,没有及时的进行内存释放,导致OutOfDirectMemoryError

2种解决办法:

第一种:
lettuce的高并发下没有及时回收内存的bug导致:
1、 netty堆外内存溢出
2、 netty如果没有指定堆外内存,他默认使用 -Xmx300m
3、 这个问题:可以通过netty的-Dio.netty.maxDirectMemory进行设置,但是治标不治本,加大-Xmx的配置,但是没有及时得到内存释放,一定会出现这个异常

第二种:
不采用Lettuce作为底层,切换成Jedis。等官方更新Lettuce客户端

1.1.2、Connection断连问题与方案

lettuce 的Connection长时间会断开,导致 RedisCommandTimeoutException

  1. 因为Socket连接断已经是事实,而且在分布式环境中,网络分区是必然的。
  2. 在网络环境,Redis 服务器主动断掉连接是很正常的,【lettuce 的作者也提及 lettuce 一天发生一两次重连是很正常的】

RedisCommandTimeoutException解决方案:

第一种:netty提供另一个参数的设置:TCP_USER_TIMEOUT,这个参数就是为了针对单独设置某个应用程序的超时重传的设置

第二种:lettuce提供了NettyCustomizer进行扩展,netty所提供的【心跳机制–IdleStateHandler】
【心跳机制】
1、 分析客户端自己做心跳检测,一旦发现Channel死了,主动关闭ctx.close(),那么ChannelInactived事件一定会被触发了。
2、 缺点:增加了客户端的压力

1.1.3、Netty防止内存泄露常识

  1. 在AbstractNioByteChannel.NioByteUnsafe.read() 处创建了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链。
  2. 根据上面的谁最后谁负责原则,每个Handler对消息可能有三种处理方式
  3. 对原消息不做处理,调用 ctx.fireChannelRead(msg)把原消息往下传,那不用做什么释放。
  4. 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,那更要把原消息release掉。
  5. 假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty在Handler链的最末补了一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。

三、整合redis(多数据源)

1.1、工程结构

后台配置redis数据源 redis多数据源配置_多数据源

1.2、pom依赖

<properties>
  <java.version>1.8</java.version>
  <!--版本控制-->
  <jackson.version>2.11.0</jackson.version>
</properties>


 <!--spring-boot-starter-data-redis-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
     <!-- 排除lettuce。这个采用netty通讯目前有2bug。oom和断连-->
     <exclusions>
         <exclusion>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
         </exclusion>
     </exclusions>
 </dependency>

 <!--引入jedis-->
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
 </dependency>

 <!-- 这个包的作用是生成配置元数据官网推荐引入  @ConfigurationProperties(prefix = "spring.redis02")-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-configuration-processor</artifactId>
     <optional>true</optional>
 </dependency>


 <!--jackson-->
 <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-core</artifactId>
     <version>${jackson.version}</version>
 </dependency>
 <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-annotations</artifactId>
     <version>${jackson.version}</version>
 </dependency>
 <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-databind</artifactId>
     <version>${jackson.version}</version>
 </dependency>
 <dependency>
     <groupId>com.fasterxml.jackson.dataformat</groupId>
     <artifactId>jackson-dataformat-xml</artifactId>
     <version>${jackson.version}</version>
 </dependency>

1.3、application.properties

# 端口
server.port=8080
# 应用名称
spring.application.name=boot-redis

# ===============redis的配置-开始===============
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接密码(默认为空)
spring.redis.password=
# Redis服务器连接端口
spring.redis.port=6379
# Redis数据库索引(默认为0)【相当于库的意思】
spring.redis.database=0
# 连接超时时间(毫秒)
spring.redis.timeout=6000
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=1000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=5

#-----第二个redis数据源-----
spring.redis02.host=127.0.0.1
spring.redis02.password=
spring.redis02.port=6379
spring.redis02.database=5
spring.redis02.timeout=6000
# ===============redis的配置-结束===============

1.4、redis的配置类

1.4.1、公共配置抽取

package sqy.config.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Redis配置类
 *      @EnableCaching注解是spring framework中的注解驱动的缓存管理功能。
 *      自spring版本3.1起加入了该注解。如果你使用了这个注解,那么你就不需要在配置cache manager
 *      当你在配置类(@Configuration)上使用@EnableCaching注解时,会触发一个post processor,这会扫描每一个spring bean,查看是否已经存在注解对应的缓存。
 *          如果找到了,就会自动创建一个代理拦截方法调用,使用缓存的bean执行处理。
 * @author suqinyi
 * @Date 2022/1/12
 */
@SuppressWarnings("all")//压制警告
//@EnableCaching
@Configuration
public class RedisConfig {


    @Value("${spring.redis.jedis.pool.max-active}")
    private int redisPoolMaxActive;

    @Value("${spring.redis.jedis.pool.max-wait}")
    private int redisPoolMaxWait;

    @Value("${spring.redis.jedis.pool.max-idle}")
    private int redisPoolMaxIdle;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int redisPoolMinIdle;

    /**
     * 创建redis连接工厂
     */
    public JedisConnectionFactory createJedisConnectionFactory(int dbIndex, String host, int port, String password, int timeout) {
        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
        jedisConnectionFactory.setDatabase(dbIndex);
        jedisConnectionFactory.setHostName(host);
        jedisConnectionFactory.setPort(port);
        jedisConnectionFactory.setPassword(password);
        jedisConnectionFactory.setTimeout(timeout);
        jedisConnectionFactory.setPoolConfig(setPoolConfig(redisPoolMaxIdle, redisPoolMinIdle, redisPoolMaxActive, redisPoolMaxWait, true));
        return jedisConnectionFactory;

    }

//    /**
//     * 配置CacheManager
//     */
//    @Bean
//    public CacheManager cacheManager(RedisTemplate redisTemplate) {
//        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
//        return redisCacheManager;
//    }

    /**
     * 设置连接池属性
     */
    public JedisPoolConfig setPoolConfig(int maxIdle, int minIdle, int maxActive, int maxWait, boolean testOnBorrow) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxWaitMillis(maxWait);
        poolConfig.setTestOnBorrow(testOnBorrow);
        return poolConfig;
    }

    /**
     * 设置RedisTemplate的序列化方式
     */
    public void setSerializer(RedisTemplate template) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
    }
}

1.4.2、第一个redis数据源

package sqy.config.redis;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author suqinyi
 * @Date 2022/1/12
 */
@Configuration
@EnableCaching
public class OneRedisConfig extends RedisConfig {

    @Value("${spring.redis.database}")
    private int dbIndex;

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.timeout}")
    private int timeout;

    /**
     * 配置redis连接工厂
     */
    @Bean
    public RedisConnectionFactory defaultRedisConnectionFactory() {
        return createJedisConnectionFactory(dbIndex, host, port, password, timeout);
    }

    /**
     * 配置redisTemplate
     * 注入方式使用@Resource(name="") 方式注入
     */
    @Bean(name = "defaultRedisTemplate")
    public RedisTemplate defaultRedisTemplate() {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(defaultRedisConnectionFactory());
        setSerializer(template);
        /**
         * 非spring注入使用RedisTemplate,需先调用afterPropertiesSet()方法
         * 不然会报:JedisConnectionFactory was not initialized through afterPropertiesSet()
         */
        template.afterPropertiesSet();
        return template;
    }
}

1.4.2、第二个redis数据源

参数配置类

package sqy.config.redis;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 第二个redis参数配置
 */
@ConfigurationProperties(prefix = "spring.redis02")
@Component
public class SecondProperties {
    private String host;
    private String password;
    private int port;
    private int database;
    private int timeout;

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public int getDatabase() {
        return database;
    }

    public void setDatabase(int database) {
        this.database = database;
    }
}

第二数据源配置

package sqy.config.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author suqinyi
 * @Date 2022/1/12
 */
@Configuration
@EnableCaching
public class SecondRedisConfig extends RedisConfig {

    @Autowired
    SecondProperties secondProperties;
    
    /**
     * 配置redis连接工厂
     * @Primary 默认会注入@Primary配置的组件
     */
    @Primary
    @Bean
    public RedisConnectionFactory cacheRedisConnectionFactory() {
        int dbIndex= secondProperties.getDatabase();
        int port= secondProperties.getPort();
        String host= secondProperties.getHost();
        String password= secondProperties.getPassword();
        int timeout = secondProperties.getTimeout();
        return createJedisConnectionFactory(dbIndex, host, port, password, timeout);
    }


    /**
     * 配置redisTemplate
     *  注入方式使用@Resource(name="")  名称注入
     */
    @Bean(name = "redisTemplate2")
    public RedisTemplate redisTemplate2() {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(cacheRedisConnectionFactory());
        setSerializer(template);

        /**
         * 非spring注入使用RedisTemplate,需先调用afterPropertiesSet()方法
         * 不然会报:JedisConnectionFactory was not initialized through afterPropertiesSet()
         */
        template.afterPropertiesSet();
        return template;
    }
}

1.5、controller

package sqy.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sqy.pojo.Student;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 *  * @author suqinyi
 *  * @Date 2022/1/12
 *  * 测试redis
 */
@RestController
public class RedisController {


    /**
     * 采用@Resource注入 。名称注入
     */
    @Resource(name = "defaultRedisTemplate")
    RedisTemplate redisTemplate;

    @Resource(name = "redisTemplate2")
    private RedisTemplate<String,String> redisTemplate2;

    /**
     * 使用【第一个redis数据源】 存入list类型
     * localhost:8080/redis01Test
     */
    @GetMapping("redis01Test")
    public void redis01Test() throws JsonProcessingException {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("张三","123456"));
        studentList.add(new Student("李四","789456"));
        //jackson
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(studentList);
        //存 5分钟
        redisTemplate.opsForValue().set("studentList", json,5, TimeUnit.MINUTES);
        //取
        String list = (String)redisTemplate.opsForValue().get("studentList");
        System.out.println(list);
    }


    /**
     * 使用【第二个redis数据源】 存入string字符串
     * localhost:8080/redis02Test
     */
    @GetMapping("redis02Test")
    public void redis02Test()  {
        String key="student:name";
        //存  有效期5分钟
        redisTemplate2.opsForValue().set(key+"qq", "存入字符串",5, TimeUnit.MINUTES);
        //取
        String s = redisTemplate2.opsForValue().get(key + "qq");
        System.out.println(s);
    }


}

1.6、注入说明与测试效果

1.6.1、注入方式说明

@Autowired 默认按照类型进行注入

  1. required属性,并且默认为true
  2. required = true 注入bean的时候该bean必须存在,不然就会注入失败!启动就报错
  3. required = false 注入bean的时候如果bean存在,就注入成功,如果没有就忽略跳过,启动不会报错。 但是不能直接使用,因为doRequiredTest为NULL!

@Resource 默认按照名称进行注入

有两个重要属性,分别是name和type

1.6.2、测试效果

后台配置redis数据源 redis多数据源配置_后台配置redis数据源_02

后台配置redis数据源 redis多数据源配置_redis_03