选客户端的第一个问题不是"哪个快",而是"一个连接能不能被多个线程同时用"。Jedis 的回答是不能,Lettuce 的回答是能——这一个差异决定了两者的连接池策略、阻塞命令写法和故障形态。以下按 Jedis 5.x 与 Lettuce 6.x(Spring Boot 3.x 默认)核对。
1. Jedis:同步阻塞、实例非线程安全
一个 Jedis 实例内部持有单个 socket 连接与输入输出流,多个线程同时读写会互相踩到协议帧,结果是拿到别人的响应或协议错乱。所以它必须配连接池。
try (Jedis jedis = jedisPool.getResource()) { // 借用
jedis.set("k", "v");
} // 归还
| 特性 | Jedis |
|---|---|
| IO 模型 | 同步阻塞,一连接一请求 |
| 实例线程安全 | 否 |
| 正确用法 | 每次操作从 JedisPool 借还 |
| 单连接并发能力 | 1 |
阻塞命令(BLPOP) |
独占一个连接直到返回 |
"非线程安全"不是 bug 而是设计:它把并发控制交给连接池,池大小就是并发上限。
2. JedisPool 的关键参数
GenericObjectPoolConfig<Jedis> cfg = new GenericObjectPoolConfig<>();
cfg.setMaxTotal(64); // 池内最多 64 个连接
cfg.setMaxIdle(32); // 空闲连接上限
cfg.setMinIdle(8); // 常驻空闲连接
cfg.setMaxWait(Duration.ofMillis(500)); // 借不到连接的最长等待
cfg.setTestOnBorrow(true); // 借出前 PING
cfg.setTestWhileIdle(true); // 空闲连接定期检测
JedisPool pool = new JedisPool(cfg, "redis-host", 6379);
| 参数 | 作用 | 设错的后果 |
|---|---|---|
maxTotal |
连接数上限 | 太小 → 借连接排队;太大 → Redis maxclients 打满 |
maxIdle |
空闲连接上限 | 大于 maxTotal 无效,过小导致频繁建连 |
minIdle |
常驻连接 | 太小 → 冷启动抖动 |
maxWaitMillis |
借连接超时 | 默认 -1(无限等),故障时线程全挂住 |
testOnBorrow |
借出前探活 | 关掉则可能借到已断的连接,报 JedisConnectionException |
maxWait 必须设置。默认无限等待意味着 Redis 一慢,Tomcat 线程会被逐个拖死在 getResource() 上,外部看是"全站 502"。
3. 连接耗尽长什么样
| 现象 | 含义 |
|---|---|
Could not get a resource from the pool |
maxTotal 用尽且 maxWait 超时 |
线程栈停在 GenericObjectPool.borrowObject |
借连接阻塞,不是 Redis 慢 |
redis-cli CLIENT LIST 里 age 很大的连接很多 |
连接泄漏(借了没还) |
cmd=null 的空闲连接持续增长 |
连接池配置与实例数不匹配 |
排查顺序:先看 Redis 端 connected_clients,再看应用线程栈,最后看池指标(numActive、numWaiters)。numWaiters 持续大于 0 就是容量不够。
4. Lettuce:Netty 多路复用、单连接线程安全
Lettuce 用 Netty 的 channel 做异步 IO,一个连接上可以并发多个命令,靠 RESP 协议的顺序响应做匹配。因此默认 shareNativeConnection=true,所有线程共用一个连接。
RedisClient client = RedisClient.create("redis://redis-host:6379");
StatefulRedisConnection<String, String> conn = client.connect(); // 线程安全
RedisCommands<String, String> cmd = conn.sync(); // 同步门面
cmd.set("k", "v");
conn.close();
client.shutdown();
| 特性 | Lettuce |
|---|---|
| IO 模型 | Netty 异步 + 多路复用 |
| 实例线程安全 | 是(连接级共享) |
| 默认连接数 | 1(共享原生连接) |
| 阻塞命令 | 会独占该连接,shareNativeConnection 下影响其他人 |
| 断线重连 | 内置自动重连 + 命令缓冲 |
shareNativeConnection=true 的隐含风险:一条 BLPOP(无超时)会把这个共享连接永久占住,后续所有命令排队。处理阻塞命令必须用专门的连接。
5. Spring Boot 的默认选择
Spring Boot 2.x 起,spring-boot-starter-data-redis 默认客户端是 Lettuce。
spring:
data:
redis:
host: redis-host
port: 6379
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 500ms
| 配置项 | Lettuce 语义 | 说明 |
|---|---|---|
lettuce.pool.* |
复用 Apache Commons Pool 管理连接 | 只有开了 pool 才有池概念 |
jedis.pool.* |
Jedis 专用 | 需显式引入 jedis 并排除 lettuce |
spring.data.redis.client-type |
lettuce / jedis |
决定装配哪个 RedisConnectionFactory |
要在 Boot 3.x 里换回 Jedis,需要排除 lettuce-core、引入 jedis,并设 client-type: jedis。两者的 RedisTemplate API 完全一致——差异都在连接层。
6. 集群与哨兵下的行为差异
| 维度 | Jedis | Lettuce |
|---|---|---|
| 集群拓扑刷新 | 靠 JedisCluster 定期/按错刷新 |
内置拓扑刷新(ClusterTopologyRefreshOptions) |
| 哨兵 | JedisSentinelPool,主从切换需重建池 |
自动感知,无需重建 |
| MOVED/ASK 重定向 | 客户端处理 | 客户端处理 |
| 连接数 | 每个节点 × 池大小 | 每节点默认 1 条共享连接 |
| 主从切换抖动 | 池内旧连接报错,需重试 | 缓冲命令后重连,抖动更小 |
集群模式下 Jedis 的连接数容易失控:3 主 3 从 × 每节点 32 连接 = 192 条。Lettuce 共享连接后通常只有个位数,这是它在云环境更受欢迎的现实原因。
7. Spring Data Redis 抽象:@RedisHash
@RedisHash(value = "user", timeToLive = 3600)
public class User {
@Id private String id;
@Indexed private String city;
private String name;
}
@RedisHash 走的是 RedisKeyValueAdapter,底层用 RedisTemplate 的 opsForHash(),与客户端无关;@Indexed 会在 user:city 之类的 set 里维护二级索引。注意三点:
| 注意点 | 说明 |
|---|---|
| TTL 单位 | timeToLive 是秒,且依赖 @EnableRedisRepositories 的 keyspaceNotifications 清理索引 |
| 索引维护 | 改字段要重新 save,索引不会自动迁移 |
| 大对象 | 整个对象序列化成 hash,字段多的实体读放大明显 |
它适合"按 id 存取 + 简单二级索引"的场景,不适合复杂查询——那种需求该用 RedisJSON 或干脆回数据库。
8. Pipeline 与事务的写法差异
Pipeline(批量发送,不保证原子):
// Jedis
try (Jedis jedis = pool.getResource()) {
Pipeline p = jedis.pipelined();
p.set("a", "1");
p.incr("b");
List<Object> res = p.syncAndReturnAll();
}
// Lettuce
RedisAsyncCommands<String, String> async = conn.async();
async.setAutoFlushCommands(false);
async.set("a", "1");
async.incr("b");
async.flushCommands();
事务(MULTI/EXEC):
// Jedis
Transaction tx = jedis.multi();
tx.set("a", "1");
tx.incr("b");
List<Object> r = tx.exec();
// Lettuce
RedisCommands<String, String> sync = conn.sync();
sync.multi();
sync.set("a", "1");
sync.incr("b");
sync.exec();
| 维度 | Pipeline | 事务 |
|---|---|---|
| 原子性 | 无 | 有(EXEC 一起执行) |
| 能否读到中间结果 | 不能 | 不能 |
| 适用 | 批量写、批量读 | 需要一起生效的少量命令 |
| 失败语义 | 单条失败不影响其他 | 运行时错误不回滚已执行命令 |
两者在 Lettuce 上都要注意共享连接:事务期间该连接被占用,其他线程的命令会排队。生产上建议给事务单独开一条连接。
9. 连接泄漏排查
$ redis-cli -h redis-host CLIENT LIST | awk '{print $2, $5, $9, $12}' | head
id=184 addr=10.0.3.21:51234 ... age=8421 idle=0 cmd=client|list
| 字段 | 用途 |
|---|---|
age |
连接存活秒数,异常大说明没被回收 |
idle |
空闲秒数,长期 0 且数量增长 → 泄漏 |
cmd |
当前命令,null/`client |
db / name |
区分不同业务,建议用 CLIENT SETNAME 打标 |
$ redis-cli -h redis-host INFO clients
connected_clients:1024
判据:connected_clients 是否随实例数线性增长、GC 后是否回落。Jedis 的泄漏几乎都是"借了没还"——getResource() 必须配 try-with-resources;Lettuce 的泄漏多为"手动 connect() 后没 close()"。
10. 怎么选
| 场景 | 推荐 | 理由 |
|---|---|---|
| Spring Boot 3.x 默认 | Lettuce | 自动装配、共享连接、拓扑刷新 |
| 大量短命令、高 QPS | Lettuce | 多路复用,连接数恒定 |
需要 BLPOP/BRPOP 阻塞消费 |
Jedis 或 Lettuce 的专用连接 | 阻塞会独占连接 |
| 老项目、同步 API、团队熟悉 | Jedis + JedisPool |
心智模型简单,池即并发 |
| 集群规模大、节点多 | Lettuce | 连接数不会随节点数爆炸 |
| 需要精细控制连接池 | Jedis | 池参数直接对应并发上限 |
关键判断:阻塞命令。Jedis 里 BLPOP 会占住借出的那条连接直到返回,池够大就行;Lettuce 共享连接下必须 connection.async() 单独建连或关掉 shareNativeConnection,否则整个应用被一条 BLPOP 卡死。
11. 排错与边界
| 现象 | 原因 | 处理 |
|---|---|---|
Could not get a resource from the pool |
Jedis 池耗尽 | 调 maxTotal,查泄漏,设 maxWait |
RedisCommandTimeoutException |
Lettuce 命令超时 | 查慢命令、网络、commandTimeout 设置 |
一条 BLPOP 后全站变慢 |
共享连接被独占 | 关 shareNativeConnection 或用专用连接 |
| 主从切换后持续报错 | Jedis 哨兵池未重建 | 换 Lettuce,或监听切换事件重建池 |
connected_clients 持续上涨 |
连接泄漏 | CLIENT LIST 看 age,修借还未配对 |
| 边界场景 | 更合适的做法 |
|---|---|
| 单机小流量脚本 | redis-cli 或裸 Jedis,不用池 |
| 超高并发且阻塞命令多 | 独立连接池专供阻塞命令 |
| 跨语言/跨版本 | 直接用 RESP 协议或 redis-py 等原生客户端 |
需要 Redis 7 的新特性(如 FUNCTION) |
确认客户端版本支持,必要时用 sendCommand |
一句话总结:Jedis 把并发交给连接池,Lettuce 把并发交给多路复用;前者连接数=并发上限,后者连接数≈1 但要小心阻塞命令把共享连接吃干净。
12. 自测
- 为什么一个
Jedis实例不能被多线程共用? maxTotal与maxWait哪个不设更容易引发全站故障,为什么?- Lettuce 的
shareNativeConnection=true在什么情况下会变成陷阱? - 集群模式下为什么 Jedis 的连接数容易失控?
- Pipeline 与
MULTI/EXEC在失败语义上有什么区别? CLIENT LIST里哪两个字段最能指示连接泄漏?