我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录Redis 从 0 到 1二十四1 万次 GET 从 800ms 降到 9ms——Pipeline 的 Go 压测一、现象循环里 1 万次 GET接口 800ms二、三种批量方式压测对比收藏点 1三、Pipeline 的两个误区收藏点 2四、Go 实操批量操作模板收藏点 3五、命令清单与排查步骤六、总结Redis 从 0 到 1二十四1 万次 GET 从 800ms 降到 9ms——Pipeline 的 Go 压测上一篇《Redis 从 0 到 1二十三一个热 key 打到 10 万 QPS——hotkey 与本地缓存的 Go 实现》我们用二级缓存卸掉了单点压力链接https://blog.csdn.net/2501_92769340/article/details/167399896 。本篇优化另一个看不见的开销RTT网络往返时间。一、现象循环里 1 万次 GET接口 800ms某个批量查询接口for 循环里逐条GET user:1001、GET user:1002……1 万次。每条命令本身 0.05ms 就执行完了但接口耗时 800ms——96% 的时间花在网络上。本地 Docker 环境 RTT 约 0.08ms1 万次就是 800ms。这就是 RTT 税每条命令 1 次发送 1 次等待 1 次接收。命令越快RTT 占比越高。解法有两个MGET单条多 key和 Pipeline批量打包。二、三种批量方式压测对比收藏点 1用第 5 篇的压测思路改造对比 1 万次读方式耗时本地实测量级原理逐条 GET × 10000~800ms1 万次 RTTMGET 一次~2ms单条命令带 1 万个 key1 次 RTTPipeline 100 条/批~9ms100 次 RTT客户端服务端都不等单条MGET 看着碾压但注意它的限制key 必须在同一个槽Cluster 下第 10 篇 CROSSSLOT而且 MGET 的参数列表太长也有解析开销。Pipeline 是通用解。三、Pipeline 的两个误区收藏点 2误区 1Pipeline 是并行的。不是。Pipeline 只是把多条命令一次性发过去、一次性收回来Redis 仍按顺序单线程执行第 5 篇的模型。它省的是 RTT不是执行时间。所以 100 条慢命令打包总耗时还是 100 条之和。误区 2Pipeline 是原子的。也不是。打包的 100 条之间别的客户端的命令可能插进来。要原子性请用 WATCH/MULTI第 13 篇或单命令。还有一个实际约束大包会抬高所有人的延迟。1 万条打包成一个大包主线程处理这批的几毫秒里别人都在等——所以生产上按 100~500 条分批第 5 篇压测里 Pipeline(100) 的 P99 3.5ms 就是这个量级。四、Go 实操批量操作模板收藏点 3packagemainimport(contextfmttimegithub.com/redis/go-redis/v9redis-0to1/redisx)// BatchGet 分批 Pipeline 读取通用模板batchSize 建议 100~500funcBatchGet(ctx context.Context,rdb*redis.Client,keys[]string,batchSizeint)(map[string]string,error){result:make(map[string]string,len(keys))forstart:0;startlen(keys);startbatchSize{end:startbatchSizeifendlen(keys){endlen(keys)}batch:keys[start:end]cmds,err:rdb.Pipelined(ctx,func(pipe redis.Pipeliner)error{for_,k:rangebatch{pipe.Get(ctx,k)}returnnil})iferr!nilerr!redis.Nil{// redis.Nil 表示部分 key 不存在returnnil,fmt.Errorf(批次 %d-%d 失败: %w,start,end,err)}fori,cmd:rangecmds{val,err:cmd.(*redis.StringCmd).Result()iferrnil{result[batch[i]]val}}}returnresult,nil}// BatchSet 同理写操作也可以 Pipeline但注意失败时的部分成功问题funcBatchSet(ctx context.Context,rdb*redis.Client,kvmap[string]string,ttl time.Duration,batchSizeint)error{keys:make([]string,0,len(kv))fork:rangekv{keysappend(keys,k)}forstart:0;startlen(keys);startbatchSize{end:min(startbatchSize,len(keys))_,err:rdb.Pipelined(ctx,func(pipe redis.Pipeliner)error{for_,k:rangekeys[start:end]{pipe.SetEx(ctx,k,kv[k],ttl)}returnnil})iferr!nil{returnfmt.Errorf(批次 %d-%d 部分失败: %w需对账补写,start,end,err)}}returnnil}funcmain(){rdb,err:redisx.NewClient(redisx.Config{Addr:127.0.0.1:6379,Password:redis123,PoolSize:20})iferr!nil{panic(err)}ctx:context.Background()// 准备 1 万个 keykeys:make([]string,10000)fori:rangekeys{keys[i]fmt.Sprintf(user:%d,100000i)}rdb.Pipelined(ctx,func(pipe redis.Pipeliner)error{for_,k:rangekeys{pipe.SetEx(ctx,k,v,time.Hour)}returnnil})start:time.Now()m,err:BatchGet(ctx,rdb,keys,200)iferr!nil{panic(err)}fmt.Printf(1 万次读取Pipeline 200/批: %v命中 %d\n,time.Since(start),len(m))// 典型输出1 万次读取: 8~12ms命中 10000 —— 对比逐条的 ~800ms}写 Pipeline 的注意事项Pipelined返回时整批已执行错误检查要逐条看部分成功是常态对一致性要求高的批量写宁可慢也要逐条或走 MULTI。五、命令清单与排查步骤命令/指标作用MGET/MSET同槽批量Cluster 受限Pipeline 分批 100~500通用批量client-output-buffer-limit防客户端大包把输出缓冲打爆slowlog INFO commandstats验证大包期间没有误伤别人排查批量接口慢算理论值条数 × RTTredis-cli --latency测 RTT≈ 实际耗时是 → RTT 税上 Pipeline上 Pipeline 后还慢 → 看批次大小太大 P99 高和池大小并发批量时连接不够仍慢 → 慢命令问题回第 21 篇。六、总结快命令的瓶颈常是 RTT 不是执行1 万次 GET 800ms 里 96% 是网络MGET 最快但受同槽限制Pipeline 是通用解分批 100~500Pipeline 不并行、不原子——它只省 RTT批量写要防部分成功失败要有对账补写。下一篇预告命令快了连接别拖后腿——下一篇《连接池又打满了——PoolSize 公式、泄漏排查与 PoolStats》我们把拍脑袋配池变成一道公式。