Spring Boot整合Redis 4.x实战:配置、序列化与避坑指南

发布时间:2026/10/13 18:00:41
Spring Boot整合Redis 4.x实战:配置、序列化与避坑指南 说实话Redis 这个组件在 Spring Boot 项目里已经快成标配了。这篇是我 Spring Boot 学习笔记系列的第十一篇主题是整合 Redis 4.x。标题里的 4.x 指的是 Redis 服务端版本不是 Spring Boot 版本这个得先说明白免得有人对不上号。为什么单独拿一篇写 Redis 4.x因为不少老项目到现在还跑在 4.x 上它不像 5.0 之后的 Stream 那样花哨但模块系统、PSYNC2 部分重同步、LFU 淘汰策略这些改动对日常业务开发的影响其实不小。尤其当你需要在 Spring Boot 里做缓存、分布式锁、接口限流、热点数据存储时Redis 4.x 配合 Spring Data Redis 这条链路搭起来快坑也相对少。这篇内容偏实战。适合两类人看一是刚把 Spring Boot 基础跑通的初学者想弄清楚 Redis 到底怎么接进来、序列化器为什么要改二是已经在用 RedisTemplate 但总遇到连接池报错、缓存乱码问题的老哥。我会从环境准备、配置、代码实战、问题排查四个角度把整条链路过一次写的都是我自己反复用过的方案。1. 整合前先搞明白Redis 4.x 能带来什么1.1 Redis 4.x 和旧版本的关键差异很多教程上来就贴依赖、写配置完全不解释为什么。我觉得先搞清楚手里这个 Redis 4.x 跟以前用的 2.8、3.2 有什么本质区别后面出问题时才知道该往哪个方向排查。Redis 4.0 第一个大变化是模块系统。你可以往 Redis 里加载第三方模块比如搜索、JSON 支持等。对普通业务开发来说可能用不上但它意味着 Redis 不再是单纯的 kv 缓存而是往“数据平台”的方向走。第二个变化是 PSYNC2也叫部分重同步的优化。以前的版本里从节点一旦断开重连主节点可能被迫做全量同步数据量大时非常痛苦。4.x 优化了复制缓冲区的处理逻辑从节点重启后有机会只同步缺失部分对主从架构的稳定性提升很明显。第三个变化是内存淘汰策略增加了 LFU。以前我们最常用的是 LRU也就是淘汰最近最少使用的 key。LFU 则是统计访问频率对那种“一段时间被猛刷、后面不再用”的冷热数据场景更准确。配置上可以直接写 allkeys-lfu 或 volatile-lfu。这个特性在缓存容量受限时很实用。另外还有几个工程上特别有价值的命令。UNLINK 是异步删除大 key 的如果你要清理一个几十 MB 的字符串或者超大 Hash用 DEL 会阻塞 Redis 主线程可能造成服务抖动UNLINK 只标记删除后台线程慢慢回收。MEMORY USAGE 命令可以查某个 key 到底占多少内存排查大 key 比用 keys 遍历高效得多。FLUSHALL 和 FLUSHDB 也支持 ASYNC 参数清库不会卡死在线请求。这些特性和 Spring Boot 整合没直接关系但它们决定了你在项目里怎么用 Redis。比如后来我在清理过期用户会话时就特意用 UNLINK 而不是 delete。1.2 客户端选型Lettuce 还是 JedisSpring Boot 2.x 之后spring-boot-starter-data-redis 默认整合的是 Lettuce。早期很多老项目用的是 Jedis如果你维护过这类工程应该见过 JedisPool 相关配置。这里我强烈建议新项目直接用 Lettuce不要动默认依赖。两者最大的区别在连接模型。Lettuce 基于 Netty一个连接可以被多个线程共享实例是线程安全的。换句话说你可以定义一个全局的 RedisTemplate随手往里面扔请求不用担心连接被并发搞乱。Jedis 不一样Jedis 实例不是线程安全的同一个连接在多个线程里用会出问题只能靠连接池来保证每次拿到独立连接。Spring Boot 默认方案已经是 Lettuce如果你还需要连接池额外引入 commons-pool2 依赖并配置 lettuce.pool。大多数场景下一个应用实例保持几条连接就够用因为 Netty 多路复用把网络连接压得很低。对比项LettuceJedis线程安全性线程安全可共享连接非线程安全需独立连接连接模型Netty 多路复用每实例一个 socket 连接异步支持原生支持需要自己封装Spring Boot 默认2.x / 3.x 默认早期版本默认适用场景大多数 Spring Boot 项目轻量工具、脚本如果你是初学者我的建议很直接别纠结用默认的 Lettuce。你真正需要花时间的是序列化器选型这才是日常开发里最容易出问题的地方。2. 从零开始环境准备与项目核心配置2.1 Redis 服务端准备先把服务端搞起来。我自己的习惯是开发环境用本地编译安装或者用包管理工具直接装。Linux 上装 4.x 不难但要注意系统源里的版本可能比较老如果源里是 3.x建议直接用官方源码编译安装。启动方式很简单redis-server如果不想占用当前终端窗口就用后台模式启动redis-server --daemonize yes启动之后一定要验证一下能不能连上redis-cli ping正常情况下会返回 PONG。如果你配置了密码连接和验证要分开写redis-cli -a yourpassword ping这里我要额外强调一个安全习惯。开发环境哪怕只是本机跑也建议设置密码。不要把 Redis 裸奔在一个公网上尤其是 6379 端口默认就是公开的扫描器天天都在扫。设置密码很简单在 redis.conf 里改一行requirepass yourpassword另外注意 bind 和 protected-mode 的配置。开发机上 bind 127.0.0.1 就够了如果是局域网环境要给其他机器连再改成对应内网 IP。protected-mode 默认开启的情况下如果 Redis 没有密码且没有配置 bind它会拒绝外部连接。这个设计本来是为了安全但很多人不清楚导致远程连不上后面排查时会讲到。2.2 Spring Boot 工程依赖与配置项目里引入依赖。如果你是 Spring Boot 2.x直接用官方 starter版本号不需要自己写交给 Spring Boot 的依赖管理就行dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency第二个依赖是给 Lettuce 连接池用的。如果你不想用连接池其实可以不引但我不建议你省这一步。虽然 Lettuce 本身支持共享连接压测下来连接池能帮你控制资源水位避免极端流量下连接数无限上涨。然后是配置文件。这是我最常用的配置模板直接贴在这里spring: redis: host: 127.0.0.1 port: 6379 password: 123456 database: 0 timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 0 max-wait: 3000ms逐项解释一下host 和 port 是连接地址password 对应服务端 requirepass没有就留空database 是 Redis 逻辑库编号默认 0Redis 最多支持 16 个库。timeout 要注意这个值在 Lettuce 下表示命令读写超时时间。如果是 3 秒一次大的批量操作超过 3 秒就会抛超时异常。所以这个值不要设得太激进尤其是后面讲 Pipeline 和大 key 操作时会遇到。连接池参数里面max-active 是最大连接数max-idle 是最大空闲连接min-idle 是最小空闲连接max-wait 是拿连接的最大等待时间。这组参数不建议照搬需要结合你的并发量调。并发不高 16 够用QPS 很高就往上加同时配合压测观察线程等待时间。2.3 RedisTemplate 与序列化器配置服务端和客户端都通了你就会发现直接用默认 RedisTemplate 存数据Redis 客户端里看到的 key 会变成一串乱码比如 \xAC\xED\x00\x05t... 之类的东西。第一次遇到时我真的一脸懵以为数据坏了。其实原因是 Spring Boot 默认的 RedisTemplate 使用 JdkSerializationRedisSerializer也就是 Java 对象序列化。存进去的内容是一大段二进制key 和 value 都是这样。这种序列化方式有几个问题可读性差、体积大、没法跨语言。比如你用 Java 存的数据用 Python 或其他语言读就很麻烦。所以我做任何项目第一件事就是自定义 RedisTemplate 的序列化器。核心配置如下Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); StringRedisSerializer stringRedisSerializer new StringRedisSerializer(); template.setKeySerializer(stringRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); GenericJackson2JsonRedisSerializer jsonRedisSerializer new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonRedisSerializer); template.setHashValueSerializer(jsonRedisSerializer); template.afterPropertiesSet(); return template; } }这里的思路是key 用 String 序列化保证可读性value 用 JSON 序列化存到 Redis 里是一段 JSON其他语言也能读。为什么用 GenericJackson2JsonRedisSerializer 而不是 Jackson2JsonRedisSerializer区别在于前者会在 JSON 里保留 class 类型信息反序列化时不需要你手动指定目标类型灵活很多。代价是 JSON 里多一个类型字段数据体积稍微大一点。这一节配置看起来简单但直接影响后面所有缓存代码。如果你在旧项目里看到 key 是乱码多半就是这里没改。配置完之后我建议顺手把缓存管理器也配好不然用 Cacheable 注解时又是默认 Java 序列化和 RedisTemplate 的行为不一致Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); }注意这个配置里写了 disableCachingNullValues含义是方法返回 null 时不缓存。这个选项在部分场景下要让位给缓存穿透的空值缓存策略后面问题排查那节我会细说。3. 实操把 Redis 用起来3.1 最常用的 String 和 Hash 操作配好了 RedisTemplate第一个落地的场景通常是缓存。字符串操作最典型的就是验证码、计数、临时状态。我一般直接用 StringRedisTemplate因为这类场景 key 和 value 都是字符串写起来非常干净Autowired private StringRedisTemplate stringRedisTemplate; // 写验证码5分钟有效 stringRedisTemplate.opsForValue().set(captcha:user:1001, 482913, Duration.ofMinutes(5)); // 读验证码 String code stringRedisTemplate.opsForValue().get(captcha:user:1001); // 删除验证码 stringRedisTemplate.delete(captcha:user:1001);如果要存对象常规做法是把对象转成 JSON 字符串再塞进去。但如果你用的是前面配置的 RedisTemplateString, Object可以直接塞对象Autowired private RedisTemplateString, Object redisTemplate; User user new User(); user.setId(1001L); user.setName(张三); redisTemplate.opsForValue().set(user:1001, user, Duration.ofMinutes(30)); User cacheUser (User) redisTemplate.opsForValue().get(user:1001);Hash 结构在业务开发里也经常用。它的优势是可以单独读写一个对象里的某个字段不用每次整个序列化和反序列化。比如用户信息缓存的场景HashOperationsString, Object, Object hash stringRedisTemplate.opsForHash(); hash.put(user:1001, name, 张三); hash.put(user:1001, age, 18); MapObject, Object userMap hash.entries(user:1001);使用字符串值的时候有个小坑从 Hash 里读出来的 age 是字符串 18如果你需要数字类型必须手动转换。我见过不少同事在这里直接强转运行时报错才反应过来。3.2 缓存注解Cacheable、CachePut、CacheEvict如果说 RedisTemplate 是手动挡那 Spring Cache 注解就是自动挡。日常开发里我大量使用注解做方法级缓存代码层面几乎不需要关心 Redis 操作细节。首先在主配置类或者启动类上加上EnableCaching然后在 Service 方法上声明缓存行为Cacheable(cacheNames goods, key #id) public Goods getGoodsById(Long id) { // 查询数据库 return goodsMapper.selectById(id); } CachePut(cacheNames goods, key #goods.id) public Goods updateGoods(Goods goods) { goodsMapper.updateById(goods); return goods; } CacheEvict(cacheNames goods, key #id) public void deleteGoods(Long id) { goodsMapper.deleteById(id); }这三个注解的区别Cacheable 先查缓存缓存没有才执行方法并把返回值放缓存CachePut 每次都会执行方法然后把结果更新到缓存适合更新操作CacheEvict 只做缓存删除适合删除操作和失效操作。key 的写法是 SpEL 表达式比如 #id 取方法参数 id 的值#goods.id 取参数对象里的 id 字段。cacheNames 加 key 最终会拼成 Redis 的 key默认是 goods::1001 这样两个冒号分隔的格式。组合使用时要特别小心。如果更新操作只做了数据库更新忘了加 CachePut 或 CacheEvict那缓存里的数据一直是旧的就会产生数据不一致。这个问题在改需求时尤其容易踩到我的习惯是对所有写操作做一次全局检查。3.3 分布式锁一个能用但别得意的实现多实例部署时Java 自身的 synchronized 锁不到别的机器上。最常见的解决办法就是在 Redis 里做分布式锁。基础思路是利用 SET NX EX 的原子语义只有 key 不存在时才设置成功。我的实现模板如下String lockKey lock:stock:1001; String requestId UUID.randomUUID().toString(); Boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(30)); if (Boolean.TRUE.equals(locked)) { try { // 扣减库存等业务逻辑 } finally { // 释放锁 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId); } }两个关键细节必须理解到位。第一value 为什么要用唯一标识比如 UUID。因为如果直接 set 一个固定值任何线程都能删除这把锁会出现线程 A 的锁被线程 B 释放的严重问题。用请求唯一标识后释放前先判断持有者是不是自己防止误删。第二释放锁为什么要用 Lua 脚本。因为“判断值是否相等”和“删除 key”是两个操作如果不放在同一个原子脚本里中间可能被其他线程插进来又出现误删。Lua 脚本保证这两步操作原子执行。这套实现能应付大多数内部系统但它不是满分的分布式锁。比如锁超时后业务还没执行完锁会被自动释放其他线程就能进来导致并发问题。要解决得做锁续期也就是“看门狗”机制每过一段时间自动延长过期时间。这个逻辑自己写比较繁琐如果项目里允许引入额外依赖我会建议直接用 Redisson 的分布式锁它对续期、重入、失败重试都处理得更完善。3.4 过期监听让 Redis 帮你做延迟通知Redis 的 key 过期事件是个很实用的能力。比如订单超时未支付自动取消、定时任务提醒这类场景如果不希望单独起一个后台线程扫描数据库就可以考虑 Redis 过期监听。首先需要在 Redis 服务端开启键空间通知Redis 4.x 支持这个特性。可以在配置里加notify-keyspace-events Ex或者运行时执行命令CONFIG SET notify-keyspace-events Ex然后写一个监听器继承 Spring Data Redis 提供的 KeyExpirationEventMessageListenerComponent public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener { public RedisKeyExpiredListener(RedisConnectionFactory connectionFactory) { super(connectionFactory); } Override public void onMessage(Message message, byte[] pattern) { String expiredKey message.toString(); // 根据 key 前缀做业务处理比如 order:expire:123456 } }这里要注意onMessage 收到的内容只是过期的 keyvalue 已经拿不到了。所以如果需要关联业务数据要把业务信息编进 key 里比如 order:expire:123456。Redis 过期事件有个天然局限它不是严格准时的事件消息也可能因为内部清理机制延迟。实际上 Redis 的过期 key 是被动清理和定期清理结合完成的可能 key 已经到点但事件过一会儿才出来。所以不能把它当作强一致消息系统用。如果业务要求“几分钟内处理”这种宽松时序它很合适如果要求“必须精确处理”还得配合定时扫描兜底。3.5 Pipeline 批量操作把网络开销拉下来批量操作是 Redis 性能优化的重头戏。业务上经常遇到要写几千个 key 的情况如果写一个 for 循环每条命令都走一次网络往返耗时成倍增长。Pipeline 可以把一批命令打包发到 Redis一次往返搞定。Spring Data Redis 里用 executePipelined 实现stringRedisTemplate.executePipelined((RedisCallbackObject) connection - { for (int i 0; i 10000; i) { byte[] key (batch:key: i).getBytes(StandardCharsets.UTF_8); byte[] value (batch:value: i).getBytes(StandardCharsets.UTF_8); connection.stringCommands().set(key, value); } return null; });我实际测下来的感受是本地环境下 1 万次普通 set 和 1 万次 Pipeline 的耗时差距能达到几十倍。网络越慢、命令越多Pipeline 的优势越明显。所以只要是批量初始化缓存、批量写入临时数据我第一反应就是用 Pipeline。Pipeline 有几个注意点如果返回结果不为 nullexecutePipelined 会收集每条命令的返回值记得按顺序对应。并且 Pipeline 中命令是在同一个连接上顺序执行的如果是在 Redis 集群环境key 必须落在同一个 slot否则会报跨 slot 错误。集群模式下想用 Pipeline 批量操作需要先按 slot 分组或者用 hash tag 强制 key 落到同一个槽位。4. 真实项目中的问题排查与避坑手册4.1 连接池报错 Could not get a resource from pool这个报错几乎是 Redis 集成最常见的拦路虎关键信息在后面半句一般会跟着 Connection refused 或者 Read timed out。第一步确认 Redis 到底启动没有。不要猜直接敲命令redis-cli -h 127.0.0.1 -p 6379 -a 123456 ping能看到 PONG 说明服务没问题。连接拒绝要么是端口不对要么是 bind 配置没包含你当前访问的 IP。如果 Redis 只 bind 了 127.0.0.1你从另一台服务器连它一定拒绝。第二步确认密码是否正确。密码错误时 Spring Boot 日志会提示 NOAUTH Authentication required但如果你把异常直接吞了可能只看到连接失败。第三步考虑连接池本身。如果流量很大max-active 设置太小或者 max-wait 设置太短短时间内连接被借完就会出现 Could not get a resource from pool。处理办法不是一味调大连接数而是先看 Redis 服务端的连接数和耗时再结合业务并发量调整。连接池参数我给过一个经验值常规 Web 项目 max-active 20 到 50max-wait 3 秒比较合理。压测时观察到超时再去微调不要一上来就堆几百个连接Redis 本身处理能力有限连接太多反而增加上下文切换开销。4.2 缓存 key 出现乱码怎么办如果你在 Redis 客户端看到 key 长这样\xAC\xED\x00\x05t\x00\x06key:1说明用的还是 JdkSerializationRedisSerializer也就是默认序列化器。解决办法就是我前面给的 RedisConfig把 key 序列化器改成 StringRedisSerializervalue 序列化器改成 GenericJackson2JsonRedisSerializer。改了之后新写入的数据就是可读字符串之前写入的乱码数据只能删掉没法自动解析回来。这里提醒一点如果你的业务里大量使用了 Cacheable 注解而且没配置 RedisCacheManager那缓存 key 和 value 还是可能乱码。很多人只改了 RedisTemplate 忘记改 CacheManager结果 RedisTemplate 操作正常注解缓存还是有问题。这才是真正容易忽略的地方。4.3 LocalDateTime 反序列化失败用 JSON 序列化缓存对象常见问题之一是对象里有 LocalDateTime、LocalDate 这类时间字段。反序列化时报错 java.time.LocalDateTime cannot be cast to... 或者 InvalidDefinitionException。原因是 Jackson 默认没有注册 Java 8 时间模块LocalDateTime 会被序列化成数组或数字格式反序列化时无法自动转回。解决思路是给序列化器配一个自定义 ObjectMapper注册 JavaTimeModuleObjectMapper objectMapper new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(objectMapper);然后把 RedisConfig 里的 value 序列化器换成这个自定义实例。如果还要处理 LocalDate、LocalTime同样在 JavaTimeModule 覆盖范围内。这个坑在 Spring Data Redis 较新版本里有所改善但如果你遇到时间类型问题第一反应就应该是 JavaTimeModule。4.4 缓存穿透、击穿、雪崩的应对思路这三个词是 Redis 缓存面试必问题实战里也确实会遇到。缓存穿透是查一个肯定不存在的数据。缓存没有数据库也没有每个请求都会打到数据库。简单的应对是缓存空值即使查出来是 null也往缓存里写一个空对象或者特殊标记过期时间设短一点比如 30 秒。但我前面配置里写了 disableCachingNullValues两者冲突如果选择缓存空值方案就要把这个配置去掉。缓存击穿是某个热点 key 正好过期大量并发同时去查数据库。可以用互斥锁只让一个线程去查库其他线程等待。另一种思路是逻辑过期把 key 的过期时间放在 value 里不真正设 Redis TTL后台异步刷新。实现复杂度高一点但能避免锁带来的等待。缓存雪崩是大量 key 在同一时间集体过期。解决方式是在设置缓存过期时间时加一个随机值比如 30 到 60 分钟之间随机让过期时间错开。也可以用多级缓存或者加多副本但最简单实用的还是随机过期时间。这三大问题的核心都指向同一个原则别让请求全打到数据库削峰、错峰、直接挡住。4.5 排查和验证缓存的小技巧实际开发中验证 Redis 缓存到底有没有生效我一般先用 SLOWLOG 和 MONITOR。Redis 4.x 的慢查询日志查看方法SLOWLOG GET 10 SLOWLOG RESET慢查询里能看到耗时高的命令能帮你快速定位是不是有某个大 key 或批量操作拖慢了 Redis。MONITOR 命令可以实时打印 Redis 收到的所有命令上线环境下不要随便开影响性能。开发联调时可以用它确认某个请求到底有没有打到 Redis。可视化工具方面我自己的习惯是先用命令行把逻辑全部跑通再打开图形工具观察 key 的变化和 TTL。如果你用的是默认序列化器图形工具里看到一堆二进制那问题就肉眼可见了。5. 一些我后来才想明白的体会最后说一个我自己的习惯。很多人做 Spring Boot 整合 Redis第一步就是抄一个配置类然后开开心心跑通 demo后面遇到问题就全靠搜索引擎。其实整合 Redis 这件事最值得花时间的不是配置而是想清楚你项目里每个场景应该用哪种数据结构、哪种序列化策略、怎样设置过期时间。序列化器选型是我最想让你记住的内容。一个合理的配置能让你从开发到运维都舒服Redis 里的数据清楚可读排查问题时一眼就能看到 key 和 value一个偷懒的默认配置后面要改必须清数据那才是真正难受。Redis 4.x 的 UNLINK、LFU、模块系统这些东西看着不像 Spring Boot 直接相关但它们在系统稳定性、容量规划、扩展能力上都能帮上忙。用熟基础功能之后我建议你回来看这些特性会发现自己能解决的问题又多了一圈。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询