
上周被一位老同事拉去线上看Redis连接超时Spring Boot服务高峰期疯狂报错Redis服务端CPU、内存都正常端口也通客户端就是连不上。翻完他pom里的依赖版本、application.yml里的连接参数、还有自己写的RedisConfig半小时不到基本锁定了问题。这种场景我近几年遇到过太多次所以借今天这个题系统聊一聊Redis在Spring里的配置——从依赖选型、YAML参数、序列化策略到Spring Cache、分布式锁再落到一线排查思路整条链路串起来讲。这篇文章的目标读者有两类。一类是刚把redis-server装好、准备在Spring Boot项目里接入第一个RedisTemplate的新手可以从第二章看起先抄一份能跑的配置再逐步理解参数意义另一类是在生产环境用过Spring Cache、Redis但被乱码、超时、主从切换折磨过的老手重点看第三、四、六章很多坑你大概率也踩过或正在踩。1. 动手配置前先弄清依赖版本和客户端选型1.1 版本矩阵是配置的第一个坑很多人配置完连不上Redis第一反应是host写错或者服务没启动其实依赖版本不对引起的现象同样隐蔽。Spring Boot 2.7及以前Redis配置项前缀是spring.redis.*从Spring Boot 3.0开始这一堆配置整体迁移到了spring.data.redis.*。如果你从2.x升到3.x照着旧文档写spring.redis.host配置文件里的参数全部静默失效——服务能正常启动等到第一次访问Redis才冒出connection refused排查起来非常迷惑。Spring Boot 2.x对应Spring Data Redis 2.xSpring Boot 3.x对应Spring Data Redis 3.x。Spring Data Redis 3.x要求JDK 17Redis服务端版本建议至少6.x因为Redis 6开始引入ACL、RESP3协议这些特性对客户端版本有要求。如果项目还在用很老的lettuce-core版本连Redis 7可能出现协议兼容异常比如UNKNOWN或ERR Protocol error这类报错。我建议的依赖策略很简单直接用Spring Boot父工程管理的版本不要手动指定Spring Data Redis的子版本。手动指定极易导致jar包冲突尤其是在微服务多模块场景下每个模块引一个版本最终行为就可能不一样。下面这张表整理了我常用的版本对应关系仅供参考Spring Boot版本配置前缀Spring Data Redis推荐Redis服务端2.3.x ~ 2.6.xspring.redis.*2.4.x ~ 2.6.x5.x / 6.x2.7.xspring.redis.*2.7.x6.x3.0.x ~ 3.2.xspring.data.redis.*3.0.x ~ 3.2.x6.x / 7.x3.3.xspring.data.redis.*3.3.x6.x / 7.x如果你的项目还在Spring Boot 2.7上且短期内不打算升级那么配spring.redis.*就可以新项目直接用Spring Boot 3.x配spring.data.redis.*。这两个前缀看起来只是多了个data但接线上就是完全不同的两套配置模型一定不要混。1.2 Lettuce还是Jedis两种客户端两种配置思路Spring Boot默认使用Lettuce作为Redis客户端这一点很多老开发者容易忽略。Lettuce基于Netty单连接多线程复用线程安全天然适合异步和响应式场景Jedis则是阻塞式I/O多线程下通常需要依靠连接池分担压力配置参数和侧重点都不一样。在Spring Boot 3.x中如果你想把客户端切换成Jedis需要先从依赖里排除Lettuce再引入Jedisdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId exclusions exclusion groupIdio.lettuce/groupId artifactIdlettuce-core/artifactId /exclusion /exclusions /dependency dependency groupIdredis.clients/groupId artifactIdjedis/artifactId /dependency切换后配置项从spring.data.redis.lettuce.pool.*变成spring.data.redis.jedis.pool.*。我实际经历过一个项目默认Lettuce在高并发下没配连接池瞬时连接数飙升到上千Redis服务端文件描述符告警。后来在配置里显式加了连接池同样业务量下连接数被控制在几十问题立刻缓解。这里的关键认知是Lettuce虽然是多路复用但在Spring Boot里没有显式开启连接池时行为未必是你想要的尤其在高并发场景下连接数管理依然要靠池化参数约束。1.3 单机、哨兵、Cluster三种部署三种配置路径Redis部署模式决定了配置结构完全不同。单机模式最简单spring: data: redis: host: 127.0.0.1 port: 6379 database: 0哨兵模式要配置master节点名称和哨兵节点列表spring: data: redis: sentinel: master: mymaster nodes: 10.0.0.11:26379,10.0.0.12:26379,10.0.0.13:26379Cluster模式则要配置集群节点列表spring: data: redis: cluster: nodes: 10.0.0.21:6379,10.0.0.22:6379,10.0.0.23:6379 # 最大重定向次数 max-redirects: 5一个很容易踩的坑是有些人在配置里同时写了host/port和cluster.nodes客户端会优先走单机模式集群节点地址完全不生效导致连上某台节点后访问其他slot时报MOVED重定向。所以部署模式要认真确定一个项目同一时间只使用一种模式配置文件里不要多套参数混写。2. 配置文件里的门道从application.yml到敏感信息2.1 一组能直接抄的Spring Boot 3.x配置模板先给一份我目前在Spring Boot 3.x项目里使用的配置模板可以直接复制去改spring: data: redis: host: 127.0.0.1 port: 6379 password: ${REDIS_PASSWORD:} database: 0 timeout: 3s connect-timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 4 max-wait: 1s time-between-eviction-runs: 60s shutdown-timeout: 200ms单独说明几个参数的含义方便你按需调整timeout读取/写入操作的超时时间单位可以是3s或3000ms。这里指的是单个命令执行的超时不是连接超时。connect-timeout建立TCP连接的超时。两者要分开设置因为连接超时往往意味着网络不通命令超时则说明Redis处理慢或连接池耗尽。databaseRedis逻辑库索引默认0。多个业务共享同一个Redis实例时适当使用不同database做隔离但注意Cluster模式不支持多个database只能使用db0。lettuce.pool.max-active最大活跃连接数默认是8。生产环境这个小了会非常容易触发超时。lettuce.pool.max-wait当连接池耗尽时最大的等待时间。默认-1表示无限等待生产环境建议设置1s左右宁可快速失败做降级也不要让线程全部挂起。2.2 连接池参数为什么默认值在生产环境不够用Spring Boot默认情况下Lettuce连接池如果没有显式配置spring.data.redis.lettuce.pool.*默认max-active8、max-idle8、min-idle0。对纯IO密集的Redis访问8个连接在大多数情况下是够用的因为Lettuce一个连接可以并发处理多个命令。但一旦业务里有同步阻塞调用、大Key操作、或者高频的BLPOP之类的命令单个连接的处理时长会被拖长可用连接数迅速见底。我处理过一个线上事故一个Java服务线程池200个并发线程连接池max-active8max-wait用的是默认-1。高峰期200个线程全部卡在等待连接上接口RT从50ms涨到5s最后整个线程池被打满服务不可用。改法不复杂把max-active调到50max-wait设置成800ms超时后走降级系统立刻恢复。这里真正的问题是很多人不知道默认max-wait-1意味着无限等待等于把故障从Redis层传导到了业务线程池。还有个容易忽略的参数是time-between-eviction-runs它控制空闲连接检测周期。在连接池使用不均衡的情况下长时间空闲的连接可能被服务端断掉客户端又没感知下次请求时才发现连接失效。设置60s的检测周期基本够用。2.3 多环境切换与密码安全多环境配置一般是靠spring.profiles.active配合多个文件切分比如application-dev.yml和application-prod.yml。但要注意如果两边都写spring.data.redis.*那么启动时加载的就是当前profile对应文件里的值没写的那部分会使用默认值。这个机制很多新手容易踩坑比如dev环境没有配密码prod环境漏配了密码字段导致生产连接直接认证失败。密码和敏感信息尽量不要硬编码在配置文件里。最基础的做法是引用环境变量spring: data: redis: password: ${REDIS_PASSWORD:}如果你的环境变量没设置就回退成空密码。稍微进阶一点可以接入jasypt-spring-boot做加密或者把敏感配置放到配置中心统一管理。这里额外提醒Redis本身没有传输加密能力明文密码走网络在生产环境存在风险有条件时建议启用TLS或者在网络层做隔离。3. RedisTemplate序列化默认配置是乱码根源3.1 默认JDK序列化的三个坑Spring Boot的RedisTemplate默认使用JdkSerializationRedisSerializer也就是说你往Redis里存一个对象它会先用Java序列化成二进制字节流再写进Redis。这个默认选型带来了几个实际问题第一是可读性差。用redis-cli或者Redis Desktop Manager打开看到的是\xAC\xED\x00\x05t...这类二进制乱码排错时根本没法直接看出key和value内容。第二是跨语言兼容性差。你的Java服务写入的数据Python或Node服务根本读不出来。在一个微服务架构里经常有多个语言的服务共享Redis缓存JDK序列化的数据对非Java服务就是一堆无意义字节反序列化直接报错。第三是安全问题。Java原生反序列化如果遇到恶意构造的字节流存在反序列化漏洞注入的风险。虽然Redis内部网络通常是可信的但对暴露在复杂网络环境下的服务来说这个问题不应该被忽视。3.2 一套经过生产验证的序列化组合我的做法是key使用StringSerializervalue使用GenericJackson2JsonRedisSerializerhash结构的key和value同样分别指定。这样配置出来的Redis里key是人可读的字符串value是JSON跨语言读取毫无压力也方便运维排查。Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }注意GenericJackson2JsonRedisSerializer会在JSON里写入class元数据用来标记反序列化时的目标类型。这个元数据是黑名单式的而不是白名单式的所以如果你对安全要求极高可以自己继承并扩展一个带类型白名单的序列化器只允许反序列化特定包下的类。另外如果你的项目里大量使用LocalDateTime、LocalDate这类Java时间类型直接使用上面的序列化器反序列化时很可能会报InvalidDefinitionException。这时候需要自定义ObjectMapper注册JavaTimeModule并关闭WRITE_DATES_AS_TIMESTAMPObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMP); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(mapper);这块属于序列化配置里最容易忽略的细节建议在项目初始化时就加上免得上线后出现LocalDateTime缓存反序列化失败再手忙脚乱。3.3 StringRedisTemplate和RedisTemplate的边界Spring Boot里内置了一个StringRedisTemplate它是RedisTemplateString, String的特例key和value都使用StringSerializer。很多项目里两种template同时存在StringRedisTemplate用来存简单的字符串RedisTemplate被改成JSON处理对象。这个设计本身没问题但有一个坑同一个key用两种template读写时序列化结果不一致数据就乱了。举个例子你用StringRedisTemplate写了一个user:100value是普通字符串然后改用自定义的RedisTemplate去读同一个key因为RedisTemplate里的valueSerializer是JSON它会尝试把字符串按JSON反序列化轻则读到格式意外的结果重则直接抛异常。反过来的情况也一样用自定义RedisTemplate写入的JSON字符串用StringRedisTemplate读出来虽然能看到字符串但已经不是原始业务数据了。所以我的建议是项目里指定一个统一的RedisTemplate作为唯一入口另一个只在极简单的场景使用。如果非要共存也要在命名上明确区分比如自定义Bean名不要叫redisTemplate而是叫objectRedisTemplate给团队一个清晰的警示。4. Spring Cache缓存的配置细节与陷阱4.1 从EnableCaching到RedisCacheManagerSpring Cache本身是一套缓存抽象底层接入Redis需要RedisCacheManager。使用时首先在配置类上打EnableCaching然后声明RedisCacheManager的Bean。Configuration EnableCaching public class CacheConfig { Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .computePrefixWith(cacheName - cacheName ::); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); } }这段配置里最容易漏掉的是serializeValuesWith。如果不显式设置Spring Cache存进Redis的数据同样会用JDK序列化你会在Redis里看到\xAC\xED开头的乱码和上一章说的问题一模一样。很多人在业务中使用Cacheable一切正常但从来没打开过Redis客户端看过直到某个需求要求跨服务读取该缓存时才发现存了一堆二进制。computePrefixWith也很好用。它决定了最终key的格式这里设置后Cacheable(cacheNames user, key #userId)实际写入Redis的key就是user::123可读性非常好也方便后续手工清理。4.2 TTL与Key前缀在配置层的统一规划Spring Cache允许对不同的缓存区域配不同的TTL做法是把默认配置和带名称的配置分开设置RedisCacheManager builder RedisCacheManager.builder(factory) .cacheDefaults(defaultConfig) .withCacheConfiguration(userCache, RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(5))) .withCacheConfiguration(productCache, RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1))) .build();这种按业务维度拆分TTL的方式我在实战中非常推荐。一个常见的错误是全项目只用一个统一TTL比如30分钟结果有些高频数据频繁失效打穿到数据库有些低效数据却长期占用内存。规划缓存时先想清楚这组数据多久需要更新一次再配置对应的TTL。还要留意key冲突问题。如果你的系统有多个服务或模块都使用Cacheable缓存名都叫common但业务逻辑完全不同那么Redis里的key前缀会重叠相互覆盖。这时候缓存名设计就很重要建议按模块起名比如serviceA:common和serviceB:common再配合computePrefixWith自动生成隔离前缀。4.3 穿透、击穿、雪崩该不该由配置层扛缓存穿透、击穿、雪崩这三个老生常谈的问题有一部分能在配置层或代码层面缓解。穿透是指查询一个不存在的key每次请求都落库。配置层能做的很有限因为Spring Cache默认是不缓存null值的。如果你确实想缓存空结果要么自己封装一层CacheManager把value封装成含有空标记的对象要么在业务代码里用RedisTemplate手动写空值占位并设置一个较短的TTL比如60秒。击穿是指某个热点key过期的瞬间大量请求同时打到数据库。配置上能用的办法不多但Cacheable有个sync true属性开启后会对同一个key做线程级别的互斥避免缓存重建时并发穿透到数据库。雪崩是指大量key在同一时间集体过期数据库瞬间被打满。配置层的缓解方案是给TTL加随机偏移不要把所有key的过期时间设成全等常量。比如固定TTL为10分钟实际设置时加一个0~60秒的随机值这样过期时间分散开压力就不会形成脉冲。这里需要说点实在话配置层能缓解的是面上的问题真正的热点key、大key治理还是要靠业务侧拆缓存、拆key甚至人工预热。别指望配置一个合理TTL就万事大吉。4.4 别把Spring三级缓存和Spring Cache混在一谈聊到Spring缓存经常有朋友把Spring容器里的三级缓存和Spring Cache搞混。这里明确区分一下Spring的三级缓存解决的是单例Bean创建过程中的循环依赖问题它在IoC容器内部和Redis毫无关系本文说的Spring Cache是一套基于注解的缓存抽象底层可以换成Redis、Caffeine、Ehcache等各种实现。我在面试候选人的时候发现很多人一听到Spring三级缓存就直接说Redis的缓存设计这是两码事。如果你的项目里同时用到Spring的三级缓存机制为了解决循环依赖比如A依赖B、B依赖A和Spring Cache比如Cacheable它们井水不犯河水。理解清楚这一点排查问题时才能定位到正确的层。5. 分布式锁Redis配置从能跑到扛得住5.1 锁场景对连接池和超时的苛刻要求在Spring配置层面分布式锁和普通缓存业务对连接的要求完全不同。缓存读多写少偶尔超时还能容忍快速失败分布式锁则要求在极短时间内获取连接并完成命令一旦等待连接超时意味着整个临界区逻辑没有拿到锁业务可能直接失败或重试。所以我给锁场景单独建了一套连接配置逻辑。比如在同一个服务里缓存用的max-active16锁操作我希望至少有独立连接池或者在主连接池里留出足够余量。理想情况是给锁单独创建RedisConnectionFactory不要让锁操作和缓存操作争抢同一批连接。锁命令本身也要设置合理超时。使用Lettuce时命令超时受spring.data.redis.timeout控制如果你把它设成5s而连接池又在满负荷状态那么等待执行的时间很容易把业务拖垮。我通常会把锁操作相关的连接池max-wait设成300~500ms超时立即放弃用重试或幂等补偿来保证最终一致性。5.2 Redisson与Spring Data Redis双实例的取舍谈到Redis分布式锁绕不开Redisson。Redisson提供RLock实现了可重入锁、看门狗自动续期这些能力比手动用SETNX EXPIRE可靠得多。如果你已经在用Spring Data Redis又想引入Redisson建议把两者当作独立的Redis客户端实例来看待不要复用同一个RedisConnectionFactory。Redisson配置方式很简单先引入依赖dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.2/version /dependency然后配置一个独立的客户端BeanConfiguration public class RedissonConfig { Bean(destroyMethod shutdown) public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(xxx) .setDatabase(0) .setConnectionPoolSize(8) .setConnectionMinimumIdleSize(4); return Redisson.create(config); } }如果你的Redis启用了ACLRedisson的配置里也要对应写用户名和密码。我遇到过一种情况同一个Redis实例Spring Data Redis连接正常Redisson连接却报认证失败。排查后发现是因为Redis启用了ACL默认用户两者使用的username不一致。Redisson和Spring Data Redis双实例在业务上各司其职Spring Data Redis管缓存和数据访问Redisson管分布式锁。虽然看起来运行在同一套Redis上但连接管理、心跳、线程模型完全隔离反而降低互相影响的风险。6. 配置问题排查实录五个线上事故的根因复盘6.1 连接超时根因是连接池被占满不是Redis慢最常遇到的线上问题就是连接超时。我在文章开头提到的那个案例最终原因就是连接池配置不合理。具体现象是服务进程还在但大量线程阻塞在获取Redis连接上Redis服务端慢日志很少CPU也不高客户端日志统一的报错是“Redis command timed out”。排查链路是这样的先看Redis服务端INFO命令的连接数和慢日志排除服务端本身慢。再看Spring Boot客户端日志有没有连接池耗尽相关的warning。用jstack抓线程栈看线程卡在什么方法上。如果是卡在LettucePool的等待获取连接处十有八九是max-active太小或max-wait设置成无限等待。调整参数后压测验证观察连接数和RT曲线。这个案例让我养成一个习惯任何Spring Data Redis配置里max-active和max-wait必须是显式写出的不能依赖默认值。6.2 缓存乱码引发跨服务反序列化失败有一次线上告警是支付回调事件里的一个Python服务读不到Java服务写入的Redis缓存直接json解析报错。我打开Redis客户端一看key存在value是一串\xAC\xED开头的二进制。原因就是Java服务用的默认JDK序列化。解决这类问题需要分两步先统一序列化策略改成JSON方案然后处理存量数据。线上Redis里的旧二进制数据不会自动消失所以我通常会做一个迁移策略代码写的时候双写临时兼容期读的时候优先读新格式读到旧格式或者解析失败时主动删除并触发回源。这样灰度切换期间不会出现大面积缓存击穿。6.3 主从切换后配置漂移哨兵节点列表忘写了在哨兵模式下配置只写了spring.data.redis.sentinel.master却漏了nodes节点列表。Redis主从切换后客户端还能连上哨兵返回的新master吗如果没配节点客户端根本不知道去哪找哨兵自然也无法感知master变化服务一直访问旧master地址直到连接超时。正确的做法是把哨兵所有节点都写上并且确保Lettuce的拓扑刷新在较新版本中默认开启。集群模式同理cluster.nodes要写全不能只写一个节点地址否则节点变更后客户端冷启动会发现拓扑信息不完整。Redis主从切换这类问题在配置阶段就很容易避免关键是不要为了省事只写一个哨兵地址。6.4 同一把Key混用多种RedisTemplate导致读写错乱这个坑在团队协作项目里特别常见。一个团队里A同学用StringRedisTemplate存了字符串B同学在另一个服务里用自定义RedisTemplate直接读同一个key结果自然是不兼容。因为两者序列化后的字节完全不同读出来要么是null要么是解析异常。定位这种问题最快的办法是打开Redis可视化客户端对比同一key下数据的字节格式。如果发现某些key是可见字符串某些key是二进制乱码基本可以确认是序列化器使用不一致。规范上我建议在项目里写好Redis操作工具类或统一封装层所有读写都走这个入口不要在业务代码里直接注入多种Template。6.5 毫无监控指标问题排查全凭猜最后这个不算配置事故但比配置事故更致命很多项目Redis相关监控只靠告警平台报连接超时除此之外没有任何指标。Redis用满没满、慢命令有多少、连接池等待时间多长全都不清楚出了问题只能逐层翻日志。建议从配置阶段就把监控底座搭好。Redis侧开启慢日志slowlog-log-slower-than设为10000微秒10ms再配合第三方监控组件采集慢命令Spring Boot侧把Actuator的Redis健康检查打开记录连接池活跃数、空闲数、待处理任务数。这些指标一旦齐全大部分问题在爆发前就能观察到拐点。日常运维工具方面Redis Desktop Manager合适看数据方便生产环境也可以用命令行的redis-cli --stat做快速观察但监控别依赖GUI自动化指标采集才是正路。说到底Redis在Spring里的配置不是一个yml文件就完事的事它横跨版本选型、客户端模式、序列化策略、缓存抽象、分布式锁和监控治理。把这些环节想清楚服务在线上跑得稳不稳就心里有数了。