news 2026/9/13 14:55:09

Spring Cloud服务连不上Redis和数据库?整套排查思路与实战复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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.password,Spring 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-totalmax-idlemax-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 (total=10, active=10, idle=0, waiting=95)

注意看active=10表示 10 个连接全部被占用,idle=0表示没有空闲连接,waiting=95表示有 95 个线程在排队等连接。出现这个状态,基本坐实了连接池耗尽。

连接池耗尽的直接原因通常有以下几种:

  • 某个操作持有连接时间过长(慢 SQL)
  • 事务中执行了外部调用,事务迟迟不提交
  • 连接泄漏,用完没归还
  • 并发量突增,超过了连接池容量

排查时刻重点关注活动连接数变化趋势。我习惯用 Grafana 观察数据源指标,HikariCP 提供了很完整的 Micrometer 指标,包括hikaricp.connections.activehikaricp.connections.idlehikaricp.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: 20

leak-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,同样可能被包装。我见过一个坑,方法签名里是Map<String, Object>,XML 里用的却是单个对象属性,结果运行时报There is no getter for property named 'xx' in 'class java.util.HashMap'

第三个是执行 SQL 时数据库连接已经失效。比如数据库发生了主从切换,旧连接被服务端断开,但连接池里的连接不知道已经失效,应用拿到旧连接执行查询时抛异常。HikariCP 虽然默认会做连接有效性检测,但检测频率不高,如果使用默认配置可能检测不到失效连接。此时配置:

spring: datasource: hikari: validation-timeout: 3000 keepalive-time: 30000

keepalive-time让 HikariCP 每隔 30 秒主动发一次心跳,保持连接有效,避免拿到已失效的连接。

4.3 从堆栈快速定位问题层

排查 MyBatisSystemException 最有效的方法是看完整堆栈,不要只看第一行。我建议在日志配置里把异常堆栈完整打出来,生产环境可以用独立的 error.log 单独记录异常,避免堆栈和业务日志混在一起。

定位时有一个小技巧:先看堆栈里org.apache.ibatisorg.mybatis.spring这些包在哪一层,再一层层往下找com.zaxxer.hikaricom.mysql.cj.jdbcredis.clients.jedis这些具体实现相关的包。Java 异常堆栈是从上往下的调用顺序,最底部的 cause 是根因。如果堆栈里有大量HikariCP相关的帧,问题基本出在连接获取阶段。如果堆栈在 MyBatis 执行器层就断了,可能问题出在 SQL 解析或参数映射阶段。

5. 一次连环故障的完整复盘

5.1 故障现象记录

我把这次故障完整复盘一遍,供各位对比。

某天 14:30 左右,业务监控突然报警,订单查询接口超时率飙升到 60%。接警后查看服务日志,发现大量MyBatisSystemExceptionRedisConnectionFailureException。15:00 时,服务已经出现明显的请求堆积,线程池打满。

看监控面板,服务所在服务器的 CPU 只有 35%,内存 50%,网络流量正常。数据库服务器 CPU 20%,连接数 120,上限 500,远未耗尽。Redis 服务器 CPU 35%,内存正常,INFO命令响应正常。

表面上一切正常,但服务就是连不上。当时团队里有人提议扩容服务,其实这个方向完全错了,因为即使扩容再多的实例,连接池依然拿不到连接,问题反而会蔓延到新实例。

5.2 逐步排查过程

我当时的排查顺序是:先看异常堆栈最内层,定位到SQLTransientConnectionExceptionRedisConnectionFailureException都是连接建立或获取超时。接着在数据库执行SHOW FULL PROCESSLIST,没有发现慢 SQL,所有连接都处于Sleep状态。

然后查看 HikariCP 指标,active=20, idle=0,最大连接池就是 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 指标、数据库 PROCESSLIST
Failed to obtain JDBC Connection连接池获取连接失败检查连接池配置、网络连通性、数据库状态
Unable to connect to RedisRedis 服务不可达或认证失败ping Redis、检查防火墙、检查密码配置
NOAUTH Authentication requiredRedis 密码错误或未填写检查 spring.redis.password
WRONGPASS invalid username-password pairRedis 用户名/密码错误确认连接账号密码是否匹配服务端配置
Could not get a resource from the poolJedis 连接池耗尽检查 Redis 连接池 max-total/max-wait
invalid stream header反序列化失败(非连接问题)检查 RedisTemplate 序列化器配置
Communications link failureMySQL 连接链路异常/服务端断开检查网络、MySQL 实例状态、空闲连接最大存活时间 wait_timeout
Invalid bound statement (not found)Mapper 接口与 XML 不匹配检查 Mapper 扫描配置和 XML namespace
Connection is closed拿到了已失效的连接调整 HikariCP keepalive-time / max-lifetime

这张表是我平时排查问题的起点,看到关键词就能快速判断该往哪个方向查。实际工作中省了很多时间。

7. 最后分享一点实操经验

踩过这么多次坑,我最大的体会是:遇到连环异常,第一件事不是修复,而是分清楚哪些是根因,哪些只是表象。很多问题看起来像中间件故障,实际是应用层调用了不该放在事务里的操作,或者是连接池配置不合理。盲目的重启服务、扩容实例,往往掩盖问题而解决不了问题的本质。

再多说一个小技巧:生产环境的服务一定要配上完整的监控指标,至少要有 HikariCP 连接池的activeidle指标、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

最后再分享一句实在话:线上没有玄学,只有没看够的日志和没理清的依赖关系。耐心剥开每一层异常,根因一定会浮出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 14:54:04

某度翻译Acs-Token算法逆向分析与安全机制解析

1. Acs-Token算法背景与应用场景某度翻译作为国内领先的机器翻译服务提供商&#xff0c;其API接口采用了名为Acs-Token的安全验证机制。这种算法本质上是一种动态签名技术&#xff0c;主要用于&#xff1a;防止未授权调用翻译API限制接口滥用和恶意爬取实现请求来源的身份验证保…

作者头像 李华
网站建设 2026/9/13 14:51:14

LeetCode-Go 题解 18. 4Sum:三种去重方案实现四数之和

LeetCode-Go 题解 18. 4Sum&#xff1a;三种去重方案实现四数之和 【免费下载链接】LeetCode-Go ✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解 项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go 本篇文章以 …

作者头像 李华
网站建设 2026/9/13 14:50:43

Gleam 编译器版本发布全流程指南:从版本号更新到 CI 自动发布

Gleam 编译器版本发布全流程指南&#xff1a;从版本号更新到 CI 自动发布 【免费下载链接】gleam ⭐️ A friendly language for building type-safe, scalable systems! 项目地址: https://gitcode.com/GitHub_Trending/gl/gleam 本篇指南围绕 Gleam 编译器仓库根目录下…

作者头像 李华
网站建设 2026/9/13 14:49:54

RoboMaster硬件实战:PCB设计、调试与嘉立创EDA工程落地

1. 这份讲义不是“教材”&#xff0c;而是电控组新人上手前必须拆开的三块电路板你拿到《Robomaster硬件基础讲义V0.2.1》时&#xff0c;大概率正坐在实验室长桌前&#xff0c;面前摆着一块刚焊完但没亮灯的主控板&#xff0c;旁边是半盒散落的0402电阻和一卷被烫弯的杜邦线。讲…

作者头像 李华