外观
Java 客户端:Lettuce 与 Jedis 的参数怎么配
Spring Boot 2.x 之后默认客户端是 Lettuce(基于 Netty、连接可复用),Jedis 是传统的一连接一线程。两者的池参数含义完全不同,照抄配置一定出问题。
场景:先选客户端
| 维度 | Lettuce | Jedis |
|---|---|---|
| 默认选择 | 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: 30sJedis 的 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-timeout | TCP 建连 | 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,别只调大超时 |
池耗尽,线程全部 WAITING | max-wait: -1 | 设有限值 + 池扩容 |
高并发下 JedisConnectionException: Could not get a resource | max-active 小于并发 | 按并发线程数配 |
应用连不上但 redis-cli 正常 | 密码 / db 号 / 白名单不一致 | 对齐配置,检查 client-name 是否出现在 CLIENT LIST |
Redis 出现大量 name= 空连接 | 没设 client-name | 加上,便于按服务统计 |
切换主库后一直报 READONLY | 客户端用了固定 IP 而非哨兵/集群 | 改配置,或手工刷新 |
key 在 redis-cli 里看不到 | JDK 序列化 | 换 StringRedisSerializer |
反序列化报 ClassNotFoundException | 用了 GenericJackson2JsonRedisSerializer 且类改名 | 用显式类型序列化 |