Spring Cloud服务连不上Redis和数据库?整套排查思路与实战复盘
Spring Cloud 服务突然连不上 Redis 和数据库这套排查思路送给你前两周我这边一套 Spring Cloud 微服务在下午高峰期突然开始连环报错日志里同时出现 Redis 连接失败、MyBatisSystemException、Failed to obtain JDBC Connection 三兄弟新进来的请求几乎全军覆没。当时群里已经有人喊重启大法我一个做运维的老同事也准备直接重启服务。我说先别急这三个错同时出现大概率不是 Redis 和数据库同时挂了而是某个底层因素把两条链路一起拖垮了。最后果然不是因为外部中间件宕机而是我们自己埋的坑。这篇文章我就把整套排查思路、底层原理和实际操作过程完整记录下来。不管你是刚接触 Spring Cloud 的初学者还是已经在生产环境里踩过坑的同学看完之后遇到这类报错都能有条理地排查而不是靠重启碰运气。整套方案不依赖具体某个框架版本Spring Boot 2.x 和 3.x 都能适用。1. 三个异常同时出现先别急着重启1.1 先看现场这些报错长什么样我先把当时日志里最扎眼的几段贴出来你对照一下自己遇到的场景。Redis 客户端用的是 Lettuce报错是这样org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost/127.0.0.1:6379数据库这边HikariCP 的报错是java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.MyBatis 层把上面的异常包了一层org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: nested exception is org.springframework.dao.DataAccessResourceFailureException: Failed to obtain JDBC Connection看到这个组合很多人的第一反应是中间件全挂了。但请注意Connection is not available, request timed out after 30000ms这句话它的意思是连接池里拿不到空闲连接等 30 秒也没等到。这说明数据库服务可能根本没问题问题出在连接池本身——要么被占满了要么拿连接的操作被什么东西卡住了。1.2 理顺异常链谁是谁的根因这三个异常不是并列关系而是层层嵌套的关系这一点特别重要。从最外层看业务代码调用 Mapper 接口时MyBatis 会通过 SqlSessionTemplate 执行 SQL这个过程中如果底层拿不到数据库连接MyBatis 就会抛 MyBatisSystemException。而 MyBatisSystemException 只是包装袋里面真正的内容是 Failed to obtain JDBC Connection也就是连接池获取连接失败。Redis 连接失败则是另一条独立的链路但经常和数据库连接失败同时出现原因不是巧合而是通常有一个共同的放大器。我做了个简单类比MyBatisSystemException 就像你去饭馆吃饭等了一个小时没上菜服务员告诉你后厨出不了餐。你真正要排查的不是服务员的态度而是后厨——也就是数据源连接池。如果同一时间 Redis 也连不上那相当于后厨的水电气也出了问题你要看的是管线哪里堵了。1.3 第一步永远不是改代码而是分清故障范围动手排查前先回答两个问题Redis 和数据库是部署在同一台机器上还是分别部署报错是从某个服务实例开始还是所有服务实例同时出现这两个问题的答案决定了根因方向。如果是同一台物理机先看机器负载CPU、内存、磁盘 I/O 任何一个打满都会让两个中间件同时假死。如果是从某个实例开始多半是那个实例的连接池出了问题。如果是所有实例同时报错那基本可以确定是网络或者中间件本身的故障。我见过最离谱的一次是因为服务器磁盘满了Redis 的 AOF 持久化写不进去加上数据库 binlog 也写不进去两个中间件同时进入不可用状态。如果当时直接重启服务重启完还是连不上白白浪费几分钟关键时间。2. Redis 连接失败的排查套路2.1 一条命令先确认 Redis 本身是死是活排查 Redis 连接问题永远先从服务端开始验证。在应用所在机器上执行redis-cli -h 127.0.0.1 -p 6379 ping能正常返回PONG说明 Redis 服务本身没问题。如果这一步就失败再执行telnet 127.0.0.1 6379检查端口是否可达。telnet都连不上问题出在网络层防火墙、安全组或者 Redis 进程没起来。用ps -ef | grep redis确认进程是否存活redis-cli info检查内存使用情况CONFIG GET maxmemory看内存限制。这里有个容易忽略的点即使 Redis 服务正常如果应用所在机器的网络与 Redis 之间有防火墙规则连接同样会超时。公司内部网络的机器变更、安全组策略调整都是引发突发连接失败的常见原因。如果 Redis 需要密码认证用命令行验证时要加上密码参数redis-cli -a 你的密码 ping如果密码配置错了应用日志里会出现NOAUTH Authentication required或者WRONGPASS invalid username-password pair。这类报错和连接失败不一样它表明 TCP 连接是通的只是认证没过。排查方向完全是两码事。2.2 配置文件里最坑的三个点确认 Redis 服务正常后回头看应用配置。我几乎每次排查都能从配置里找到问题常见的有三种。第一个是spring.redis.host配置成了localhost而 Redis 部署在远程服务器上。开发环境用 localhost 没问题打包部署到服务器后就废了。这种低级错误在团队成员各自维护配置文件时特别常见。第二个是密码字段没对齐。Spring Boot 2.x 里 Redis 密码配置项是spring.redis.password但如果你用了 Spring Cloud Config配置中心的 key 可能被共同前缀覆盖了。我真实遇到过spring.redis.password被误写成spring.data.redis.passwordSpring Boot 版本不同解析结果也不同导致认证一直失败。第三个是超时时间配置不合理。默认的连接超时是 60 秒如果 Redis 压力很大或者网络不稳定客户端可能很长时间都在等连接建立表现就是服务响应慢、线程大量堆积。建议主动设置超时时间spring: redis: host: 你的redis地址 port: 6379 password: 你的密码 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: 3000ms把超时调短一点如果 Redis 有问题就快速失败而不是让线程池被长等连接的线程占满。2.3 连接池参数引发假死的经典场景Redis 连接池参数错误导致的故障表现往往非常具有迷惑性因为 Redis 服务端日志看起来完全正常但应用就是连不上。Lettuce 是 Spring Boot 默认的 Redis 客户端基于 Netty 实现底层复用一个连接。它的max-active参数控制的是并发命令的个数不是 TCP 连接数。很多人在 HikariCP 用惯了连接池的概念容易把 MySQL 连接池的参数习惯套到 Redis 上结果调出来的参数并不合理。有个很容易踩的坑是这样的连接池的max-wait设置太短比如 100ms而某个 Redis 命令执行时间稍长比如涉及大 key 的查询耗时 200ms此时并发的其他请求拿不到连接直接抛异常。日志里会出现RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect但实际 Redis 服务端没有任何问题就是连接池分配不到命令执行名额。调整max-total、max-idle、max-wait后恢复正常。2.4 序列化问题带来的伪连接异常还有一种情况看上去像连接异常实际上根本不是连接问题而是序列化问题。Spring Data Redis 提供了多种序列化器默认的JdkSerializationRedisSerializer会把对象序列化成二进制格式。如果你在 Redis 里存的数据是字符串相关类型但代码里读取时用一个设置了错误序列化器的 RedisTemplate反序列化阶段就会抛异常异常信息里可能包含Caused by: java.io.StreamCorruptedException: invalid stream header: 7B22636F6465223A2031看到这种日志不懂底层的人很容易误判为 Redis 连接问题。其实 TCP 连接是好的数据也读回来了只是反序列化失败。我的建议是在配置 RedisTemplate 时显式指定 StringRedisSerializer 和 Jackson2JsonRedisSerializer不要依赖默认的 JDK 序列化。这样不同服务之间、不同语言之间读写同一个 key 都能正常解析。3. Failed to obtain JDBC Connection 的深层原因3.1 连接池的工作机制决定了你会遇到哪些问题HikariCP 是目前 Spring Boot 默认的数据库连接池它维护了一组到数据库的物理连接。应用每次执行 SQL 不是直接新建 TCP 连接而是从池里借一个空闲连接用完再还回去。这个机制的目的很明确避免频繁建连和断连的开销提升性能。但池化机制也带来了新的故障模式如果池里没有空闲连接新的请求就必须等待。HikariCP 默认的最大等待时间是 30 秒超过这个时间就抛出SQLTransientConnectionException: Connection is not available, request timed out after 30000ms.也就是我们遇到的这个报错。这就像银行柜台办理业务连接池里的连接是柜台窗口如果 100 个客户同时进来而只有 10 个窗口那后面 90 个人只能排队。排队超过规定时间系统就告诉你办理不了下次再来。3.2 连接池耗尽的典型特征连接池耗尽时有几个明显的特征截图日志就能判断。HikariCP 在连接池耗尽时除了抛Connection is not available异常还会在日志里周期性打印池状态的指标类似HikariPool-1 - Pool stats (total10, active10, idle0, waiting95)注意看active10表示 10 个连接全部被占用idle0表示没有空闲连接waiting95表示有 95 个线程在排队等连接。出现这个状态基本坐实了连接池耗尽。连接池耗尽的直接原因通常有以下几种某个操作持有连接时间过长慢 SQL事务中执行了外部调用事务迟迟不提交连接泄漏用完没归还并发量突增超过了连接池容量排查时刻重点关注活动连接数变化趋势。我习惯用 Grafana 观察数据源指标HikariCP 提供了很完整的 Micrometer 指标包括hikaricp.connections.active、hikaricp.connections.idle、hikaricp.connections.pending。如果活跃连接数一路走高最后触及上限说明业务侧有只借不还的连接。3.3 慢 SQL 如何压垮连接池给大家讲一个我印象很深的案例。某服务上线一个新功能后开始出现间歇性的Failed to obtain JDBC Connection。数据库 CPU 正常连接数却不断上涨。后来查到问题出在一张表数据量暴涨某个查询没走索引单次查询耗时十几秒。业务并发几十个请求进来每个请求都拿着连接卡在慢查询上连接池迅速被占满。后面的请求全在排队等连接等不到就报错。这个问题有个很大的迷惑性数据库本身没有宕机CPU 也不算高恰恰是慢 SQL 把连接池的资源耗尽了。排查方法很直接在数据库里执行SHOW FULL PROCESSLIST;能看到大量Query类型的连接长时间处于执行中的状态Time字段显示执行耗时Info字段显示正在执行的 SQL。找到耗时最长的几条 SQL用EXPLAIN分析执行计划把缺的索引补上问题就解决了。我记得那个查询语句条件列上有一行WHERE status 1 AND created_at ...created_at 有索引但 status 没有。数据量小的时候没问题数据量涨到几百万行之后查询走了全表扫描。补上(status, created_at)联合索引之后单次查询从 12 秒降到 50 毫秒。3.4 连接泄漏的排查手段慢 SQL 好揪连接泄漏就隐蔽得多。连接泄漏指的是业务代码里取到了连接但用完之后没有归还。短时间几秒可能看不出来问题运行一段时间后连接池就会彻底被偷走的连接占满。HikariCP 自带连接泄漏检测机制。在配置中开启泄露检测时间阈值spring: datasource: hikari: connection-timeout: 30000 leak-detection-threshold: 60000 maximum-pool-size: 20leak-detection-threshold设置为 60000 毫秒表示如果某个连接被借出超过 60 秒没有归还HikariCP 会记录一条告警日志包含当时的堆栈信息。堆栈里可以看到是哪个业务方法借了连接没还。这个机制在排查阶段非常有用因为异常日志看不懂的时候堆栈能帮你直接定位到具体代码行。Spring 框架里最常见的连接泄漏来源是TransactionTemplate和Transactional混用以及手动DataSourceUtils.getConnection()后忘记释放。大家在写工具类时要注意尽量交给 Spring 管理事务不要手动获取连接。4. MyBatisSystemException是症状不是病因4.1 看懂 MyBatis 的异常链MyBatisSystemException 是 MyBatis-Spring 整合包里的异常类型官方文档的说法是表示在 MyBatis 操作过程中发生了无法分类的系统性错误。这句话比较抽象我的理解是它不是点名某个具体技术而是一个包装器把底层各种异常重新包装后进行抛出。异常链一层层打开最内层的cause才是真相。比如我们前面遇到的场景MyBatisSystemException └── PersistenceException └── DataAccessResourceFailureException └── SQLTransientConnectionException └── HikariCP: Connection is not available看到SQLTransientConnectionException就知道是连接池的问题。如果最内层是SQLSyntaxErrorException那就要检查 SQL 语句语法。如果是最内层是DataIntegrityViolationException那是数据约束冲突。每层异常都有自己的含义耐心剥开就行。4.2 MyBatis 中常见的系统异常场景除了数据库连接问题MyBatisSystemException 还可能由下面几种情况触发排查时要区分清楚。第一个是 Mapper 接口和 XML 映射文件不匹配。比如 Mapper 接口方法名改了但 XML 里的statementid 没同步改执行时会报Invalid bound statement (not found)。这种情况不属于 MyBatisSystemException 而是 BindingException但如果 MyBatis-Spring 版本较旧或者配置了configuration.addMapper的方式也可能被包成 MyBatisSystemException。第二个是参数类型不匹配。Mapper 方法传入的参数和 XML 中parameterType不一致报TypeException同样可能被包装。我见过一个坑方法签名里是MapString, ObjectXML 里用的却是单个对象属性结果运行时报There is no getter for property named xx in class java.util.HashMap。第三个是执行 SQL 时数据库连接已经失效。比如数据库发生了主从切换旧连接被服务端断开但连接池里的连接不知道已经失效应用拿到旧连接执行查询时抛异常。HikariCP 虽然默认会做连接有效性检测但检测频率不高如果使用默认配置可能检测不到失效连接。此时配置spring: datasource: hikari: validation-timeout: 3000 keepalive-time: 30000keepalive-time让 HikariCP 每隔 30 秒主动发一次心跳保持连接有效避免拿到已失效的连接。4.3 从堆栈快速定位问题层排查 MyBatisSystemException 最有效的方法是看完整堆栈不要只看第一行。我建议在日志配置里把异常堆栈完整打出来生产环境可以用独立的 error.log 单独记录异常避免堆栈和业务日志混在一起。定位时有一个小技巧先看堆栈里org.apache.ibatis和org.mybatis.spring这些包在哪一层再一层层往下找com.zaxxer.hikari、com.mysql.cj.jdbc、redis.clients.jedis这些具体实现相关的包。Java 异常堆栈是从上往下的调用顺序最底部的 cause 是根因。如果堆栈里有大量HikariCP相关的帧问题基本出在连接获取阶段。如果堆栈在 MyBatis 执行器层就断了可能问题出在 SQL 解析或参数映射阶段。5. 一次连环故障的完整复盘5.1 故障现象记录我把这次故障完整复盘一遍供各位对比。某天 14:30 左右业务监控突然报警订单查询接口超时率飙升到 60%。接警后查看服务日志发现大量MyBatisSystemException和RedisConnectionFailureException。15:00 时服务已经出现明显的请求堆积线程池打满。看监控面板服务所在服务器的 CPU 只有 35%内存 50%网络流量正常。数据库服务器 CPU 20%连接数 120上限 500远未耗尽。Redis 服务器 CPU 35%内存正常INFO命令响应正常。表面上一切正常但服务就是连不上。当时团队里有人提议扩容服务其实这个方向完全错了因为即使扩容再多的实例连接池依然拿不到连接问题反而会蔓延到新实例。5.2 逐步排查过程我当时的排查顺序是先看异常堆栈最内层定位到SQLTransientConnectionException和RedisConnectionFailureException都是连接建立或获取超时。接着在数据库执行SHOW FULL PROCESSLIST没有发现慢 SQL所有连接都处于Sleep状态。然后查看 HikariCP 指标active20, idle0最大连接池就是 20说明连接全部被占满。但数据库侧Sleep状态说明连接没有在执行任务而是被借走不用。接着要判断这些连接是被哪些线程借走的。我们使用 HikariCP 的 leak-detection 功能开启了leak-detection-threshold: 60000五分钟后日志出现了泄露告警堆栈指向一个外部接口调用。再看下这个外部接口调用的发生位置业务代码里有这样一个方法Transactional public OrderInfo getOrderDetail(String orderId) { OrderInfo order orderMapper.selectById(orderId); // 调用外部订单服务查询附加信息耗时平均 8 秒 String extraInfo restTemplate.getForObject(http://order-external-service/api/info/ orderId, String.class); order.setExtraInfo(extraInfo); return order; }问题一目了然Transactional开启的事务在方法执行期间始终占着数据库连接而方法内同步调外部接口花了 8 秒。8 秒内又有并发请求进来每个请求都占一个连接连接池 20 个连接很快全被占满。Redis 那边的情况也类似。业务代码里有个 Redis 分布式锁的工具类锁的获取和释放之间调用了同一个外部接口导致锁被持有 8 秒。高并发情况下Redis 连接池也被长等待的线程耗尽。5.3 解决方案和最终效果找到根因后方案就很清晰了。第一把外部接口调用从事务方法里移出去先查询本地订单数据再调用外部接口最后用编程式事务提交本地更新。这样事务方法和调用耗时解耦数据库连接不再被长任务占用。第二给外部接口调用设置合理的超时时间。RestTemplate 默认没有超时限制业务高峰期外部接口响应慢会一直阻塞。配置连接超时和读取超时HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(2000); factory.setReadTimeout(3000);第三Redis 分布式锁的释放放在 finally 块里确保任何情况下都能释放锁并且给锁设置合理的过期时间防止持有锁的服务实例崩溃导致死锁。调整完成后压测模拟高峰流量服务响应时间恢复正常连接池的active连接数最高只有 8池子不再被打满。这个 case 后续还被写进了部门的技术分享核心结论就一句话别在事务里做耗时的外部调用别在锁的持有期间做不可控操作。6. 典型报错信息速查表日常排查时对照日志关键词能快速缩小范围。我整理了一张表看一眼就能定位问题的大致方向。报错关键词典型场景排查方向Connection is not available, request timed out after 30000ms连接池耗尽 / 慢 SQL / 连接泄漏看 HikariCP active 指标、数据库 PROCESSLISTFailed to obtain JDBC Connection连接池获取连接失败检查连接池配置、网络连通性、数据库状态Unable to connect to RedisRedis 服务不可达或认证失败ping Redis、检查防火墙、检查密码配置NOAUTH Authentication requiredRedis 密码错误或未填写检查 spring.redis.passwordWRONGPASS invalid username-password pairRedis 用户名/密码错误确认连接账号密码是否匹配服务端配置Could not get a resource from the poolJedis 连接池耗尽检查 Redis 连接池 max-total/max-waitinvalid stream header反序列化失败非连接问题检查 RedisTemplate 序列化器配置Communications link failureMySQL 连接链路异常/服务端断开检查网络、MySQL 实例状态、空闲连接最大存活时间 wait_timeoutInvalid bound statement (not found)Mapper 接口与 XML 不匹配检查 Mapper 扫描配置和 XML namespaceConnection is closed拿到了已失效的连接调整 HikariCP keepalive-time / max-lifetime这张表是我平时排查问题的起点看到关键词就能快速判断该往哪个方向查。实际工作中省了很多时间。7. 最后分享一点实操经验踩过这么多次坑我最大的体会是遇到连环异常第一件事不是修复而是分清楚哪些是根因哪些只是表象。很多问题看起来像中间件故障实际是应用层调用了不该放在事务里的操作或者是连接池配置不合理。盲目的重启服务、扩容实例往往掩盖问题而解决不了问题的本质。再多说一个小技巧生产环境的服务一定要配上完整的监控指标至少要有 HikariCP 连接池的active和idle指标、Redis 连接池的活跃连接数、线程池的活跃线程数。这三个数据在故障发生时能快速帮你判断瓶颈在哪。你可以先用下面的核心配置做兜底避免因为连接池耗尽导致服务整体不可用spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 leak-detection-threshold: 60000 keepalive-time: 30000 connection-timeout: 30000 redis: timeout: 3000ms lettuce: pool: max-active: 8 max-wait: 3000ms最后再分享一句实在话线上没有玄学只有没看够的日志和没理清的依赖关系。耐心剥开每一层异常根因一定会浮出来。