Pipeline本身不发命令,必须显式调用pipe.Exec(ctx)才触发网络I/O;所有命令仅缓存在客户端内存中,不执行Exec则Redis服务器完全无感知,且重复调用会panic。

client.Pipeline() 本身不发命令,不调 pipe.Exec(ctx) 就等于没操作——这是最常被忽略、也最致命的漏点。
为什么 pipeline.Exec() 必须显式调用
Pipeline 对象只是内存里的命令缓冲区,所有 Set()、ZAdd()、Incr() 调用都只是往 slice 里追加指令,不会触发任何网络 I/O。Redis 服务器压根收不到数据,日志里也看不到请求痕迹。
- 每个
pipe := client.Pipeline()后必须配对一次且仅一次pipe.Exec(ctx) - 重复调用
Exec()会 panic,不是静默失败 - 如果在
Exec()前做了 HTTP 请求或 DB 查询,Pipeline 的“省 RTT”优势就白费了
怎么取每条命令的结果,而不是只看 Exec 的 error
pipe.Exec(ctx) 返回的 error 只代表网络层失败(如连接断开、超时),不代表某条命令执行成功。比如 Set("user:123", "", 0) 中 key 名含空格,或写到了只读副本,Exec() 仍返回 nil,但对应 *redis.StatusCmd 的 Err() 非空。
- 每条命令调用后要存下返回值:
setCmd := pipe.Set(ctx, "k", "v", 0) -
Exec()后,逐个检查:if err := setCmd.Err(); err != nil { ... } - 读操作推荐用
getCmd.Result()(它内部已合并.Val()和.Err()) - 数值类如
Incr()必须用.Int64()或.Uint64(),别用.Val(),否则类型断言 panic
哪些命令能塞进同一个 pipeline,哪些不能
核心判断标准就一条:所有命令是否可乱序执行、彼此不读写同一 key、也不依赖上一条的返回值。满足才上 pipeline;否则要么拆批,要么改 Lua。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 适合:
ZAdd()批量更新排行榜、HSet()写用户资料、Del()清理一批 key、Set()+Expire()缓存预热 - 不适合:
Get()后根据值决定是否Set()、Incr()后立刻用返回值做条件判断、对大 key(>10KB)批量操作(打包后易超proto-max-bulk-len) - 更优替代:读多优先用
MGET(keys),写多优先用MSET(kvs)——它们是 Redis 原生命令,性能更稳,还天然支持集群 slot 分发
集群环境下 pipeline 的实际约束
*redis.ClusterClient 不允许直接调 Pipeline(),go-redis 会 panic。集群模式下跨 slot 命令打包会被拒绝,哪怕两个 key 只差一个字符(如 {user:1}:a 和 {user:2}:b)也可能落在不同节点。
- 正确做法是先算 slot:
slot := clusterClient.KeySlot(key),再查节点:node := clusterClient.NodeForKey(slot),最后在*redis.Client上调node.Pipeline() - 但实操中更推荐绕过手动路由:直接用
MGET()、ZAdd(key, zs)等原生命令,它们内部已做 slot 分片,代码简洁且不易出错 - 别碰
TxPipeline()——它底层带WATCH,性能更低,还容易把 Redis 搞 OOM
真正难的不是写对语法,而是判断「这批命令到底适不适合走 pipeline」:命令间有没有隐式依赖?key 分布是否集中?错误容忍度有多高?线上压测前,务必用真实数据量 + redis-cli --intrinsic-latency 10 测本机延迟基线,再对比不同 batch size 对 P99 的影响。


















