RedisNotes

第 07 章:客户端与连接池

zjc 于 2026-01-07 发布

这是《Redis 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Redis 服务端再快,客户端也可能成为瓶颈。本章讲 Jedis、Lettuce 的差异、连接治理、pipeline、订阅与序列化边界。

7.1 Java 客户端怎么选

客户端 特点 适合
Jedis 简单直接,同步 API,连接池 传统应用、简单同步场景
Lettuce Netty 驱动,支持同步/异步/响应式,连接共享 Spring Boot 默认、高并发长连接
Redisson 分布式对象与锁,底层 Netty 需要锁、限流、延迟队列等高级能力

Spring Boot 3 默认集成 Lettuce;Redisson 常与 Lettuce 并存,一个负责基础数据访问,一个负责分布式能力。

7.2 Maven 依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>5.1.0</version>
</dependency>

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.3.0.RELEASE</version>
</dependency>

7.3 Jedis 快速开始

JedisPool pool = new JedisPool("127.0.0.1", 6379);

try (Jedis jedis = pool.getResource()) {
    jedis.auth("redis123");
    jedis.set("user:1001", "Tom");
    System.out.println(jedis.get("user:1001"));
}

推荐显式构建池:

JedisPool pool = new JedisPool(
        new JedisPoolConfig(),
        "127.0.0.1",
        6379,
        2000,      // connection timeout
        2000,      // socket timeout
        "default", // user
        "redis123",
        0,         // db
        "order-service"
);

7.4 Lettuce 快速开始

RedisURI uri = RedisURI.builder()
        .withHost("127.0.0.1")
        .withPort(6379)
        .withAuthentication("default", "redis123")
        .withDatabase(0)
        .withTimeout(Duration.ofSeconds(2))
        .build();

RedisClient client = RedisClient.create(uri);
StatefulRedisConnection<String, String> conn = client.connect();
RedisCommands<String, String> sync = conn.sync();

sync.set("user:1001", "Tom");
System.out.println(sync.get("user:1001"));

conn.close();
client.shutdown();

Lettuce 的连接是线程安全的,可以多个线程共享一个 StatefulRedisConnection。它的异步 API 返回 RedisFuture

RedisAsyncCommands<String, String> async = conn.async();
RedisFuture<String> future = async.get("user:1001");
future.thenAccept(System.out::println);

7.5 连接池参数

以 Jedis 为例:

maxTotal=200
maxIdle=100
minIdle=20
testWhileIdle=true
testOnBorrow=false
timeBetweenEvictionRunsMillis=30000
minEvictableIdleTimeMillis=60000
blockWhenExhausted=true
maxWaitMillis=2000

参数含义:

参数 建议
maxTotal 按并发与 RTT 估算,不要盲目几千
maxIdle 接近 maxTotal,避免频繁建连
minIdle 保留常温连接,避免冷启动尖刺
maxWaitMillis 必须设置,防止无限等待
testOnBorrow 生产通常 false,借出检测会增加延迟

粗略估算:

需要的连接数 ≈ 请求速率(RPS) * Redis 平均耗时(s)

例:1 万 QPS,Redis 平均 2ms -> 10000 * 0.002 = 20
   预留峰值 3 倍 -> 60 连接

7.6 超时与重试

必备四类超时:

  1. 连接超时:TCP 建连;
  2. 命令超时:等待 Redis 响应;
  3. 资源等待超时:连接池借连接;
  4. 请求整体超时:业务接口层兜底。

Jedis 示例:

Jedis jedis = pool.getResource();
jedis.setTimeout(2000);

Lettuce 示例:

RedisURI uri = RedisURI.builder()
        .withHost("host")
        .withTimeout(Duration.ofMillis(500))
        .build();

重试原则:

7.7 Pipeline:减少网络往返

一次网络往返可能消耗 0.5-2ms。100 个命令逐个执行,网络成本会远高于 Redis 执行成本。

try (Jedis jedis = pool.getResource()) {
    Pipeline pipe = jedis.pipelined();
    for (int i = 0; i < 1000; i++) {
        pipe.set("key:" + i, "value:" + i);
    }
    pipe.sync();
}

Lettuce:

conn.async().pipelined();  // 通过批量提交自动合并

Pipeline 与事务不同:

特性 Pipeline MULTI/EXEC
减少 RTT
服务端原子执行 不保证
失败处理 每条命令独立 队列错误影响执行

批次建议:

7.8 Pub/Sub 客户端

JedisPubSub listener = new JedisPubSub() {
    @Override
    public void onMessage(String channel, String message) {
        System.out.println(channel + " -> " + message);
    }
};

new Thread(() -> jedis.subscribe(listener, "cache:invalidate")).start();

注意:

7.9 序列化策略

Redis value 是字节数组,客户端序列化决定可读性、体积和兼容性:

格式 优点 缺点
String 人类可读,调试方便 体积较大
JSON 跨语言,生态好 解析 CPU、schema 弱
JDK 序列化 Java 原生 体积大、安全风险、跨语言差
Kryo/Protobuf 体积小,速度快 schema 与调试成本

业务缓存建议:

7.10 客户端常见异常

异常 常见原因 处理
JedisConnectionException 网络断、实例挂、超时 检查健康、超时、重试
JedisDataException: NOAUTH 认证失败 检查密码/ACL
JedisExhaustedPoolException 池耗尽 排查慢命令、泄漏、池参数
RedisCommandTimeoutException 命令超时 看慢查询/CPU/网络
RedisBusyException Cluster 迁移/重定向 客户端识别 MOVED/ASK
WRONGTYPE 类型不匹配 统一 key schema

资源泄漏典型错误:

Jedis jedis = pool.getResource();
jedis.set("k", "v"); // 异常后未归还

正确写法必须 try-with-resourcesfinally归还

本章小结

思考题

  1. 1000 QPS、Redis 平均 5ms,需要多少连接?如果命令变成 50ms 呢?
  2. 为什么写操作不能盲目自动重试?哪些写可以安全重试?
  3. Pipeline 里执行 10 万条命令会有什么风险?如何分批?