• Java21 + SpringBoot3整合Redis,使用Lettuce连接池,推荐连接池参数配置,封装Redis操作


    前言

    近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。

    本项目为前后端分离开发,后端基于Java21SpringBoot3开发,后端使用Spring SecurityJWTSpring Data JPA等技术栈,前端提供了vueangularreactuniapp微信小程序等多种脚手架工程。

    本文主要介绍在SpringBoot3项目中如何整合Redis,JDK版本是Java21

    项目地址:https://gitee.com/breezefaith/fast-alden

    相关技术简介

    Redis

    Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

    image

    和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

    redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。
    Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

    官网:http://redis.io/

    Redis 常用数据类型使用场景:

    • String,存短信验证码、缓存、计数器、分布式session
    • List,发布订阅等
    • Set,共同好友、点赞或点踩等
    • Hash,存储对象
    • Zset,排行榜
    • HyperLogLog,在线用户数、统计访问量等
    • GeoHash,同城的人、同城的店等
    • BitMap,签到打卡、活跃用户等

    实现步骤

    引入maven依赖

    pom.xml中添加spring-boot-starter-data-redis以及相关依赖。

    <dependencies>
      <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
      dependency>
      
      <dependency>
        <groupId>com.fasterxml.jackson.datatypegroupId>
        <artifactId>jackson-datatype-jsr310artifactId>
        <version>2.13.0version>
      dependency>
    dependencies>
    

    项目中引入spring-boot-starter-data-redis后默认使用Lettuce作为Redis客户端库。与老牌的Jedis客户端相比,Lettuce功能更加强大,不仅解决了线程安全的问题,还支持异步和响应式编程,支持集群,Sentinel,管道和编码器等等功能。

    如果想使用Jedis,还需要引入Jedis相关依赖。

    <dependency>
        <groupId>redis.clientsgroupId>
        <artifactId>jedisartifactId>
    dependency>
    

    修改配置文件

    修改SpringBoot项目配置文件,本项目使用的是application.yml文件。

    spring:
      data:
        redis:
          host: localhost     # Redis服务器地址
          port: 6379          # Redis服务器连接端口
          password: 123456    # Redis服务器连接密码(默认为空)
          database: 0         # Redis数据库索引(默认为0)
          timeout: 60s        # 连接空闲超过N(s秒、ms毫秒,不加单位时使用毫秒)后关闭,0为禁用,这里配置值和tcp-keepalive值一致
          # Lettuce连接池配置
          lettuce:
            pool:
              max-active: 10  # 允许最大连接数,默认8(负值表示没有限制),推荐值:大于cpu * 2,通常为(cpu * 2) + 2
              max-idle: 8     # 最大空闲连接数,默认8,推荐值:cpu * 2
              min-idle: 0     # 最小空闲连接数,默认0
              max-wait: 5s    # 连接用完时,新的请求等待时间(s秒、ms毫秒),超过该时间抛出异常,默认-1(负值表示没有限制)
    

    定义Redis配置类

    在Redis配置类中,我们声明了一个自定义的RedisTemplate和一个自定义的Redis序列化器RedisSerializer,不声明也可以使用Spring Boot提供的默认的Bean。

    
    /**
     * Redis相关Bean配置
     */
    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisSerializer serializer = redisSerializer();
            RedisTemplate redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(serializer);
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashValueSerializer(serializer);
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    
        @Bean
        public RedisSerializer redisSerializer() {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            //必须设置,否则无法将JSON转化为对象,会转化成Map类型
            objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
    
            // 自定义ObjectMapper的时间处理模块
            JavaTimeModule javaTimeModule = new JavaTimeModule();
    
            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    
            javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
            javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    
            javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
            javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
    
            objectMapper.registerModule(javaTimeModule);
    
            // 禁用将日期序列化为时间戳的行为
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    
            //创建JSON序列化器
            return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
        }
    }
    
    
    

    上述代码中针对java.time包下的LocalDateLocalDateTimeLocalTime等类做了兼容,如果要缓存的实体数据中使用了LocalDateLocalDateTimeLocalTime但没有自定义ObjectMapper的时间处理模块,可能会遇到如下报错。

    2024-01-11T21:33:25.233+08:00 ERROR 13212 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
    
    org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: java.util.ArrayList[0]->com.fast.alden.data.model.SysApiResource["createdTime"])
    	at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.serialize(Jackson2JsonRedisSerializer.java:157) ~[spring-data-redis-3.2.0.jar:3.2.0]
    	at org.springframework.data.redis.core.AbstractOperations.rawValue(AbstractOperations.java:128) ~[spring-data-redis-3.2.0.jar:3.2.0]
    	at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:236) ~[spring-data-redis-3.2.0.jar:3.2.0]
    

    image.png

    这是因为Jackson库在默认情况下不支持Java8java.time包下的LocalDateLocalDateTimeLocalTime等类型的序列化和反序列化。错误堆栈中也给出了解决方案,添加 com.fasterxml.jackson.datatype:jackson-datatype-jsr310依赖,但光添加依赖是不够的,还我们需要像上述代码一样自定义序列化和反序列化的行为。

    定义Redis服务类,封装Redis常用操作

    进行到此处时,其实我们已经可以通过获取RedisTemplate这个Bean来操作Redis了,但为了使用方便,我们定义了一个RedisService执行常用的Redis相关操作,实际上就是对RedisTemplate的进一步封装。

    RedisService接口定义如下。

    /**
     * Redis操作服务类
     */
    public interface RedisService {
    
        /**
         * 保存属性
         *
         * @param time 超时时间(秒)
         */
        void set(String key, Object value, long time);
    
        /**
         * 保存属性
         */
        void set(String key, Object value);
    
        /**
         * 获取属性
         */
        Object get(String key);
    
        /**
         * 删除属性
         */
        Boolean del(String key);
    
        /**
         * 批量删除属性
         */
        Long del(List keys);
    
        /**
         * 设置过期时间
         */
        Boolean expire(String key, long time);
    
        /**
         * 获取过期时间
         */
        Long getExpire(String key);
    
        /**
         * 判断是否有该属性
         */
        Boolean hasKey(String key);
    
        /**
         * 按delta递增
         */
        Long incr(String key, long delta);
    
        /**
         * 按delta递减
         */
        Long decr(String key, long delta);
    
        /**
         * 获取Hash结构中的属性
         */
        Object hGet(String key, String hashKey);
    
        /**
         * 向Hash结构中放入一个属性
         */
        Boolean hSet(String key, String hashKey, Object value, long time);
    
        /**
         * 向Hash结构中放入一个属性
         */
        void hSet(String key, String hashKey, Object value);
    
        /**
         * 直接获取整个Hash结构
         */
        Map hGetAll(String key);
    
        /**
         * 直接设置整个Hash结构
         */
        Boolean hSetAll(String key, Map map, long time);
    
        /**
         * 直接设置整个Hash结构
         */
        void hSetAll(String key, Map map);
    
        /**
         * 删除Hash结构中的属性
         */
        void hDel(String key, Object... hashKey);
    
        /**
         * 判断Hash结构中是否有该属性
         */
        Boolean hHasKey(String key, String hashKey);
    
        /**
         * Hash结构中属性递增
         */
        Long hIncr(String key, String hashKey, Long delta);
    
        /**
         * Hash结构中属性递减
         */
        Long hDecr(String key, String hashKey, Long delta);
    
        /**
         * 获取Set结构
         */
        Set sMembers(String key);
    
        /**
         * 向Set结构中添加属性
         */
        Long sAdd(String key, Object... values);
    
        /**
         * 向Set结构中添加属性
         */
        Long sAdd(String key, long time, Object... values);
    
        /**
         * 是否为Set中的属性
         */
        Boolean sIsMember(String key, Object value);
    
        /**
         * 获取Set结构的长度
         */
        Long sSize(String key);
    
        /**
         * 删除Set结构中的属性
         */
        Long sRemove(String key, Object... values);
    
        /**
         * 获取List结构中的属性
         */
        List lRange(String key, long start, long end);
    
        /**
         * 获取List结构的长度
         */
        Long lSize(String key);
    
        /**
         * 根据索引获取List中的属性
         */
        Object lIndex(String key, long index);
    
        /**
         * 向List结构中添加属性
         */
        Long lPush(String key, Object value);
    
        /**
         * 向List结构中添加属性
         */
        Long lPush(String key, Object value, long time);
    
        /**
         * 向List结构中批量添加属性
         */
        Long lPushAll(String key, Object... values);
    
        /**
         * 向List结构中批量添加属性
         */
        Long lPushAll(String key, Long time, Object... values);
    
        /**
         * 从List结构中移除属性
         */
        Long lRemove(String key, long count, Object value);
    }
    
    

    RedisService实现类定义如下。

    /**
     * Redis操作实现类
     */
    @Service
    public class RedisServiceImpl implements RedisService {
        @Resource
        private RedisTemplate redisTemplate;
    
        @Override
        public void set(String key, Object value, long time) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        }
    
        @Override
        public void set(String key, Object value) {
            redisTemplate.opsForValue().set(key, value);
        }
    
        @Override
        public Object get(String key) {
            return redisTemplate.opsForValue().get(key);
        }
    
        @Override
        public Boolean del(String key) {
            return redisTemplate.delete(key);
        }
    
        @Override
        public Long del(List keys) {
            return redisTemplate.delete(keys);
        }
    
        @Override
        public Boolean expire(String key, long time) {
            return redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
    
        @Override
        public Long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
    
        @Override
        public Boolean hasKey(String key) {
            return redisTemplate.hasKey(key);
        }
    
        @Override
        public Long incr(String key, long delta) {
            return redisTemplate.opsForValue().increment(key, delta);
        }
    
        @Override
        public Long decr(String key, long delta) {
            return redisTemplate.opsForValue().increment(key, -delta);
        }
    
        @Override
        public Object hGet(String key, String hashKey) {
            return redisTemplate.opsForHash().get(key, hashKey);
        }
    
        @Override
        public Boolean hSet(String key, String hashKey, Object value, long time) {
            redisTemplate.opsForHash().put(key, hashKey, value);
            return expire(key, time);
        }
    
        @Override
        public void hSet(String key, String hashKey, Object value) {
            redisTemplate.opsForHash().put(key, hashKey, value);
        }
    
        @Override
        public Map hGetAll(String key) {
            return redisTemplate.opsForHash().entries(key);
        }
    
        @Override
        public Boolean hSetAll(String key, Map map, long time) {
            redisTemplate.opsForHash().putAll(key, map);
            return expire(key, time);
        }
    
        @Override
        public void hSetAll(String key, Map map) {
            redisTemplate.opsForHash().putAll(key, map);
        }
    
        @Override
        public void hDel(String key, Object... hashKey) {
            redisTemplate.opsForHash().delete(key, hashKey);
        }
    
        @Override
        public Boolean hHasKey(String key, String hashKey) {
            return redisTemplate.opsForHash().hasKey(key, hashKey);
        }
    
        @Override
        public Long hIncr(String key, String hashKey, Long delta) {
            return redisTemplate.opsForHash().increment(key, hashKey, delta);
        }
    
        @Override
        public Long hDecr(String key, String hashKey, Long delta) {
            return redisTemplate.opsForHash().increment(key, hashKey, -delta);
        }
    
        @Override
        public Set sMembers(String key) {
            return redisTemplate.opsForSet().members(key);
        }
    
        @Override
        public Long sAdd(String key, Object... values) {
            return redisTemplate.opsForSet().add(key, values);
        }
    
        @Override
        public Long sAdd(String key, long time, Object... values) {
            Long count = redisTemplate.opsForSet().add(key, values);
            expire(key, time);
            return count;
        }
    
        @Override
        public Boolean sIsMember(String key, Object value) {
            return redisTemplate.opsForSet().isMember(key, value);
        }
    
        @Override
        public Long sSize(String key) {
            return redisTemplate.opsForSet().size(key);
        }
    
        @Override
        public Long sRemove(String key, Object... values) {
            return redisTemplate.opsForSet().remove(key, values);
        }
    
        @Override
        public List lRange(String key, long start, long end) {
            return redisTemplate.opsForList().range(key, start, end);
        }
    
        @Override
        public Long lSize(String key) {
            return redisTemplate.opsForList().size(key);
        }
    
        @Override
        public Object lIndex(String key, long index) {
            return redisTemplate.opsForList().index(key, index);
        }
    
        @Override
        public Long lPush(String key, Object value) {
            return redisTemplate.opsForList().rightPush(key, value);
        }
    
        @Override
        public Long lPush(String key, Object value, long time) {
            Long index = redisTemplate.opsForList().rightPush(key, value);
            expire(key, time);
            return index;
        }
    
        @Override
        public Long lPushAll(String key, Object... values) {
            return redisTemplate.opsForList().rightPushAll(key, values);
        }
    
        @Override
        public Long lPushAll(String key, Long time, Object... values) {
            Long count = redisTemplate.opsForList().rightPushAll(key, values);
            expire(key, time);
            return count;
        }
    
        @Override
        public Long lRemove(String key, long count, Object value) {
            return redisTemplate.opsForList().remove(key, count, value);
        }
    }
    
    
    

    使用Redis服务类

    下面以简单的登录和注销为例介绍Redis服务类的简单使用,登录成功时向Redis中插入了一条当前用户的记录,如果要查询系统当前在线用户可以从Redis中查询;注销时从Redis中找到在线用户记录并删除。

    @Service
    public class AuthServiceImpl extends AuthService {
        private final RedisService redisService;
    
        public AuthServiceImpl(RedisService redisService) {
            this.redisService = redisService;
        }
    
        public String login(LoginParam param) {
            // 根据登录参数查找用户,具体代码请自行实现
            SysUser user = new SysUser();
            // 根据用户信息生成token,具体代码请自行实现
            String token = "";
            // 在Redis中增加一条在线用户记录
            redisService.set("OnlineUser:" + user.getUsername() + ":" + token, user);
    
            return token;
        }
    
        public void logout() {
            // 获取当前用户,具体代码请自行实现
            SysUser user = new SysUser();
            // 获取当前用户token,具体代码请自行实现
            String token = "";
            // 清空登录信息,具体代码请自行实现
    
            // 删除Redis中当前用户记录
            redisService.del("OnlineUser:" + user.getUsername() + ":" + token);
        }
    }
    

    总结

    本文简单介绍了一下RedisRedis常见数据类型的使用场景,以及详细介绍了SpringBoot3整合Redis的详细过程,如有错误,还望批评指正。

    在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

  • 相关阅读:
    自定义vue组件发布npm仓库
    02- 数据结构与算法 - 最长回文子串(动态规划/中心扩展算法/Manacher 算法)
    电力物联网关智能通讯管理机-安科瑞黄安南
    oracle存储过程的优缺点
    leetcode 剑指offer51:数组中的逆序对 归并排序
    为什么MySQL使用B+树索引,而不使用其他作为索引呢?
    centos下用ffmpeg推流宇视科技摄像头rtsp流到前端播放(无flash)
    CleanMyMac X2024免费苹果笔记本清理内存清理工具
    SQL优化
    Nginx从入门到入坟(十三)- 负载均衡的原理及优化
  • 原文地址:https://www.cnblogs.com/breezefaith/p/17984580