这是《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 超时与重试
必备四类超时:
- 连接超时:TCP 建连;
- 命令超时:等待 Redis 响应;
- 资源等待超时:连接池借连接;
- 请求整体超时:业务接口层兜底。
Jedis 示例:
Jedis jedis = pool.getResource();
jedis.setTimeout(2000);
Lettuce 示例:
RedisURI uri = RedisURI.builder()
.withHost("host")
.withTimeout(Duration.ofMillis(500))
.build();
重试原则:
- 只重试幂等命令或带有业务请求 ID 的操作;
- 读操作可快速重试 1-2 次;
- 写操作谨慎重试,避免重复扣减;
- 每层重试要有预算,避免重试风暴;
- Redis 事务/Lua 执行结果不明确时,必须依赖业务幂等修正。
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 | 是 | 是 |
| 服务端原子执行 | 不保证 | 是 |
| 失败处理 | 每条命令独立 | 队列错误影响执行 |
批次建议:
- 每批 100-1000 条,按消息大小实测;
- 避免一个大 value 拖慢整批;
- 监控客户端响应时间与 Redis 输出缓冲;
- 大批量导入建议分批执行并 sleep。
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();
注意:
- Pub/Sub 消息不持久,订阅者不在线就丢;
- 消费慢时输出缓冲可能被断开;
- 要可靠投递,用 Stream;
- 集群模式可考虑 Sharded Pub/Sub。
7.9 序列化策略
Redis value 是字节数组,客户端序列化决定可读性、体积和兼容性:
| 格式 | 优点 | 缺点 |
|---|---|---|
| String | 人类可读,调试方便 | 体积较大 |
| JSON | 跨语言,生态好 | 解析 CPU、schema 弱 |
| JDK 序列化 | Java 原生 | 体积大、安全风险、跨语言差 |
| Kryo/Protobuf | 体积小,速度快 | schema 与调试成本 |
业务缓存建议:
- key 可读,value 使用 JSON/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-resources 或 finally归还。
本章小结
- Lettuce 适合 Spring Boot 与高并发共享连接,Jedis 简单直观;
- 连接池大小按 QPS * 平均耗时估算,并设置等待超时;
- 四类超时与有限重试是生产客户端底线;
- Pipeline 降低 RTT,但不提供原子性;
- Pub/Sub 只适合低可靠广播,可靠消息用 Stream。
思考题
- 1000 QPS、Redis 平均 5ms,需要多少连接?如果命令变成 50ms 呢?
- 为什么写操作不能盲目自动重试?哪些写可以安全重试?
- Pipeline 里执行 10 万条命令会有什么风险?如何分批?