• SpringBoot集成Redis实战——步骤、坑点、解决方案


    背景

    回顾项目中的TODO工作,发现留了一条待办项,即对Redis配置参数的具体含义的了解。开发平台研发期间,由于时间紧张,对于Redis,没有进行相对充分的技术预研,集成的比较粗放,虽然目标达成了,使用Redis读写缓存功能也实现了,但心里实际是不踏实的。

    当下有了相对充裕的时间,深入了解下Redis,并对集成部分进行重构与优化,过程中,发现网上很多资料都是存在谬误的,一些坑点或注意事项,也在这里一并整理出来,作为知识沉淀,也能为后来者提供一定的参考,能少走一点弯路。

    原有集成方式

    既然是优化重构,那就不得不说下原有的集成的方式,这样才有对比。
    首先,是引入了jar包依赖,使用的是spring-boot-starter-data-redis

       <!-- redis缓存 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <!--2.0以上版本默认客户端是lettuce, 因为此次是采用jedis,所以需要排除lettuce的jar -->
                <exclusions>
                    <exclusion>
                        <groupId>io.lettuce</groupId>
                        <artifactId>lettuce-core</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <!-- jedis客户端 -->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    使用的springboot版本是2.3.0,而spring-boot-starter-data-redis在2.0版本以上默认使用lettuce作为redis的客户端工具,而我当时查到的集成资料,是需要使用jedis,因此将lettuce排除掉了,并且引用了jedis的maven依赖。

    其次,在yml中配置redis及jedis参数

    spring: 
      devtools:
        livereload:
          enabled: true
      redis:
        host: localhost
        port: 6379
        password: test123
        #新版本redis的timeout是一个duration,需使用如下写法
        timeout: 5s
        database: 0
        #连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
        block-when-exhausted: true
        #    集群环境打开下面注释,单机不需要打开
        #    cluster:
        #      集群信息
        #      nodes: xxx.xxx.xxx.xxx:xxxx,xxx.xxx.xxx.xxx:xxxx,xxx.xxx.xxx.xxx:xxxx
        #      #默认值是5 一般当此值设置过大时,容易报:Too many Cluster redirections
        #      maxRedirects: 3
        jedis:
          pool:
            max-active: 16
            min-idle: 4
            max-idle: 8
            max-wait: 300ms
      profiles:
        active: dev
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    再次,新建一个配置文件,来设置jedis的连接池

    /**
     * Redis配置
     * @author wqliu
     * TODO:具体配置参数待深入了解
     */
    @Configuration
    @Slf4j
    public class RedisConfig {
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.port}")
        private int port;
    
        @Value("${spring.redis.timeout}")
        private Duration timeout;
    
        @Value("${spring.redis.jedis.pool.max-idle}")
        private int maxIdle;
    
        @Value("${spring.redis.jedis.pool.max-wait}")
        private Duration maxWait;
    
        @Value("${spring.redis.password}")
        private String password;
    
        @Value("${spring.redis.block-when-exhausted}")
        private boolean  blockWhenExhausted;
    
        /**
         * 配置文件中的秒数转换为毫秒数的乘积数
         */
        private static final int THOUSAND =1000;
    
        @Bean
        public JedisPool redisPoolFactory()  throws Exception{
            log.info("JedisPool注入成功!!");
            log.info("redis地址:" + host + ":" + port);
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxIdle(maxIdle);
            jedisPoolConfig.setMaxWaitMillis((int)maxWait.getSeconds()* THOUSAND);
            // 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
            jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
            // 是否启用pool的jmx管理功能, 默认true
            jedisPoolConfig.setJmxEnabled(true);
            JedisPool jedisPool=null;
            if(StringUtils.isNotBlank(password)){
                jedisPool = new JedisPool(jedisPoolConfig, host, port, (int)timeout.getSeconds()* THOUSAND, password);
            } else{
                //redis服务器未设置密码的情况下不传递密码参数,否则读写时会报错
                jedisPool = new JedisPool(jedisPoolConfig, host, port, (int)timeout.getSeconds()* THOUSAND);
            }
            return jedisPool;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    在回顾代码的时候就发现这里有问题了,yml中的配置参数,与上述配置类中的参数对应不起来,主要有以下几点:
    1.配置文件中设置了 min-idle、max-active属性,而配置类中并没有使用
    2.配置文件中block-when-exhausted,上级节点是redis,而在配置类中,使用该属性的则是jedisPool,那这个属性,究竟上级节点应该是redis还是pool?

    大概当时是从网上拷贝的现成代码,没有认真查看(本次重构过程中,还反复搜到了多个来源,都存在这样的错误)。

    此时,产生了新的疑问,按照SpringBoot自动装配的套路,我在yml中配置了,按理说就不需要再写配置类,SpringBoot应该把这些参数给加载进去了。

    最后,看下封装的工具类,因为使用的redis功能相对简单,键值对都是string类型,未使用集合等高级特性,因此这个工具类也比较简单。

    package tech.popsoft.platform.common.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    
    import java.util.Map;
    import java.util.Set;
    
    
    /**
     * Jedis工具类
     * @author  wqliu
     * @date  2022-8-1
    */
    @Component
    @Slf4j
    public class JedisUtil {
        @Autowired
        private JedisPool jedisPool;
    
        /**
         * 存入redis缓存
         *
         * @param key
         * @param value
         */
        public void set(String key, String value) {
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                jedis.set(key, value);
            } catch (Exception ex) {
                log.error("存储redis出错" + ex);
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
    
        }
    
        /**
         * 从redis缓存中读取
         */
        public String get(String key) {
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                return jedis.get(key);
            } catch (Exception ex) {
                log.error("读取redis出错" + ex);
                return StringUtils.EMPTY;
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
    
        }
    
        /**
         * 从redis缓存中移除
         */
        public void remove(String key) {
    
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                jedis.del(key);
            } catch (Exception ex) {
                log.error("移除redis出错" + ex);
    
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    
        /**
         * 从redis缓存中移除指定前缀的所有值
         */
        public void removePrefix(String prefix) {
    
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                Set<String> set= jedis.keys(prefix+"*");
                for (String item: set) {
                    jedis.del(item);
                }
    
            } catch (Exception ex) {
                log.error("移除redis出错" + ex);
    
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
    
        }
    
        /**
         * 批量存入缓存
         * @param cachedMap
         */
        public void setBatch(Map<String,String> cachedMap) {
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                for(String key : cachedMap.keySet()) {
                    jedis.set(key, cachedMap.get(key));
                }
            } catch (Exception ex) {
                log.error("存储redis出错" + ex);
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129

    深入了解集成知识

    如上所述,发现了不少疑点,因此补了下关于集成的相关知识,整理如下。

    spring-data-redis与jedis是什么关系?

    这俩的关系,用一个类比来说明,就是slf4j与logback的关系,也就是spring-data-redis对reids底层开发包(Jedis、JRedis、lettuce等 )进行了高度封装,统一由RedisTemplate提供了redis各种操作、异常处理及序列化工作。
    也就是说,可以将spring-data-redis视作抽象的接口,对redis的读写,可以灵活更换为具体的客户端,如jedis或lettuce。

    从这一点就可以看出,我们应该使用spring-boot-data-redis,而不是直接使用Jedis等客户端,一方面,封装后的组件,往往比未封装的组件更易用;另一方面,当需要更换组件时,明显更易于实现。

    jedis与lettuce的差异

    既然spring-data-redis只是个封装,那么具体的实现组件,jedis和lettuce应该选哪个呢?

    先来看下简介。
    Jedis:是老牌的Redis的Java实现客户端,提供了比较全面的Redis命令的支持。
    Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
    好像从简介中也看不出谁优谁劣,再进一步看下技术实现。

    Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
    Lettuce基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作。

    这时候就看出来差别来了,从技术实现上,明显Lettuce更胜一筹。
    SpringBoot从2.0版本开始,将spring-boot-data-redis内置的jedis,更换为lettuce,大概也是后者优于前者的一个佐证。

    重构后集成方式

    首先,是引入了jar包依赖,使用的是spring-boot-starter-data-redis

          <!-- redis缓存 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    使用的springboot版本是2.3.0,而spring-boot-starter-data-redis在2.0版本以上默认使用lettuce作为redis的客户端工具。

    这时候,第一个坑点出现了,lettuce内部使用了apache的连接池,但并没有强依赖,需要引入commons-pool2,如上所示。

    其次,在yml中配置redis及lettuce参数

    spring: 
      devtools:
        livereload:
          enabled: true
        redis:
        host: localhost
        port: 6379
        password: test123
        #新版本redis的timeout是一个duration,需使用如下写法
        timeout: 10s
        database: 0
        lettuce:
          pool:
            # 连接池中的最小空闲连接
            min-idle: 2
            # 连接池中的最大空闲连接
            max-idle: 2
            # 连接池的最大连接数
            max-active: 16
            #连接池最大阻塞等待时间
            max-wait: 30s
      profiles:
        active: dev
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    以上配置,会自动由SpringBoot装配,根本就不需要再写个配置类,把这些配置参数加载后实例化连接池。

    再次,就是实现工具类了,这里进行了适当的功能扩展。

    package tech.popsoft.platform.common.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.stereotype.Component;
    
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    
    
    /**
     * 缓存工具类
     *
     * @author wqliu
     * @date 2022-8-1
     */
    @Component
    @Slf4j
    public class CacheUtil {
    
    
        @Autowired
        public RedisTemplate redisTemplate;
    
    
        /**
         * 设置缓存对象
         *
         * @param key   缓存的键
         * @param value 缓存的值
         */
        public <T> void set(String key, T value) {
    
            redisTemplate.opsForValue().set(key, value);
    
        }
    
        /**
         * 设置缓存对象,附带设定有效期
         *
         * @param key      缓存的键值
         * @param value    缓存的值
         * @param timeout  时间
         * @param timeUnit 时间单位
         */
        public <T> void set(String key, T value, Integer timeout, TimeUnit timeUnit) {
            redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
        }
    
        /**
         * 设置缓存对象的有效期
         *
         * @param key      缓存的键值
         * @param timeout  时间
         * @param timeUnit 时间单位
         */
        public <T> void expire(String key, Integer timeout, TimeUnit timeUnit) {
            redisTemplate.expire(key, timeout, timeUnit);
        }
    
    
        /**
         * 获取缓存对象
         *
         * @param key 缓存键值
         * @return 缓存键值对应的数据
         */
        public <T> T get(String key) {
            ValueOperations<String, T> operation = redisTemplate.opsForValue();
            return operation.get(key);
        }
    
    
        /**
         * 删除缓存对象
         *
         * @param key 缓存的键
         */
        public boolean remove(String key) {
            return redisTemplate.delete(key);
        }
    
    
        /**
         * 从redis缓存中移除指定前缀的所有值
         */
        public void removePrefix(String prefix) {
    
            Set keys = redisTemplate.keys(prefix + "*");
            redisTemplate.delete(keys);
        }
    
        
        /**
         * 批量存入缓存
         *
         * @param cachedMap
         */
        public void setBatch(Map<String, String> cachedMap) {
            for (String key : cachedMap.keySet()) {
                set(key, cachedMap.get(key));
            }
        }
    
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111

    可以看出来,使用封装后的redisTemplate,要比原生的jedis方便得多,一句代码就能实现读或写,而jedis更像是访问关系型数据库的模式,需要先从连接池中获取1个连接,然后执行读或写操作,最后再关闭连接。

    完成上面三个步骤后,这时候,已经可以正常使用Redis来进行缓存的读写了。

    但是,这还没有完,通过系统读写是没问题,但是使用redis客户端工具,直连redis服务器,查看数据时,则会显示多了一些不可读的前缀\xac\xed\x00\x05t\x00\,这是另外一个坑点。
    这是怎么出现的呢?原来lettuce默认使用JdkSerializationRedisSerializer作为序列化与反序列化的工具,将字符串转换为字节数组搞出来的幺蛾子。

    怎么解决呢?知道原因了,解决思路也有了,搞一个配置类,将默认的序列化与反序列的类替换掉。

    @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<Object, Object> redisStringTemplate(RedisTemplate<Object, Object> redisTemplate) {
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(stringRedisSerializer);
            redisTemplate.setValueSerializer(stringRedisSerializer);
            return redisTemplate;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    StringRedisSerializer是spring-data提供的,同时还提供了负责json数据的处理类,可用于键值为json的场景。

  • 相关阅读:
    MS | 使用小技巧不完全总结
    Linux crontab 命令定时任务设置
    leetcode:46.全排列
    HarmonyOS学习 -- ArkTS开发语言入门
    HTML期末学生大作业:基于html+css+javascript+jquery企业餐厅11页 企业网站制作
    12.ElasticSearch系列之分布式特性及分布式搜索机制(一)
    Mybatis框架源码笔记(一)之编译Mybatis源码和源码调试环境准备
    一文带你读懂云原生、微服务与高可用
    python实现 合并相同编号的证书名称
    如何让普通用户使用sudo?
  • 原文地址:https://blog.csdn.net/seawaving/article/details/127902440