高级篇 05. Redis 最佳实践 - 批处理优化 (MSET & Pipeline)

3 阅读5分钟

📚 高级篇 05. Redis 最佳实践 - 批处理优化 (MSET & Pipeline)

一、 核心痛点:被忽视的 RTT (往返时延)

当我们执行一条 Redis 命令(比如 set key value)时,真正消耗时间的分为两部分:

  1. 命令执行时间: Redis 在内存中执行 set 动作。这个极快,通常在微秒级
  2. 网络往返时间 (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),或者你想一次性执行 setsaddhset混合命令,原生批量命令直接歇菜。

三、 终极杀器: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)

  1. 客户端内存撑爆: Pipeline 在发送前,会把 100 万条命令暂存在 Java 进程的内存缓冲里;执行完后,接收的 100 万个响应结果也会瞬间塞满 Java 内存,极易引发 OOM。
  2. 服务端内存飙升: Redis 服务端在处理这 100 万条命令时,需要为这个客户端维持一个巨大的输出缓冲区(Output Buffer)来保存返回结果,这会导致 Redis 内存瞬间飙升。
  3. 阻塞其他请求: 虽然 Pipeline 节省了网络 RTT,但 Redis 执行这 100 万条命令依然是单线程的。在这个巨型 Pipeline 执行期间,其他所有的读写请求全部会被阻塞。

企业级最佳实践: 我们通常会将大批量数据拆分成较小的批次(例如每次打包 500 ~ 1000 条 命令)作为一个 Pipeline 发送,循环发送多个 Pipeline。这样既能享受网络压缩的红利,又能避免撑爆内存和长时间阻塞服务器。”


学习总结

这节课,你掌握了让 Redis 吞吐量产生质变的黑科技——批处理优化

记住核心口诀:同类型 String 批量优先用原生 MSET/MGET(保证原子性),混合类型海量操作用 Pipeline(控制每批 1000 条左右)。

有了这个武器,以后面对“大促预热”、“海量埋点数据入库”等场景,你就能写出性能碾压同行的代码了。