Skip to content

Java 客户端:Lettuce 与 Jedis 的参数怎么配 ​

Spring Boot 2.x 之后默认客户端是 Lettuce(基于 Netty、连接可复用),Jedis 是传统的一连接一线程。两者的池参数含义完全不同,照抄配置一定出问题。

场景:先选客户端 ​

维度LettuceJedis
默认选择Spring Boot 2.x+ 自带需显式 client-type: jedis
连接模型单连接多路复用,线程安全一个连接同一时刻只能一个命令
阻塞命令必须用独立连接天然独占,池里借一个即可
依赖Netty无额外依赖
集群/哨兵原生支持,自动刷新拓扑支持,但拓扑刷新弱
池默认不需要池必须用池

新项目选 Lettuce。只有在"大量阻塞命令(BLPOP)"或"团队已深度使用 Jedis"时选 Jedis。

一、依赖 ​

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Lettuce 池化必需,Boot 不会自动带 -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>

忘了 commons-pool2

配了 spring.data.redis.lettuce.pool.* 但没引 commons-pool2,启动时不会报错,配置静默失效——实际还是单连接。排查连接数上不去时先查这个依赖。

二、Lettuce 单机配置 ​

yaml
spring:
  data:
    redis:
      host: 10.0.0.11
      port: 6379
      password: YourStrongPass
      database: 0
      timeout: 2000ms            # 命令执行超时
      connect-timeout: 2000ms    # 建连超时
      client-name: order-service # 会出现在 Redis 的 CLIENT LIST 里
      lettuce:
        pool:
          enabled: true
          max-active: 32
          max-idle: 16
          min-idle: 4
          max-wait: 1000ms
        shutdown-timeout: 200ms

四个池参数:

参数含义配错的表现
max-active最大连接数太小 → 高并发下排队;太大 → 打满 Redis maxclients
max-idle最大空闲连接建议等于 max-active 的 1/2,避免频繁创建销毁
min-idle最小空闲连接为 0 时低峰后连接全断,突发流量要重建
max-wait借连接最长等待-1 = 无限等(千万别),会导致线程全卡死

max-active 怎么估:峰值 QPS × 平均命令耗时(秒) ÷ 单连接可承载并发。Lettuce 单连接能扛几千 QPS(多路复用),所以 32 通常够;真正需要池是因为要支持事务、阻塞命令和 pipeline 隔离。

三、Lettuce 哨兵与集群 ​

yaml
# 哨兵
spring:
  data:
    redis:
      password: YourStrongPass
      sentinel:
        master: mymaster
        nodes: 10.0.0.11:26379,10.0.0.12:26379,10.0.0.13:26379
        password: YourStrongPass   # 哨兵本身没密码时删掉这行
yaml
# 集群
spring:
  data:
    redis:
      password: YourStrongPass
      cluster:
        nodes: 10.0.0.11:7000,10.0.0.12:7002,10.0.0.13:7004
        max-redirects: 3
      lettuce:
        cluster:
          refresh:
            adaptive: true           # 拓扑变化时自动刷新
            period: 30s              # 兜底定期刷新

cluster.nodes 只需写一部分

客户端连上任意一个节点后会通过 CLUSTER NODES 拿到全量拓扑。写 3 个是为了容错,不用把所有 6 个都写上。

四、Jedis 配置 ​

yaml
spring:
  data:
    redis:
      client-type: jedis
      host: 10.0.0.11
      port: 6379
      password: YourStrongPass
      timeout: 2000ms
      connect-timeout: 2000ms
      jedis:
        pool:
          max-active: 64
          max-idle: 32
          min-idle: 8
          max-wait: 2000ms
          time-between-eviction-runs: 30s

Jedis 的 max-active 要按并发线程数估:一个线程同一时刻占用一个连接,所以 max-active ≥ 峰值并发线程数。这和 Lettuce 完全不同。

Jedis 池的 max-wait 建议设 1000~3000ms,绝不能是 -1:

yaml
# 反例:连接池耗尽时所有请求无限等待,线程池打满,服务假死
jedis:
  pool:
    max-wait: -1ms

五、序列化:Redis 里出现乱码 key ​

默认 RedisTemplate 用 JDK 序列化,存进去的 key 是 \xac\xed\x00\x05t\x00...:

bash
redis-cli -a '密码' keys '*'
text
1) "\xac\xed\x00\x05t\x00\x12order:1001:status"
2) "\xac\xed\x00\x05t\x00\x0fuser:8891:name"

redis-cli 里看不见、别的语言读不了、--bigkeys 统计也不准。改成字符串序列化:

java
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> t = new RedisTemplate<>();
        t.setConnectionFactory(factory);

        StringRedisSerializer keySer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valSer = new GenericJackson2JsonRedisSerializer();

        t.setKeySerializer(keySer);
        t.setHashKeySerializer(keySer);
        t.setValueSerializer(valSer);
        t.setHashValueSerializer(valSer);
        t.afterPropertiesSet();
        return t;
    }
}

或者直接用 StringRedisTemplate(key/value 都是 String),在应用层自己做 JSON。

GenericJackson2JsonRedisSerializer 会写入 @class

它会在 JSON 里加 "@class":"com.foo.Bar",好处是能反序列化回原类型,坏处是类改名后历史数据反序列化失败。跨语言或多版本共存时,用 Jackson2JsonRedisSerializer<T> 显式指定类型。

六、一段可直接抄的读写代码 ​

java
@Service
public class OrderCache {

    private final StringRedisTemplate redis;

    public OrderCache(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public void save(Order order) {
        String key = "order:" + order.getId();
        redis.opsForValue().set(key, toJson(order), Duration.ofMinutes(30));
    }

    public Optional<Order> find(long id) {
        String v = redis.opsForValue().get("order:" + id);
        return Optional.ofNullable(v).map(this::fromJson);
    }

    // 缓存击穿保护:只让一个线程回源
    public Order findOrLoad(long id, Supplier<Order> loader) {
        String key = "order:" + id;
        String v = redis.opsForValue().get(key);
        if (v != null) return fromJson(v);

        String lockKey = "lock:order:" + id;
        Boolean got = redis.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
        try {
            if (Boolean.TRUE.equals(got)) {
                Order o = loader.get();
                if (o != null) {
                    redis.opsForValue().set(key, toJson(o), Duration.ofMinutes(30));
                }
                return o;
            }
            Thread.sleep(50);
            return findOrLoad(id, loader);   // 简化写法,生产要限重试次数
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return loader.get();
        } finally {
            if (Boolean.TRUE.equals(got)) redis.delete(lockKey);
        }
    }

    private String toJson(Order o) { /* Jackson */ return "{}"; }
    private Order fromJson(String s) { /* Jackson */ return new Order(); }
}

七、阻塞命令必须用独立连接 ​

Lettuce 默认所有命令走同一条共享连接。一旦执行 BLPOP key 0,这条连接就被占住,整个应用的其它 Redis 命令全部排队。

java
// 错误:用共享连接做阻塞读,会把整个服务拖死
redis.opsForList().leftPop("queue:job", Duration.ofSeconds(10));

// 正确:拿一条专用连接
@Bean(destroyMethod = "close")
public RedisConnection blockingConn(RedisConnectionFactory factory) {
    return factory.getConnection();
}

public void consume() {
    while (true) {
        List<byte[]> kv = blockingConn.bPop("queue:job".getBytes(), 10);
        if (kv != null) handle(kv.get(1));
    }
}

Jedis 从池里借连接做阻塞命令是安全的,但要注意:

  • timeout 必须大于阻塞时长,否则 BLPOP 10 会被 2 秒的命令超时打断。
  • 阻塞期间连接被独占,池容量要按"阻塞线程数"留足。

八、超时怎么设 ​

三条超时不要混:

配置管什么建议
connect-timeoutTCP 建连1000~2000ms
timeout(命令超时)发命令到收到响应1000~3000ms,阻塞命令要放大
池 max-wait从池里借连接的等待时间1000~3000ms

命令超时设太短会"超时了但命令执行了"

Redis 是单线程串行执行的。客户端 2 秒超时后放弃,命令可能还在服务端队列里,之后照常执行。所以:

  • 写操作要幂等(SET、HSET 天然幂等;INCR 不幂等——超时重试会多加一次)。
  • 重试次数限制在 1~2 次,配合退避。
  • 大 key 操作不要靠调大超时硬扛,要拆。

九、踩过的坑 ​

现象根因处置
配置了池但连接数总是 1没引 commons-pool2,池配置静默失效补依赖,日志确认 LettucePoolingConnectionProvider
一个 BLPOP 让全站超时Lettuce 共享连接被阻塞阻塞命令用独立连接
TimeoutException 偶发命令本身慢(大 key / 慢 Lua)查 slowlog,别只调大超时
池耗尽,线程全部 WAITINGmax-wait: -1设有限值 + 池扩容
高并发下 JedisConnectionException: Could not get a resourcemax-active 小于并发按并发线程数配
应用连不上但 redis-cli 正常密码 / db 号 / 白名单不一致对齐配置,检查 client-name 是否出现在 CLIENT LIST
Redis 出现大量 name= 空连接没设 client-name加上,便于按服务统计
切换主库后一直报 READONLY客户端用了固定 IP 而非哨兵/集群改配置,或手工刷新
key 在 redis-cli 里看不到JDK 序列化换 StringRedisSerializer
反序列化报 ClassNotFoundException用了 GenericJackson2JsonRedisSerializer 且类改名用显式类型序列化