📚 高级篇 05. Redis 最佳实践 - 批处理优化 (MSET & Pipeline)
一、 核心痛点:被忽视的 RTT (往返时延)
当我们执行一条 Redis 命令(比如 set key value)时,真正消耗时间的分为两部分:
- 命令执行时间: Redis 在内存中执行
set动作。这个极快,通常在微秒级。 - 网络往返时间 (RTT - Round Trip Time): 命令从 Java 客户端发到 Redis 服务器,Redis 执行完再把结果返回给 Java 客户端的网络传输时间。这个通常在毫秒级。
💥 For 循环的灾难:
如果你用 for 循环执行 10 万次 set:
- 总耗时 = 10 万次 × (网络 RTT + Redis 执行时间)
- 假设 RTT 是 1 毫秒,10 万次操作光是等网络传输就要耗费 100 秒!
- 在这 100 秒内,Java 线程一直在傻等,不仅极其低效,还会白白占用大量的 TCP 连接资源。
为了消灭这 10 万次的网络往返,我们必须采用批处理(Batch Processing) 。
二、 破局方案一:原生批量命令 (mset / mget)
Redis 官方早就想到了这个问题,所以为 String 类型提供了原生的批量操作命令:
- 批量写:
MSET key1 value1 key2 value2 ... - 批量读:
MGET key1 key2 ...
👍 优势:
- 绝对的原子性: 原生命令,要么全成功,要么全失败。
- 极速: 10 万条数据,只需发送 1 次网络请求,服务端内部一次性处理完再返回。耗时从 100 秒瞬间降到 1 秒以内!
👎 致命局限性:
- 数据类型受限:
mset/mget只能用于String类型!如果你想批量写入Hash(虽然有hmset,但也只能操作同一个 key 的多个 field),或者你想一次性执行set、sadd、hset等混合命令,原生批量命令直接歇菜。
三、 终极杀器:Pipeline (流水线技术)
为了打破原生命令的局限性,Redis 客户端提供了一种极其强大的黑科技——Pipeline (流水线) 。
🌟 什么是 Pipeline?
Pipeline 并不是 Redis 服务端提供的某条特定命令,而是客户端与服务端之间的一种网络通信约定。
它的核心思想是:Java 客户端把这 10 万条乱七八糟的命令(管你是 String 还是 Hash)打包成一个巨大的“数据包”,一次性发给 Redis 服务器。Redis 收到后,在内存里一口气把这 10 万条命令全执行完,然后把所有的结果打包成一个数组,一次性返回给客户端。
形象比喻:
- For 循环: 像送快递。送一件,回网点一趟;送 10 万件,跑 10 万趟。(累死在路上)
- Pipeline: 像大卡车拉货。把 10 万件快递装满一辆大卡车,一次性开到目的地卸货。(效率百倍提升)
四、 Java 代码实战:Spring Boot 整合 Pipeline
在 Spring Boot 中,使用 Pipeline 极其优雅,底层的 RedisTemplate 已经为我们封装好了 executePipelined 方法。
演示:使用 Pipeline 批量插入 10 万条 Hash 数据
Java
@SpringBootTest
public class PipelineTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testPipeline() {
long start = System.currentTimeMillis();
// 使用 executePipelined 开启流水线
List<Object> results = stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection stringRedisConn = (StringRedisConnection) connection;
// 循环生成命令,但此时命令不会立刻发送!而是被暂存在客户端的缓冲区中
for (int i = 1; i <= 100000; i++) {
stringRedisConn.hSet("heima:user:" + i, "name", "jack" + i);
stringRedisConn.hSet("heima:user:" + i, "age", "18");
}
// 注意:这里必须返回 null
return null;
});
long end = System.currentTimeMillis();
System.out.println("Pipeline 耗时:" + (end - start) + " 毫秒"); // 通常在几百毫秒内就能完成!
}
}
五、 大厂高频拷问:Pipeline 的避坑指南
不要以为掌握了 Pipeline 就可以天下无敌地把几百万条命令往里塞。面试官一定会挖坑问你:
🗣️ 面试官发难: “我有一个包含 100 万条指令的集合,我可以直接用一个 Pipeline 一次性发给 Redis 吗?”
💡 你的满分避坑解析:
“绝对不可以!必须进行分批打包(Chunking) 。
- 客户端内存撑爆: Pipeline 在发送前,会把 100 万条命令暂存在 Java 进程的内存缓冲里;执行完后,接收的 100 万个响应结果也会瞬间塞满 Java 内存,极易引发 OOM。
- 服务端内存飙升: Redis 服务端在处理这 100 万条命令时,需要为这个客户端维持一个巨大的输出缓冲区(Output Buffer)来保存返回结果,这会导致 Redis 内存瞬间飙升。
- 阻塞其他请求: 虽然 Pipeline 节省了网络 RTT,但 Redis 执行这 100 万条命令依然是单线程的。在这个巨型 Pipeline 执行期间,其他所有的读写请求全部会被阻塞。
企业级最佳实践: 我们通常会将大批量数据拆分成较小的批次(例如每次打包 500 ~ 1000 条 命令)作为一个 Pipeline 发送,循环发送多个 Pipeline。这样既能享受网络压缩的红利,又能避免撑爆内存和长时间阻塞服务器。”
学习总结
这节课,你掌握了让 Redis 吞吐量产生质变的黑科技——批处理优化。
记住核心口诀:同类型 String 批量优先用原生 MSET/MGET(保证原子性),混合类型海量操作用 Pipeline(控制每批 1000 条左右)。
有了这个武器,以后面对“大促预热”、“海量埋点数据入库”等场景,你就能写出性能碾压同行的代码了。