第25章 Redis 客户端异常

3 阅读5分钟

Java 异常大全系列 · Part 6 中间件异常

关于本 Part 的验证说明:本 Part 涉及的中间件客户端库(Jedis/Lettuce、Kafka Client、RabbitMQ/RocketMQ Client、Dubbo/gRPC)均只发布到 Maven Central,当前环境网络策略下无法直接拉取这些 jar 包做实测编译运行(此前几个 Part 中用到的 H2、Temurin JDK 是因为它们额外在 GitHub Releases 提供了 jar 包,才得以实测)。因此本 Part 内容以这些客户端库的官方文档、源码公开信息为依据,但连接层面的行为(连接被拒绝、超时等)沿用了 Part 4/18 中已经用真实 Socket 实测验证过的 TCP/网络异常机制——我额外用原生 Socket 直接对本机真实启动的 Redis 实例发送 RESP 协议命令做了验证(见 25.1 节),确认了协议层面的基础行为,但客户端库(Jedis 等)包装后的具体异常类型和消息格式未做实测,请在实际项目中以对应版本的官方文档为准。

当前 Part 内容:

  • 第25章 Redis 客户端异常
  • 第26章 Kafka 生产消费异常
  • 第27章 RabbitMQ / RocketMQ 异常
  • 第28章 RPC 框架异常:Dubbo / gRPC

第25章 Redis 客户端异常

25.1 实测:RESP 协议层面的连接行为

Redis 使用基于文本的 RESP(REdis Serialization Protocol)协议,用原生 Socket 直接实测验证最基础的连接层行为(不经过任何 Java Redis 客户端库封装):

// 正常连接 + 发送 PING 命令
try (Socket socket = new Socket("127.0.0.1", 6379)) {
    socket.getOutputStream().write("PING\r\n".getBytes());
    // 实测响应:+PING\r\n 对应的简单字符串回复格式是 "+PONG"
}
// 连接一个没有服务监听的端口
try (Socket socket2 = new Socket("127.0.0.1", 16380)) {
} catch (ConnectException e) {
    // 实测:Connection refused —— 和 Part 4 中验证的通用 TCP 连接拒绝行为完全一致
}

这验证了一个重要认知:所有 Java Redis 客户端库(Jedis/Lettuce)底层都是基于普通 TCP Socket 实现的,"Redis 连不上"在网络层面的根因(DNS 解析失败/端口拒绝/读取超时)和 Part 4 讨论的通用网络异常完全同源,只是客户端库会把底层的 ConnectException/SocketTimeoutException 包装成各自的异常类型(如 Jedis 的 JedisConnectionException)。

25.2 Jedis 常见异常类型(依据官方文档)

异常触发场景
JedisConnectionException底层 TCP 连接失败,包装了 ConnectException/SocketTimeoutException
JedisDataExceptionRedis 服务端返回了错误响应(如对 String 类型的 key 执行了 List 操作)
JedisException(各类子类的公共父类)Jedis 库自身的通用异常基类
JedisConnectionException: Could not get a resource from the pool连接池(JedisPool)耗尽,所有连接都被占用且新连接创建也失败

连接池耗尽场景(常见于高并发下 JedisPool 配置不合理):

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);      // 最大连接数
config.setMaxWaitMillis(3000); // 获取连接最长等待时间,超时抛异常

try (Jedis jedis = jedisPool.getResource()) { // 池耗尽且等待超时后,这里抛 JedisException
    jedis.set("key", "value");
}

排查方向: 和 Part 21 讨论的数据库连接池诊断思路完全一致——检查是否存在"获取了 Jedis 实例但没有正确归还(漏了 try-with-resources 或 finally 块)"的连接泄漏问题,以及业务侧是否存在个别慢操作(如对超大 hash/set 做全量扫描式的 HGETALL/SMEMBERS)长时间占用连接。

25.3 序列化异常:跨语言/跨版本场景

Redis 本身只存储字节数组(byte[]),Java 客户端需要一层序列化/反序列化。Spring 的 RedisTemplate 默认使用 JDK 原生序列化(JdkSerializationRedisSerializer),这直接关联到 Part 4 第20章讨论的 serialVersionUID 问题——如果业务类结构发生变化(哪怕只是新增字段)且没有规范管理 serialVersionUID,缓存在 Redis 里的旧序列化数据在服务发布新版本后会集体反序列化失败,表现为大量业务日志报 SerializationException(Spring 对底层反序列化异常的包装类型)。

工程规避方式(生产环境强烈推荐):

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    // 用 JSON 序列化替代 JDK 原生序列化:跨语言可读、不依赖 serialVersionUID 这套脆弱机制、
    // 且缓存内容在 Redis 客户端工具里可以直接肉眼查看(JDK 序列化是不可读的二进制格式)
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.setKeySerializer(new StringRedisSerializer());
    return template;
}

25.4 集群模式的重定向异常:MOVED / ASK

Redis Cluster 模式下,key 按哈希槽(hash slot,共 16384 个)分布在不同节点,如果客户端向一个不负责该 key 对应哈希槽的节点发起请求,服务端会返回 MOVED(槽位已永久迁移到新节点)或 ASK(槽位正在迁移过程中)响应,客户端库需要正确处理这类重定向:

错误响应示例(Redis 服务端返回,而非 Java 层的异常,但客户端库会把它包装为异常抛出):
MOVED 3999 127.0.0.1:7002
ASK 3999 127.0.0.1:7002

Java 集群客户端(如 JedisCluster)内部会自动处理这类重定向(拿到 MOVED 响应后自动重新连接到正确节点重试),业务代码通常不会直接看到这类异常;但如果客户端的槽位映射表缓存过期没有及时刷新(比如集群刚完成一次扩容/缩容的槽位迁移),短时间内可能会出现较多 JedisMovedDataException/JedisAskDataException,通常是自愈性的(客户端会自动刷新映射表),如果持续大量出现,需要排查集群拓扑是否处于不稳定的迁移过程中。