如果你也在某个Spring Boot 3.x项目里看到过这么一行日志,而且网上搜到的配置教程试了一圈还是连不上,那这篇文章应该能帮到你。先说背景:我这边一个内部系统最近从Spring Boot 2.7升级到3.2,启动看着一切正常,结果第一个请求落到带缓存的方法上就直接抛Unable to connect to Redis,愣是折腾了一个下午才把问题定位干净。后来陆陆续续又碰到连接池、ACL、序列化相关的连环坑,干脆把整个过程整理成一份排错记录,从最基础的连通性检查一路写到Redis 6.0之后才有的认证机制,代码、命令、配置都放在这里,你照着顺序过一遍,基本能省下几个小时的排查时间。
1. 问题现场:异常栈里藏着最重要的一条线索
1.1 报错信息怎么读
Spring Boot 3.x连接Redis失败时,最典型的异常长这样:
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379第一眼看到Unable to connect to Redis,大部分人会直接去检查Spring配置,但如果仔细看下一行,会发现一个关键信息:它连接的是localhost:6379。如果你的Redis实际部署在别的机器上,这里基本就说明配置没生效,或者根本就没读到你的配置。
还有一类堆栈更“完整”一些:
Caused by: java.net.ConnectException: Connection refused: /192.168.1.100:6379Connection refused和Connection timed out这两个关键词非常值得区分。前者表示TCP连接被目标端口直接拒绝,一般意味着Redis进程没起来、端口没监听、或者Redis监听的地址不是客户端访问的地址;后者表示数据包发出去之后石沉大海,通常是防火墙、安全组、网络ACL把请求拦掉了。这俩差一个字,排查方向完全是两条路。
1.2 启动正常不代表Redis连接正常
很多人在这一步会有一个误解:应用能够正常启动,说明Redis没问题。实际上Spring Boot的RedisConnectionFactory在启动阶段默认不会主动去建连,Lettuce底层是懒加载的,真正触发连接发生在第一次执行Redis命令时。所以你会看到两个典型场景:
- 启动日志干干净净,第一次调用带缓存的方法时突然抛异常;
- 启动阶段就报错,那通常是
RedisCacheManager或SessionRepository这类组件在初始化时做了连接验证。
如果你遇到的是第一种,恭喜,问题范围已经缩小到“应用运行期的连接路径配置”,先不用怀疑Redis服务本身挂掉。我遇到过最尴尬的一次,是项目里根本没有引入spring-boot-starter-data-redis,代码里有个同事自己用Jedis写了一套工具类,配置写在application.yml里但自定义类根本没读取,结果也是报类似的错。排查之前先确认依赖、确认配置归属,比瞎改配置有性价比得多。
2. 第一轮排查:别先改代码,先确认Redis本身活着
2.1 redis-cli三步检查法
在动Spring配置之前,先用Redis自带的客户端做一次健康检查,这是最快、最不容易出错的操作。
redis-cli -h 127.0.0.1 -p 6379 ping如果返回PONG,说明Redis进程活着、端口正常、认证环境允许访问。如果返回NOAUTH Authentication required,说明Redis设置了密码,但你还没认证。如果输出Could not connect to Redis,基本就是网络、端口或服务本身的问题。
检查完ping,再看两个容易被忽略的细节。一个是端口监听的地址:
ss -lntp | grep 6379正常输出应该是:
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:*这里如果只监听127.0.0.1,远程机器无论如何都连不上,这就要看下面的bind配置了。另一个是看Redis版本和运行时长:
redis-cli info server | grep redis_version有些云上的Redis实例版本很老,行为跟新版本差别很大,确认版本能帮你提前预判一些问题。
2.2 bind、protected-mode、防火墙一起看
Redis默认配置里有两个参数很容易让新手“死都不知道怎么死的”:bind和protected-mode。
redis-cli config get bind redis-cli config get protected-modebind 127.0.0.1表示Redis只接受本机回环连接,远程客户端即使网络通,也会直接被拒。protected-mode在Redis 3.2之后默认开启,作用是没有配置密码且bind没有显式指定时,只允许本机连接,相当于给裸奔的Redis兜了一层底。两个参数一起生效时,你会发现从应用服务器用redis-cli根本连不上,但在Redis本机却能ping通,这时候就别怀疑Spring配置了,先去改Redis的配置。
改bind的方式有两种:直接改redis.conf里的bind 0.0.0.0或者指定网段,然后重启;也可以运行时用CONFIG SET临时生效,但重启会丢,建议还是改配置文件和requirepass一起做。
bind 0.0.0.0 protected-mode no requirepass YourStrongPassword这里必须强调:生产环境千万别只改bind不设密码,Redis的6379端口在公网上被扫是常态,裸奔的Redis基本等于把服务器钥匙送给别人。设了requirepass之后,客户端的连接方式也要跟着变,否则就会碰到后面第5章要讲的认证报错。
2.3 Docker与容器场景里的特殊坑
现在跑Redis用Docker太常见了,但容器化带来了一套全新的连接问题。最典型的是用默认方式启动容器:
docker run -d --name redis -p 6379:6379 redis:7.2启动后本机用redis-cli连发现连不上,或者应用在另一个容器里连不上。常见的原因有几个:
第一,容器内的Redis默认配置仍然有bind 127.0.0.1,即使宿主机端口映射了6379,数据包到达容器内部后落在回环地址上,服务没监听容器网卡,连接照样失败。这种情况要给Redis容器传配置,比如挂载自定义redis.conf,把bind改成0.0.0.0,或者用--network host让容器直接用宿主机网络栈。
第二,Spring Boot应用如果也跑在Docker里,配置里写localhost指向的是应用容器自己,不是Redis容器。正确的做法是在docker-compose里用服务名互相访问:
services: app: image: your-app:latest environment: SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_PORT: 6379 redis: image: redis:7.2在容器编排环境里,服务发现靠的是服务名,不是IP,也不是localhost。这个坑几乎人人都会踩一次,而且报错信息看起来和本机环境完全一样,特别有迷惑性。
3. 第二轮排查:Spring Boot 3.x的Redis配置前缀坑
3.1 spring.data.redis还是spring.redis
如果说前面那些是“环境问题”,那接下来这个就是纯纯的“版本迁移问题”。Spring Boot 2.x早期版本里,Redis配置前缀是:
spring: redis: host: 127.0.0.1到了Spring Boot 3.x,配置前缀迁移成了:
spring: data: redis: host: 127.0.0.1只差一个data层级,但你要是沿用旧写法,Spring Boot不会报错,只会静默忽略这些配置项。后果就是应用仍然拿着默认的localhost:6379去连Redis,如果你本机恰好装了Redis可能碰巧能连上,一旦部署到别的环境,直接就是Unable to connect。
这个坑在升级项目时出现频率极高,我和同事一开始都没往这个方向想,因为配置看着“明明写了”,但其实就是没被读取。排查这类问题,最直接的办法是看Spring Boot的配置绑定日志,或者在Actuator里查实际生效的配置值。
3.2 配置到底生效没有:用actuator验证
如果项目引入了spring-boot-starter-actuator,可以通过env端点直接查看运行时真正绑定的Redis配置:
curl http://localhost:8080/actuator/env | jq '.propertySources[] | select(.name | contains("redis"))'响应里能看到spring.data.redis.host、spring.data.redis.port这些属性的实际值。如果发现你写的是spring.redis.host而这里没有对应项,那就实锤了配置前缀写错。
没有引入Actuator也没关系,把日志级别调低,让Lettuce自己把连接信息打出来:
logging: level: io.lettuce: DEBUG org.springframework.data.redis: DEBUG启动日志里会出现类似Connecting to Redis at localhost:6379这样的输出,一眼就能看出它到底在连谁。发现问题后,把前缀改过来,配置立刻生效。
3.3 一份可以直接抄的完整配置
排查到这里,如果确认环境和配置都没有问题,参考下面这份配置基本覆盖了绝大多数单机场景:
spring: data: redis: host: 192.168.1.100 port: 6379 password: YourPassword database: 0 timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 3000ms几个参数逐一说一下:
host/port不多解释,注意别写localhost,除非Redis确实装在本机;password建议通过环境变量注入,${REDIS_PASSWORD}这种形式,不要把真实密码提交到代码仓库;database默认是0,如果Redis服务端没开启足够的db数,配置过大会报DB index is out of range;timeout表示命令执行超时时间,默认是60秒,生产环境建议设成2~3秒,避免一条慢命令把线程卡死;lettuce.pool.*是连接池参数,前提是classpath里有commons-pool2依赖,没有这个依赖,这些配置是不会生效的,下一章细说。
4. 第三轮排查:Lettuce连接池和超时
4.1 连接池开了吗:commons-pool2是前提
Spring Boot 3.x默认的Redis客户端是Lettuce,不是Jedis,这一点很多从2.x时代过来的人容易搞混。Lettuce基于Netty,底层是非阻塞模型,一个连接可以同时承载多个请求,所以它不像Jedis那样“一个连接同时只能跑一个命令”。
但连接池仍然有必要,尤其是高并发场景,池化可以在峰值时复用连接、控制总连接数。Spring Boot的spring.data.redis.lettuce.pool.*这套配置要生效,必须满足一个隐藏条件:项目里要有commons-pool2依赖。如果你没加这个依赖,配置里写再多max-active也没用,Lettuce跑的是内部直连模式。
所以先检查依赖:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>加上之后重启,会发现连接池配置真正起作用了。Lettuce默认的池参数是:max-active=8、max-idle=8、min-idle=0、max-wait=-1ms。max-wait=-1意味着获取连接时无限等待,这在Redis长时间不返回时会把业务线程全部挂住。
4.2 max-active怎么算:别拍脑袋
连接池设多大,很多人直接抄网上的“16”或者“32”,其实应该根据业务流量简单估算一下。先明确一个概念:一个连接能扛多少QPS,取决于单次Redis操作的耗时。举个例子,业务里每次请求做两次Redis操作,一次GET一次SET,平均单次耗时1ms,那么一个连接每秒能处理约500次单操作,对应250次业务请求。
如果你系统的峰值QPS是1000,理论上4个连接就够了,但考虑到连接重建、GC停顿、Redis本身抖动,留出充足余量是必要的。我一般按“峰值QPS/单连接可承载QPS × 3”来估,比如1000 QPS、单连接承载250 QPS,结果就是12,所以设置成16比较稳妥。反过来,如果设得过大,比如128,线上Redis的连接数会无谓增加,Redis默认maxclients是10000通常不担心,但连接过多会占用文件句柄和内存。
碰到下面这个异常,基本就是连接池被榨干了:
org.springframework.data.redis.connection.PoolException: Could not get a resource from the pool Caused by: java.util.NoSuchElementException: Pool exhausted看到Pool exhausted,先不要急着调大max-active,要看Redis侧是不是有慢查询拖住了连接。用redis-cli slowlog get 10查一下慢命令,如果真有大KEYS、大集合操作,调大连接池治标不治本。
4.3 timeout含义要分清
spring.data.redis.timeout这个参数经常被误解,它不是“连接超时”,而是Lettuce执行命令的超时时间。如果设置得太短,比如100ms,Redis一慢就会报:
io.lettuce.core.RedisCommandTimeoutException: Command timed out after 100 ms这个报错和Unable to connect长得不太一样,但很多人会把它们混为一谈。如果你的业务本身有耗时较长的Redis操作,比如批量写入、大对象缓存,超时设置就得宽松一些。我的建议是先设3秒,观察线上慢命令情况再做调整。
连接超时在Lettuce里也有独立参数,但Spring Boot的自动配置没有直接暴露,如果你需要精细控制,可以在自定义LettuceClientConfiguration里设置commandTimeout和shutdownTimeout。不过对大多数项目来说,timeout: 3s够用了。
5. 第四轮排查:ACL、密码与权限
5.1 Redis 6.0之后认证方式变了
如果你的Redis是6.0以上版本,恭喜你已经进入ACL(Access Control List)时代。过去Redis的认证只有一个全局密码,大家统一用。现在的ACL更像Linux的用户体系:每个用户有用户名、密码,还要授予能操作哪些key、能执行哪些命令。
这意味着连接Redis时,不仅要关心密码,还要关心用户名。Spring Boot 3.x的配置里有对应的username字段:
spring: data: redis: username: default password: YourPassword如果没配username,Lettuce默认用default用户去认证。default用户相当于Linux的root,Redis刚装好的时候它是唯一用户,你设置的requirepass实际上就是给default用户设密码。
5.2 认证报错一表区分
常见认证报错就那么几条,我整理成了一个表:
| 报错关键字 | 含义 | 修复方向 |
|---|---|---|
NOAUTH Authentication required | 服务端要求认证,客户端没带密码 | 配置里补上password |
ERR Client sent AUTH, but no password is set | 客户端带了密码,服务端根本没设密码 | 要么删配置里的password,要么在Redis设requirepass |
WRONGPASS invalid username-password pair | 用户名或密码不匹配 | 检查username和password,用ACL LIST核对 |
NOPERM this user has no permissions | 用户存在但没权限执行命令 | 给该用户授权对应命令和key范围 |
第2条特别坑,尤其在新装Redis的机器上:客户端配置里残留了密码,服务端反倒没设置密码,Lettuce启动后发现发AUTH命令被拒。这种通常改一下配置就行,别被“Unable to connect”带偏。
5.3 NOPERM和ACL授权实战
生产环境里用default用户跑业务风险很大,尤其是权限收紧的团队,会在Redis里专门建一个业务用户:
ACL SETUSER appuser on >App@2024 ~cache:* +get +set +del +ping +ttl这条命令创建了一个名为appuser的用户,密码是App@2024,只能访问cache:开头的key,只能执行get/set/del/ping/ttl这些命令。限制很严,对业务来说其实是好事,但代价是代码里如果用了没授权的命令或key,就会看到NOPERM。
比如业务代码里有个Redis操作用了keys模式,或者往temp:前缀的key里写数据,直接就被拦:
ERR NOPERM this user has no permissions to run the 'keys' command or access this key遇到这类报错,先看权限设计是否合理,如果是业务确实需要,就让管理员扩权,不要一味放开所有权限。ACL的粒度很细,可以精确到命令、key前缀,这点用好了比一个万能密码安全得多。
6. 连接成功后的那些“假连接问题”
6.1 序列化ClassCastException被误认为连接问题
连接问题排查完之后,你以为可以下班了,其实还早。Redis连上之后还有一大批“假连接问题”,它们堆栈里也出现Redis相关的类,但根因根本不是网络。
最典型的是反序列化报错:
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.User原因很简单:Spring Boot默认的RedisTemplate用的是JdkSerializationRedisSerializer,存进去的是Java序列化的二进制。如果你后来改成JSON序列化,或者从Redis里读出一个没有类型信息的对象,Jackson不知道怎么还原成User,就会返回LinkedHashMap,强转时炸掉。
解决方案是自定义RedisTemplate,用GenericJackson2JsonRedisSerializer替代默认序列化器:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } }注意,GenericJackson2JsonRedisSerializer会在JSON里写入@class类型信息,反序列化时才能还原成原来的对象。但这也带来了安全问题——如果反序列化的数据来源不可信,可能造成漏洞。生产环境建议升级到新版本Jackson并配置类型白名单,或者只对可信数据使用这个序列化器。
6.2 缓存注解在第一次调用才触发连接
另一个容易误判的场景是@Cacheable。很多人以为服务启动成功、Redis连接正常,结果第一个请求调用了带缓存的接口,突然报Unable to connect。
这是因为Spring Cache的CacheInterceptor在方法第一次被调用时才去访问Redis,启动阶段不会触发。表面上看是“运行中崩了”,实际是缓存组件从未连上Redis。排查这个场景,可以先单独触发一下缓存方法的调用,或者直接在配置里把spring.cache.redis.enable-statistics打开,用指标观察缓存访问情况。
6.3 云Redis、TLS、端口改写的特殊场景
如果你的Redis是云厂商提供的托管实例,还有几个特殊点要留意。
第一,很多云Redis不允许从公网直接连接,或者只允许VPC内网访问,应用和Redis必须在同一网络环境,否则表现就是Connection timed out,不是连接被拒。这时候先检查安全组、白名单、网络ACL,别在Spring配置上花时间。
第二,部分云Redis会开启TLS加密,连接方式变成rediss://,Spring Boot里要显式开启:
spring: data: redis: ssl: enabled: true不开TLS直接连,报错往往是Connection closed或者Unable to connect,比较难判断,可以用redis-cli --tls手动测试。
第三,有些云Redis的端口不是6379,而是随机分配的高位端口,比如6379只是默认展示,实际连接要填实例详情页上的连接地址和端口。很多人在这一步把自己写死在6379上,反复检查网络没有结果,最后发现端口压根不对。
7. 报错速查表与我的排查顺序
7.1 一表看懂常见报错
我把上面提到的报错全部汇总成一张表,建议直接截图存下来,下次遇到直接对照:
| 报错关键字 | 大概率原因 | 处理方向 |
|---|---|---|
Connection refused | Redis未启动/端口不对/bind限制 | 在Redis机器上执行redis-cli ping,检查端口监听 |
Connection timed out | 防火墙/安全组/跨网段 | telnet测端口,查云安全组和本地防火墙 |
Unable to connect to localhost:6379 | 配置没生效或写错前缀 | 检查spring.data.redis前缀,用Actuator确认绑定值 |
NOAUTH Authentication required | 客户端没带密码 | 配置里补password |
Client sent AUTH, but no password is set | 客户端有密码,服务端没有 | 删配置里password或给Redis设密码 |
WRONGPASS invalid username-password pair | 用户名或密码错误 | 核对username/password,用ACL LIST检查 |
DB index is out of range | database超过实例db数量 | 改回0或有效db编号 |
Could not get a resource from the pool | 连接池耗尽 | 查慢查询,评估后调max-active |
RedisCommandTimeoutException | 命令执行超时 | 优化慢命令,适当调大timeout |
Too many open files | 进程文件句柄耗尽 | 调大ulimit -n,减少无效连接 |
NOPERM | ACL权限不足 | 检查用户授权,补命令和key权限 |
ClassCastException: LinkedHashMap | 序列化方式不匹配 | 检查RedisTemplate序列化器 |
MOVED | 集群节点路由异常 | 确认客户端使用集群模式并连接全节点 |
7.2 排查顺序清单(建议直接抄走)
排错时最忌讳东一锤子西一棒子,我自己总结了一套固定顺序,按这个走基本十分钟内能定位:
- 在Redis服务器本机执行
redis-cli ping,排除服务本身问题; - 检查
bind和protected-mode,确认Redis允许远程访问; - 在应用服务器执行
telnet <redis-ip> 6379或nc -vz <redis-ip> 6379,验证网络通不通; - 检查Spring配置前缀是不是
spring.data.redis,必要时用Actuator查实际绑定值; - 把日志级别调到DEBUG,确认Lettuce实际连接地址;
- 确认classpath里有没有
commons-pool2,连接池参数有没有生效; - 查
redis-cli info clients和slowlog get 10,排除连接数爆满和慢查询; - 最后再根据异常堆栈里的具体报错对表找答案。
前6条按顺序做完,80%的Unable to connect都能收敛。剩下的就是各种权限、序列化、集群路由问题,对着第7章的速查表处理即可。
我个人在实际排错过程中最大的感受是:Spring Boot 3.x报Redis连接错误时,大多数情况下问题根本不在Spring Boot本身,而在连接链路上的某个环节变了。2.x时代的配置前缀、无密码裸奔、Jedis阻塞模型,到3.x加上Redis 6之后的ACL,每一样都在变化。与其去背报错信息,不如把这条连接路径上的每个节点都验证一遍。建议大家在项目里把Redis配置集中管理,用profile隔离环境,密码走环境变量,再在CI里加一个连接冒烟测试,下次再看到这行日志,就能像我一样直接按清单走,不用再陪它熬夜了。