暴怒火柴人
125.74M · 2026-03-23
如果你在 Spring Boot 或直接使用 Redis 客户端时选择了 Lettuce,最容易产生的几个问题通常是:
RedisClient 要不要复用?StatefulRedisConnection 能不能跨线程共享?pipeline 到底有没有用,Spring 里默认为什么看起来“不够快”?这篇文章把这些问题串起来,从 Lettuce 的资源模型、Netty 连接建立、命令发送链路,一直到 Spring Data Redis 中的 pipeline 刷新策略,做一次可直接落地的梳理。
RedisClient / RedisClusterClient 是重量级对象,应该长期复用,而不是每次请求都创建。StatefulRedisConnection 本身是线程安全的,普通命令场景可以复用同一个连接。pipeline 的核心价值是减少 RTT 和系统调用,提高吞吐;它不保证原子性。executePipelined(),如果不调整 flush 策略,也可能仍然接近“每条命令都 flush 一次”。public static void main(String[] args) throws Exception {
RedisClient redisClient = RedisClient.create("redis://localhost:6379");
try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
RedisAsyncCommands<String, String> asyncCommands = connection.async();
asyncCommands.set("foo", "bar").get();
System.out.println(asyncCommands.get("foo").get());
} finally {
redisClient.shutdown();
}
}
这个示例背后有两点很重要:
RedisClient 通常是全局复用的。connect() 得到的 StatefulRedisConnection 代表一个底层 Channel,后续 sync / async / reactive API 都是围绕这条连接展开。ClientResourcesClientResources 是 Lettuce 的全局基础设施,负责管理共享资源,例如:
EventLoopGroupProviderEventExecutorGroup如果你直接创建 RedisClient,底层通常会自动创建 DefaultClientResources。多个连接可以共享这套资源,因此不应该把它当成“每次操作都重新创建一次”的轻量对象。
RedisClientRedisClient / RedisClusterClient 更像是“连接工厂 + 全局资源持有者”:
ClientResourcesBootstrapChannelGroup它应该是应用级别复用的对象。
StatefulRedisConnectionStatefulRedisConnection 对应一条真实的 Redis 连接,底层绑定一个 Netty Channel。
它最重要的两个特征是:
Lettuce 在写命令时会做并发控制,因此多个线程可以复用一个连接发送普通命令。这也是“默认不强调连接池”的关键前提。
Lettuce 对外提供三种常用接口:
async:异步调用,返回 RedisFuturesync:同步阻塞风格,本质上是对 async 结果做等待reactive:响应式封装,适合和 Reactor / WebFlux 集成底层命令发送链路本质一致,只是调用方式不同。
Lettuce 底层基于 Netty。以 RedisClient.connect() 为例,简化后的过程是:
RedisClient.connect()
-> connectStandaloneAsync()
-> connectStatefulAsync(): 创建Netty的Bootstrap 绑定channelGroup,即线程组NioEventLoopGroup(CPU个NioEventLoop)
-> initializeChannelAsync0(): 这里会创建PlainChannelInitializer绑定到Boostrap
-> redisBootstrap.connect()
Lettuce中的PlainChannelInitializer在初始化(initChannel)的时候会将Lettuce中的handler添加到pipeline中 :
RedisHandshakeHandler: 初始化握手操作,会发生Hello命令发送redis server.CommandHandler:
write(): 生成LatencyMeteredCommand 放入stack,耗时统计、tracing 等channelRead --> decode(): 将数据包放入buffer缓冲区,进行解析响应, 解析到完整的数据包时,从stack获取Command,将结果塞进去ConnectionEventTrigger: 当连接发生变化的时候,发送一些eventConnectionWatchdog: 监控每个链接,当断开时支持重连。 inactive 方法CommandEncoder: 将Command 编码为ByteBuf连接可用后,Lettuce 会在握手阶段发送初始化命令(即:RedisHandshakeHandler)。流程包括:
HELLO 3
作用是尝试切换到 RESP3;如果服务端不支持,会退回 RESP2。SELECT
当你配置的不是默认库时,会切换 DB。CLIENT SETINFO
在较新的 Redis 版本中,客户端会把自身信息上报给服务端。如果你在 Spring Boot Actuator 中开启 Redis 健康检查,还会看到 INFO server 这类探测命令 (这个并不是初始化发出的)。
以 asyncCommands.set("foo", "bar") 为例,简化后的调用链可以写成:
AbstractRedisAsyncCommands.set()
-> dispatch()
-> DefaultEndpoint.write()
-> channel.writeAndFlush()
这里 DefaultEndpoint 很关键,它维护了:
ChannelautoFlushCommands 开关false的时候 会将命令存放到这里默认情况下,autoFlushCommands = true,所以命令会直接写入 socket 并 flush。
当你手动关闭自动 flush 时:
connection.setAutoFlushCommands(false);
命令不会立刻发到网络,而是先进入缓冲队列,等你显式调用:
connection.flushCommands();
再统一发出去。
这也是 Lettuce 实现 pipeline 的核心机制。
很多人第一次接触 Redis 客户端时,会下意识地把“高并发”与“连接池”绑定起来。但在 Lettuce 里,这个结论通常并不成立。
原因有三个:
因此,大部分简单 KV、缓存读写、计数器、排行榜等场景,复用少量连接就够了。
更适合专用连接或连接池的场景通常是:
pipelinepipeline 的本质不是“让 Redis 一次并行执行更多命令”,而是减少客户端和服务端之间的来回往返。
没有 pipeline 时,客户端往往是:
这会带来两个额外成本:
read() / write() 系统调用而 pipeline 的做法是:
这样能显著提升吞吐,特别适合批量写入、批量更新、缓存预热等场景。
但它有两个必须记住的边界:
pipeline 不保证原子性,它不是事务。如果你需要“多条命令要么都成功,要么都失败”,应该考虑事务或 Lua 脚本,而不是把 pipeline 当成原子操作。
最直接的方式就是关闭自动 flush:
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisAsyncCommands<String, String> commands = connection.async();
connection.setAutoFlushCommands(false);
try {
commands.set("k1", "v1");
commands.set("k2", "v2");
commands.set("k3", "v3");
connection.flushCommands();
} finally {
connection.setAutoFlushCommands(true);
}
这里有一个非常重要的约束:
不要在“被其他线程共享的连接”上随意关闭 autoFlushCommands。
否则,其他线程发出的命令也可能被一并积压到缓冲区里,直到某次 flush 才真正发出,导致延迟不可控(这也是)。
如果你在 Spring 里使用 StringRedisTemplate.executePipelined(),直觉上会认为“所有命令会一次性发出”。
但默认行为并没有这么激进。
Spring Data Redis 在 Lettuce 之上还包了一层适配。一次 opsForValue().set() 的执行路径,简化后大致是:
RedisTemplate.execute()
-> RedisConnectionUtils.getConnection()
-> LettuceConnection.invoke()
-> AbstractRedisAsyncCommands.set()
-> DefaultEndpoint.write()
而 executePipelined() 的关键流程是:
redisTempalte#executePipelined:
1. LettuceConnection#openPipeline()
-> 将 autoFlushCommands 设为 false
-> pipeliningFlushState:生成一个专用连接
2. 执行业务回调中的 Redis 命令:
LettuceConnection#exec:如果开启了pipeline,执行BufferedFlushing#onCommand 判断是否flush
3. LettuceConnection#closePipeline()
-> flush 剩余命令
-> 读取并反序列化结果
更关键的一点是:Spring 为了避免共享连接被 pipeline 状态污染,通常会为这次 pipeline 使用专用底层连接,执行结束后再释放。 这也是为什么它比“直接在共享连接上关掉 autoFlush”更安全。
Spring 中的 pipeline 默认并不等同于“攒满一批再统一发”。
spring 中默认 flush 策略是 FlushEachCommand,那效果就是“虽然走了 pipeline API,但每条命令还是在及时 flush”。
为了达到真正pipeline效果,需要覆盖默认的flush策略:
LettuceConnectionFactory connectionFactory =
(LettuceConnectionFactory) stringRedisTemplate.getConnectionFactory();
connectionFactory.setPipeliningFlushPolicy(
LettuceConnection.PipeliningFlushPolicy.buffered(3)
);
这个配置的含义是:
如果你的场景是大批量写入,可以把这个阈值调大,例如 100。 但阈值并不是越大越好,仍然要结合:
@Component
public class LettuceFactoryPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof LettuceConnectionFactory factory) {
factory.setPipeliningFlushPolicy(
LettuceConnection.PipeliningFlushPolicy.buffered(100)
);
}
return bean;
}
}
这段配置适合“明确有批量 pipeline 写入需求”的系统。它的目标不是降低单条命令延迟,而是减少 flush 次数和网络开销,提升整体吞吐。
如果从源码角度把关键点串起来,可以得到这样一条主线:
RedisTemplate.executePipelined()
-> connection.openPipeline()
-> LettuceConnection.doInvoke()
-> future.get()
-> pipeline(...)
-> BufferedFlushing.onCommand()
-> 满足阈值后 flushCommands()
BufferedFlushing 的核心逻辑非常直接:
@Override
public void onCommand(StatefulConnection<?, ?> connection) {
if (commands.incrementAndGet() % flushAfter == 0) {
connection.flushCommands();
}
}
也就是说,Spring pipeline 的收益,最终还是落回到一个问题上: “命令什么时候真正 flush 到网络里?”
Lettuce 不是“零成本客户端”。至少要意识到两类资源消耗:
连接本身的资源
Channelpipeline 带来的额外内存
从源码视角看,CommandHandler 在连接注册时就会准备解码用的临时 buffer,用来处理半包、粘包问题。
如果你建立了过多连接,或者单次 pipeline 批量过大,内存占用会很快放大。
适合使用 Lettuce pipeline 的场景:
不适合直接上 pipeline 的场景:
Lettuce 的设计思路并不复杂,可以浓缩成四句话:
RedisClient 是重量级资源,复用它。StatefulRedisConnection 在普通命令场景下可以共享,不要默认上连接池。pipeline 的价值在于减少 RTT 和 flush,不在于保证原子性。executePipelined()。如果把这几个点想清楚,Lettuce 在连接模型、性能调优和 Spring 集成上的大部分问题,基本都能顺下来。