2503 字
约 8 分钟
22
S12-X05 · Jedis 与 Lettuce:连接模型与线程安全差异

选客户端的第一个问题不是"哪个快",而是"一个连接能不能被多个线程同时用"。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. 自测

  1. 为什么一个 Jedis 实例不能被多线程共用?
  2. maxTotal 与 maxWait 哪个不设更容易引发全站故障,为什么?
  3. Lettuce 的 shareNativeConnection=true 在什么情况下会变成陷阱?
  4. 集群模式下为什么 Jedis 的连接数容易失控?
  5. Pipeline 与 MULTI/EXEC 在失败语义上有什么区别?
  6. CLIENT LIST 里哪两个字段最能指示连接泄漏?
S12-X05 · Jedis 与 Lettuce:连接模型与线程安全差异
https://www.clxhxhhr.top/posts/5675/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。