【1】问题分析
这个错误 java.io.IOException: Connection reset by peer
主要责任方在 Redis 服务端或中间网络。
以下是详细分析:
-
Connection reset by peer
的含义:- 这是一个标准的 TCP/IP 网络错误。
peer
指的是通信的对端,在这里就是 Redis 服务器。reset
意味着对端(Redis 服务器)主动发送了一个RST
包来强行关闭连接。- 当你的 Java 应用程序(Lettuce 客户端)试图在一个已经被 Redis 服务器强制关闭的 TCP 连接上读取或写入数据时,就会抛出这个异常。
-
为什么责任主要在 Redis 端/网络?
- Redis 服务端主动关闭连接:这是最常见的原因。Redis 可能因为:
- 空闲超时:客户端连接在配置的超时时间内(
timeout
配置项)没有任何活动,Redis 会主动关闭它以释放资源。 - 客户端限制:Redis 达到了最大客户端连接数限制(
maxclients
),需要关闭一些连接来接受新连接。 - Redis 服务端重启/崩溃:Redis 进程被重启、崩溃或管理员手动关闭服务,导致所有连接被重置。
- Redis 服务端资源不足:如内存耗尽(OOM),可能导致 Redis 无法处理连接而关闭。
- 空闲超时:客户端连接在配置的超时时间内(
- 中间网络设备中断:
- 防火墙、负载均衡器、路由器等网络设备可能在连接空闲一段时间后主动终止了 TCP 会话。
- 网络出现瞬时故障导致连接中断。
- 操作系统层面:Redis 服务器所在的操作系统内核因为资源限制(如端口耗尽)或策略主动关闭了连接。
- Redis 服务端主动关闭连接:这是最常见的原因。Redis 可能因为:
-
Java/Lettuce 端可能的(次要或间接)原因:
- 连接池使用不当:应用程序从连接池中获取了一个已经被 Redis 关闭的空闲连接,但在使用前没有进行有效性检查(虽然 Lettuce 通常有内置的重连和验证机制,但在某些配置或极端情况下可能来不及处理)。
- 操作时间过长:如果
deviceReasonCountDayScheduleTask()
方法执行的操作非常耗时,超过了 Redis 服务器的timeout
设置,Redis 可能会在操作执行过程中因连接空闲而关闭连接。 - 未处理连接失效:应用程序没有正确处理连接失效的异常(如重试逻辑、重新获取连接)。
- 网络问题 (Client Side):Java 应用所在机器与 Redis 服务器之间的网络出现故障(可能性小于服务器端或中间网络)。
-
如何排查和解决:
- 检查 Redis 服务端日志:这是最关键的一步!查看 Redis 服务器在错误发生时间点(2025-08-09 06:00:00 左右)的日志。日志通常会明确记录它为什么关闭连接(例如
Client idle timeout
,maxclient limit reached
,OOM
, 正常关闭/重启信息)。 - 检查 Redis 配置:
timeout
:确认值是多少(单位是秒)。如果设置为0
表示禁用超时。检查这个值是否小于你的应用操作可能的最大空闲时间或长任务的执行时间。maxclients
:确认是否达到上限。可以通过 Redis 命令INFO CLIENTS
查看当前连接数和上限。maxmemory
和相关策略:检查是否因为内存不足导致问题(INFO MEMORY
)。
- 检查网络:
- 检查应用服务器与 Redis 服务器之间的网络稳定性(延迟、丢包)。可以使用
ping
,traceroute
,mtr
等工具。 - 检查中间防火墙、负载均衡器的连接空闲超时设置,确保它们不会比 Redis 的
timeout
设置更早地切断连接。通常需要将这些中间设备的超时设置得大于 Redis 的timeout
。
- 检查应用服务器与 Redis 服务器之间的网络稳定性(延迟、丢包)。可以使用
- 检查应用/Lettuce 配置:
- 连接池配置:确保连接池(如
commons-pool2
)配置了testWhileIdle
,testOnBorrow
或testOnCreate
等选项,以便在获取连接时或定期检查连接的有效性。Lettuce 本身也支持自动重连(autoReconnect
默认开启),但检查其配置(如重连延迟、尝试次数)是否合理。 - 超时配置:检查 Lettuce/Spring Data Redis 配置的命令超时(
timeout
,commandTimeout
),确保它足够长以容纳你的操作,但又不会太长导致问题难以暴露。 - 异步任务处理:确保异步任务 (
@Async
) 有合理的超时和错误处理机制。如果任务执行时间非常长,考虑是否适合异步执行,或者将其拆分成更小的任务。
- 连接池配置:确保连接池(如
- 增加重试逻辑:对于这种因网络瞬时故障或连接被回收导致的错误,可以在应用层(或使用 Lettuce/Spring Retry)为 Redis 操作添加适当的重试逻辑,特别是对非幂等操作需要小心处理。
- 监控:加强对 Redis 服务器关键指标的监控(连接数、内存使用、CPU、网络流量、错误日志),以便及时发现和预警类似问题。
- 检查 Redis 服务端日志:这是最关键的一步!查看 Redis 服务器在错误发生时间点(2025-08-09 06:00:00 左右)的日志。日志通常会明确记录它为什么关闭连接(例如
总结:
错误 Connection reset by peer
明确指示是 Redis 服务器(或中间网络设备)主动重置了 TCP 连接。虽然 Java/Lettuce 应用的配置(尤其是连接池和超时)需要合理设置以应对这种情况,但问题的根源通常需要到 Redis 服务端的配置、日志、资源使用情况和网络链路中去寻找。优先排查 Redis 服务器的日志和配置(特别是 timeout
和 maxclients
)以及中间网络设备的超时设置。
【2】 当前配置分析
spring:
redis:
host: ${property.redis.host}
port: ${property.redis.port}
password: ${property.redis.password}
timeout: 60000 #连接Redis服务器的超时时间(单位毫秒),即建立连接的超时时间
lettuce:
pool:
max-active: 128 # 最大连接数(默认为8)
max-idle: 32 # 最大空闲连接
min-idle: 8 # 最小空闲连接
max-wait: 3000ms # 获取连接最大等待时间
time-between-eviction-runs: 60000ms # 每60秒检查一次空闲连接(重要!)
min-evictable-idle-time: 300000ms # 空闲5分钟以上的连接可被驱逐
test-while-idle: true # 检查空闲连接是否有效
shutdown-timeout: 5000ms # 关闭连接超时时间
您的 Redis 配置存在几个关键问题,这些正是导致 “Connection reset by peer” 错误的原因。以下是详细分析和修复方案:
问题分析
-
min-evictable-idle-time
和test-while-idle
爆红问题- 在 Spring Boot 2.x+ 中,Lettuce 连接池配置已废弃这些参数
- 实际生效的是新参数:
test-on-borrow
、test-on-return
、test-on-create
-
核心配置缺陷
min-evictable-idle-time: 300000ms # 空闲5分钟可被驱逐 time-between-eviction-runs: 60000ms # 60秒检查一次 test-while-idle: true # 已废弃
- Redis 默认连接超时(
timeout
)是 300秒(5分钟) - 您的配置会导致:
- 连接空闲 5 分钟时,可能被 Redis 服务器关闭
- 连接池在 5 分钟内不会主动检查/清理这些连接
- 当应用程序获取到这些"僵尸连接"时,就会触发 “Connection reset by peer”
- Redis 默认连接超时(
修复方案
spring:
redis:
host: ${property.redis.host}
port: ${property.redis.port}
password: ${property.redis.password}
timeout: 60000
lettuce:
pool:
max-active: 128
max-idle: 32
min-idle: 8
max-wait: 3000ms
# 关键修复配置 ▼▼▼
time-between-eviction-runs: 30000ms # 缩短为30秒检查一次
min-evictable-idle-time: 240000ms # 空闲4分钟驱逐(小于Redis超时)
test-on-borrow: true # 替换test-while-idle
shutdown-timeout: 5000ms
为什么这样修改
-
连接有效性检查 (
test-on-borrow
)- ✅ 替换废弃的
test-while-idle
- 每次从连接池获取连接时执行
PING
命令验证 - 确保获取的连接都是有效的
- ✅ 替换废弃的
-
驱逐时间优化 (
min-evictable-idle-time
)- 设置为 4分钟(240秒) 而不是 5分钟
- 确保在 Redis 服务器关闭连接前(5分钟)主动驱逐空闲连接
- 公式:
min-evictable-idle-time < Redis服务器timeout
-
检查频率提高 (
time-between-eviction-runs
)- 从 60秒 → 30秒
- 更快清理无效连接,减少获取到僵尸连接的概率
-
额外建议配置
lettuce: shutdown-timeout: 5000ms # 增加重连配置 ▼ client-name: my-app-conn # 在Redis端识别连接 ping-before-activate: true # 激活前PING
服务器端优化建议
在 Redis 服务器配置中(redis.conf):
timeout 600 # 增加为10分钟(单位秒)
tcp-keepalive 60 # 启用TCP心跳
验证是否生效
添加此配置查看连接池状态:
@Bean
public CommandLineRunner redisPoolMonitor(LettuceConnectionFactory factory) {
return args -> {
GenericObjectPool<?> pool = factory.getPool();
log.info("Redis pool active: {}/{}", pool.getNumActive(), pool.getMaxTotal());
log.info("Redis pool idle: {}/{}", pool.getNumIdle(), pool.getMaxIdle());
};
}
关键点:通过主动驱逐(4分钟)+ 借用时验证 + 更频繁检查的三重保障,可彻底解决此问题。同时建议在 Redis 服务器端增加超时时间作为双重保险。
【3】Spring Boot 的 RedisProperties.Pool 没有直接暴露 testOnBorrow 等高级连接池配置参数
SpringBoot版本2.4.4
从您提供的代码片段来看,这是 Spring Boot 内部处理 Redis 连接池配置的部分。确实,Spring Boot 的 RedisProperties.Pool
没有直接暴露 testOnBorrow
等高级连接池配置参数。让我解释原因并提供解决方案:
问题分析
-
Spring Boot 的 RedisProperties 限制:
RedisProperties.Pool
只提供了基础配置参数(maxActive, maxIdle, minIdle, maxWait, timeBetweenEvictionRuns)- 高级参数如
testOnBorrow
、testWhileIdle
等没有在 Spring Boot 的配置属性中公开
-
源码证据:
public static class Pool { private int maxIdle = 8; private int minIdle = 0; private int maxActive = 8; private Duration maxWait = Duration.ofMillis(-1); private Duration timeBetweenEvictionRuns; // 没有 testOnBorrow 等属性 }
解决方案:自定义连接池配置
您需要自定义 LettuceConnectionFactory
来启用这些高级参数:
1. 创建自定义配置类
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import io.lettuce.core.resource.ClientResources;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.time.Duration;
@Configuration
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {
RedisProperties.Lettuce lettuce = redisProperties.getLettuce();
RedisProperties.Pool poolProps = lettuce.getPool();
// 创建自定义连接池配置
GenericObjectPoolConfig<?> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(poolProps.getMaxActive());
poolConfig.setMaxIdle(poolProps.getMaxIdle());
poolConfig.setMinIdle(poolProps.getMinIdle());
if (poolProps.getMaxWait() != null) {
poolConfig.setMaxWait(poolProps.getMaxWait());
}
if (poolProps.getTimeBetweenEvictionRuns() != null) {
poolConfig.setTimeBetweenEvictionRuns(poolProps.getTimeBetweenEvictionRuns());//后台线程每隔 XXX 秒检查一次空闲连接
}
// 添加 Spring Boot 未公开的配置
poolConfig.setMinEvictableIdleTime(Duration.ofMillis(240000)); // 4分钟 一个连接在连接池中至少空闲多长时间后,才允许被“驱逐”(即被销毁回收)。
poolConfig.setTestOnBorrow(true); // 当应用程序从连接池中“借用”(获取)一个连接时,是否先对该连接进行有效性检测
poolConfig.setTestWhileIdle(true); // 空闲检查 当连接处于“空闲”状态时,是否由后台的“清理线程”定期检查它的有效性(是否还活着)。 必须同时设置 timeBetweenEvictionRuns 才会生效
// 构建 Lettuce 配置
LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(poolConfig)
.commandTimeout(redisProperties.getTimeout())
.shutdownTimeout(lettuce.getShutdownTimeout())
.build();
// ✅ 正确方式:使用 setter 设置 host、port、password
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName(redisProperties.getHost());
redisConfig.setPort(redisProperties.getPort());
redisConfig.setPassword(RedisPassword.of(redisProperties.getPassword())); // 注意:使用 RedisPassword.of()
// 创建连接工厂
return new LettuceConnectionFactory(redisConfig, clientConfig);
}
}
2. 在 application.yml 中移除无效配置
spring:
redis:
host: ${property.redis.host}
port: ${property.redis.port}
password: ${property.redis.password}
timeout: 60000
lettuce:
pool:
max-active: 128
max-idle: 32
min-idle: 8
max-wait: 3000ms
time-between-eviction-runs: 30000ms # 30秒检查一次
shutdown-timeout: 5000ms
# 移除以下无效配置
# min-evictable-idle-time: 300000ms # 已移动到代码配置
# test-while-idle: true # 已移动到代码配置
关键配置说明
-
poolConfig.setTestOnBorrow(true)
:- 每次从连接池获取连接时执行
PING
命令验证连接 - 确保获取的连接都是有效的
- 每次从连接池获取连接时执行
-
poolConfig.setMinEvictableIdleTimeMillis(240000)
:- 设置空闲连接 4 分钟后可被驱逐
- 确保早于 Redis 服务器的默认 5 分钟超时
-
poolConfig.setTestWhileIdle(true)
:- 在空闲时间检查连接有效性
- 配合
time-between-eviction-runs
使用
-
时间单位转换:
- Spring 的
Duration
需要转换为毫秒:toMillis()
- 例如:
.setMinEvictableIdleTimeMillis(240000)
- Spring 的
验证配置生效
SpringBoot版本2.4.4,添加以下 Bean 来监控连接池状态:
@Bean
public CommandLineRunner redisPoolMonitor(RedisConnectionFactory connectionFactory) {
return args -> {
if (connectionFactory instanceof LettuceConnectionFactory) {
LettuceConnectionFactory lettuceFactory = (LettuceConnectionFactory) connectionFactory;
// 安全地获取客户端配置
LettuceClientConfiguration clientConfiguration = lettuceFactory.getClientConfiguration();
if (clientConfiguration instanceof LettucePoolingClientConfiguration) {
LettucePoolingClientConfiguration poolingConfig =
(LettucePoolingClientConfiguration) clientConfiguration;
GenericObjectPoolConfig<?> poolConfig = poolingConfig.getPoolConfig();
// ✅ 正确打印连接池配置
logger.info("✅ Redis 连接池已启用,配置如下:");
logger.info(" TestOnBorrow={}", poolConfig.getTestOnBorrow());
logger.info(" TestWhileIdle={}", poolConfig.getTestWhileIdle());
logger.info(" MaxTotal={}", poolConfig.getMaxTotal());
logger.info(" MaxIdle={}", poolConfig.getMaxIdle());
logger.info(" MinIdle={}", poolConfig.getMinIdle());
logger.info(" MaxWait={}ms", poolConfig.getMaxWaitMillis());
logger.info(" MinEvictableIdleTime={}ms", poolConfig.getMinEvictableIdleTimeMillis());
logger.info(" TimeBetweenEvictionRuns={}ms", poolConfig.getTimeBetweenEvictionRunsMillis());
} else {
logger.warn("Redis 连接池未启用,当前配置类型: {}", clientConfiguration.getClass().getSimpleName());
}
} else {
logger.warn("未使用 Lettuce 作为 Redis 客户端: {}", connectionFactory.getClass().getSimpleName());
}
};
}
最终效果
- ✅ 连接池在借用时会自动验证连接有效性
- ✅ 空闲连接会在 4 分钟时被驱逐(早于 Redis 5 分钟超时)
- ✅ 每 30 秒检查一次空闲连接
- ❌ 不再有无效的 YAML 配置导致爆红
这种方案完全绕过了 Spring Boot 配置的限制,直接通过代码设置所需的连接池参数,是解决此类高级配置需求的标准做法。