记一次 Spring Boot 项目中 Redis 工具类的重构实践

当然可以!以下是一篇标准、规范、详尽、可直接发布的技术博客,涵盖了你遇到的问题、解决思路、技术权衡分析,以及最终优化后的高质量代码实现。


📌 前言

在开发一个基于 Spring Boot 的单体 OA 系统时,我遇到了一个看似简单却极具代表性的技术问题:如何在不升级框架的前提下,安全、稳定地使用 Redis 功能?

起初,我尝试通过引入 Redis Streams 来实现消息队列功能,却因 Spring Boot 版本过低(2.1.17.RELEASE)导致 NoClassDefFoundError。升级框架看似是"标准答案",但随之而来的是一系列依赖冲突与兼容性问题

最终,我选择了一条更务实的路径:放弃复杂依赖,回归基础,重构 Redis 工具类,实现轻量、可控、可维护的缓存操作方案。## 🔍 问题背景:一场由"功能需求"引发的版本灾难

1. 初始需求

项目需要实现一个轻量级消息通知机制 ,初步考虑使用 Redis 的 Streams 数据结构,因其具备:

  • 消息持久化
  • 多消费者支持
  • 消息确认机制(ACK)

2. 技术选型与问题爆发

我尝试在项目中使用 Spring Data Redis 提供的 RedisStreamCommands 接口,却发现:

java 复制代码
java.lang.NoClassDefFoundError: org/springframework/data/redis/connection/RedisStreamCommands

原因分析

  • 当前项目使用的是 Spring Boot 2.1.17.RELEASE
  • RedisStreamCommands 是在 Spring Data Redis 2.2+(对应 Spring Boot 2.2+)中才引入的
  • 项目中其他依赖(如 MyBatis-Plus、Druid)与高版本 Spring Boot 存在兼容性问题

3. 升级?还是重构?

升级到 Spring Boot 2.7.x 看似是"标准解法",但评估后发现:

  • 需同步升级 MyBatis-Plus、Druid、Swagger 等多个组件
  • 配置项变更(如 server.context-pathserver.servlet.context-path
  • 安全配置、Actuator 路径等均需调整
  • 风险高、成本大、收益小

💡 核心洞察:一个单体 OA 系统,真的需要 Redis Streams 吗?是否过度设计?


🧠 解决思路:从"解决问题"到"重新定义问题"

我意识到,问题的本质不是"如何使用 Redis Streams",而是:

如何以最低成本、最高稳定性,实现业务所需的消息/缓存功能?

基于此,我提出了两个备选方案:

方案 优点 缺点 适用场景
升级 Spring Boot 功能完整,技术先进 风险高,兼容性问题多 微服务、长期维护项目
重构工具类,回归基础 风险低,稳定可控 功能有限 单体、轻量级应用

最终,我选择了方案二移除 Redisson,重构 RedisUtils,使用 Spring Data Redis 原生 API


✅ 最终方案:轻量、高效、可维护的 Redis 工具类

经过多次迭代与优化,我设计出一个生产级的 RedisUtils 工具类,具备以下特点:

  • ✅ 基于 Spring Data Redis,轻量、标准、兼容性好
  • ✅ 支持所有核心数据结构:String、List、Set、Hash
  • ✅ 支持 TTL 保留、批量操作、存在性检查
  • ✅ 泛型支持,类型安全
  • ✅ 无外部依赖,易于维护

🛠️ 优化后的 RedisUtils 工具类

java 复制代码
package com.example.utils;

import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
 * Redis 工具类
 * <p>
 * 基于 Spring Data Redis 实现,提供对 String、List、Set、Hash 等数据结构的封装操作。
 * 适用于单体应用,轻量、稳定、易维护。
 * </p>
 *
 * @author xiaoli
 * @version 1.0
 * @since 2025-10-26
 */
@Component
public class RedisUtils {

    private static RedisTemplate<String, Object> redisTemplate;

    /**
     * 注入 RedisTemplate 并赋值给静态变量
     */
    @PostConstruct
    public void init() {
        RedisUtils.redisTemplate = this.redisTemplate;
    }

    /**
     * 设置 RedisTemplate(由 Spring 注入)
     */
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        RedisUtils.redisTemplate = redisTemplate;
    }

    // ============================= common =============================

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return true=设置成功;false=设置失败
     */
    public static boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public static boolean expire(String key, long time, TimeUnit unit) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, unit);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据 key 获取过期时间
     *
     * @param key 键 不能为 null
     * @return 时间(秒) 返回 0 代表为永久有效
     */
    public static long getExpire(String key) {
        Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        return expire == null ? 0 : expire;
    }

    /**
     * 判断 key 是否存在
     *
     * @param key 键
     * @return true 存在 false 不存在
     */
    public static boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或 多个
     */
    @SuppressWarnings("unchecked")
    public static void delete(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(Arrays.asList(key));
            }
        }
    }

    /**
     * 批量删除
     *
     * @param keys 键集合
     */
    public static void delete(Collection<String> keys) {
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }

    /**
     * 模糊查询 keys
     *
     * @param pattern 模式,如 "user:*"
     * @return 匹配的 key 集合
     */
    public static Set<String> keys(String pattern) {
        try {
            return redisTemplate.keys(pattern);
        } catch (Exception e) {
            e.printStackTrace();
            return Collections.emptySet();
        }
    }

    // ============================ String =============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public static <T> T get(String key) {
        return key == null ? null : (T) redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true 成功 false 失败
     */
    public static boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time 要大于 0 如果 time 小于等于 0 将设置无限期
     * @return true 成功 false 失败
     */
    public static boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于 0)
     * @return 递增后的值
     */
    public static long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于 0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于 0)
     * @return 递减后的值
     */
    public static long decr(String key, long delta) {
        if (delta > 0) {
            throw new RuntimeException("递减因子必须小于 0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    // ================================ List =================================

    /**
     * 获取 list 缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1 代表所有值
     * @return 列表
     */
    public static <T> List<T> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return Collections.emptyList();
        }
    }

    /**
     * 获取 list 缓存的全部内容
     *
     * @param key 键
     * @return 列表
     */
    public static <T> List<T> lGetAll(String key) {
        return lGet(key, 0, -1);
    }

    /**
     * 获取 list 缓存的长度
     *
     * @param key 键
     * @return 长度
     */
    public static long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取 list 中的值
     *
     * @param key   键
     * @param index 索引 index >= 0 时,0 表头,1 第二个元素,依次类推;
     *              index < 0 时,-1 表尾,-2 倒数第二个元素,依次类推
     * @return 元素值
     */
    public static <T> T lGetIndex(String key, long index) {
        try {
            return (T) redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将 list 放入缓存
     *
     * @param key   键
     * @param value 值
     * @return 长度
     */
    public static long lSet(String key, Object value) {
        try {
            return redisTemplate.opsForList().rightPush(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将 list 放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return 长度
     */
    public static long lSet(String key, Object value, long time) {
        try {
            Long index = redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) {
                expire(key, time);
            }
            return index == null ? 0 : index;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将多个 list 放入缓存
     *
     * @param key    键
     * @param values 值集合
     * @return 长度
     */
    public static long lSetAll(String key, Collection<?> values) {
        try {
            return redisTemplate.opsForList().rightPushAll(key, values.toArray());
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ================================ Set =================================

    /**
     * 根据 key 获取 Set 中的所有值
     *
     * @param key 键
     * @return 值集合
     */
    public static <T> Set<T> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return Collections.emptySet();
        }
    }

    /**
     * 根据 value 从一个 set 中查询, 是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false 不存在
     */
    public static boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入 set 缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public static long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将 set 数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public static long sSet(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) {
                expire(key, time);
            }
            return count == null ? 0 : count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除 set 中的值
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public static long sRemove(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().remove(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ================================ Hash =================================

    /**
     * HashGet
     *
     * @param key  键 不能为 null
     * @param hKey 项 不能为 null
     * @return 值
     */
    public static <T> T hGet(String key, String hKey) {
        try {
            return (T) redisTemplate.opsForHash().get(key, hKey);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取 hash 中所有键值对
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public static <T> Map<String, T> hGetAll(String key) {
        try {
            return (Map<String, T>) redisTemplate.opsForHash().entries(key);
        } catch (Exception e) {
            e.printStackTrace();
            return Collections.emptyMap();
        }
    }

    /**
     * 获取 hash 中所有字段
     *
     * @param key 键
     * @return 字段集合
     */
    public static Set<String> hKeys(String key) {
        try {
            return redisTemplate.opsForHash().keys(key);
        } catch (Exception e) {
            e.printStackTrace();
            return Collections.emptySet();
        }
    }

    /**
     * 获取 hash 中所有值
     *
     * @param key 键
     * @return 值集合
     */
    public static <T> List<T> hValues(String key) {
        try {
            return (List<T>) redisTemplate.opsForHash().values(key);
        } catch (Exception e) {
            e.printStackTrace();
            return Collections.emptyList();
        }
    }

    /**
     * 向 hash 中放入一个键值对
     *
     * @param key   键
     * @param hKey  字段
     * @param value 值
     * @return true 成功 false 失败
     */
    public static boolean hSet(String key, String hKey, Object value) {
        try {
            redisTemplate.opsForHash().put(key, hKey, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向 hash 中放入多个键值对
     *
     * @param key   键
     * @param map   对应多个键值
     * @return true 成功 false 失败
     */
    public static boolean hSetAll(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向 hash 中放入多个键值对并设置时间
     *
     * @param key   键
     * @param map   对应多个键值
     * @param time  时间(秒)
     * @return true 成功 false 失败
     */
    public static boolean hSetAll(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除 hash 中的一个或多个字段
     *
     * @param key  键 不能为 null
     * @param hKey 字段 可以是多个 不能为 null
     */
    public static void hDel(String key, Object... hKey) {
        redisTemplate.opsForHash().delete(key, hKey);
    }

    /**
     * 判断 hash 中是否存在该字段
     *
     * @param key  键 不能为 null
     * @param hKey 字段 不能为 null
     * @return true 存在 false 不存在
     */
    public static boolean hHasKey(String key, String hKey) {
        return redisTemplate.opsForHash().hasKey(key, hKey);
    }

    /**
     * hash 递增 如果不存在, 就会创建一个 并把新增后的值返回
     *
     * @param key   键
     * @param hKey  项
     * @param delta 要增加几(大于 0)
     * @return 递增后的值
     */
    public static double hIncr(String key, String hKey, double delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于 0");
        }
        return redisTemplate.opsForHash().increment(key, hKey, delta);
    }

    /**
     * hash 递减
     *
     * @param key   键
     * @param hKey  项
     * @param delta 要减少几(小于 0)
     * @return 递减后的值
     */
    public static double hDecr(String key, String hKey, double delta) {
        if (delta > 0) {
            throw new RuntimeException("递减因子必须小于 0");
        }
        return redisTemplate.opsForHash().increment(key, hKey, delta);
    }
}

📚 使用说明

1. 添加依赖(pom.xml

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 配置 Redis(application.yml

yaml 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 
    database: 0
    timeout: 5s
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

3. 注入并使用

java 复制代码
@Service
public class UserService {

    @Autowired
    private RedisUtils redisUtils;

    public void cacheUser(User user) {
        redisUtils.set("user:" + user.getId(), user, 3600);
    }

    public User getUser(Long id) {
        return redisUtils.get("user:" + id);
    }
}
相关推荐
xu_yule16 分钟前
Redis存储(15)Redis的应用_分布式锁_Lua脚本/Redlock算法
数据库·redis·分布式
毕设源码-钟学长1 小时前
【开题答辩全过程】以 基于Springboot的扶贫众筹平台为例,包含答辩的问题和答案
java·spring boot·后端
Java水解2 小时前
Spring Boot 4 升级指南:告别RestTemplate,拥抱现代HTTP客户端
spring boot·后端
神云瑟瑟2 小时前
spring boot拦截器获取requestBody的最佳实践
spring boot·拦截器·requestbody
暮色妖娆丶2 小时前
Spring 源码分析 BeanFactoryPostProcessor
spring boot·spring·源码
南极企鹅3 小时前
springBoot项目有几个端口
java·spring boot·后端
清风拂山岗 明月照大江3 小时前
Redis笔记汇总
java·redis·缓存
忧郁的Mr.Li3 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶4 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
消失的旧时光-19434 小时前
第十四课:Redis 在后端到底扮演什么角色?——缓存模型全景图
java·redis·缓存