Redis Pipeline 实战:Spring Data Redis 批量写入最佳实践

一、背景

在高并发系统中,如果对 Redis 进行大量写操作,网络 RTT会成为主要性能瓶颈。

举个一个简单场景:

用户给作品打标签,需要给 Redis 的 Tag 热度排行榜(ZSet) 进行计数。

如果每次操作都单独请求 Redis,会产生大量网络开销。如果我们使用 Redis 提供了 Pipeline(管道)机制,允许一次发送多条命令,大幅减少网络往返次数。

举例,假设我们有这样一个业务:

  • 一个作品绑定多个 tag

  • Redis 中维护 tag 的 热度排行

Redis 数据结构:

复制代码
key: gallery:tag:hot
type: ZSET

member = tagId
score = tag热度

Java中普通写法(非批量):

java 复制代码
for (GalleryArtworkTag tag : tagList) {
    stringRedisTemplate.opsForZSet()
        .incrementScore(RedisConstant.GALLERY_TAG_HOT_KEY, tag.getTagId(), 1);
}

执行流程:

java 复制代码
Java -> Redis  (incrby tag1)
Java -> Redis  (incrby tag2)
Java -> Redis  (incrby tag3)
Java -> Redis  (incrby tag4)
...

每次操作都会产生:

java 复制代码
请求 -> Redis
响应 <- Redis

可见,系统响应瓶颈就在 网络往返时间

如果这时候我们使用Pipeline,执行流程变成:

java 复制代码
Java -> Redis  (cmd1 cmd2 cmd3 cmd4 ...)
Redis -> Java  (result1 result2 result3 ...)

网络往返次数从N 次变为了1 次,大幅提高吞吐量。

二、Redis Pipeline 原理

Redis Pipeline 的核心思想是:

客户端一次性发送多条命令,不等待响应,Redis 执行完后再统一返回结果。

三、Pipeline 写法

1、示例:

我们可以这样子开启管道操作,然后按需写redis命令进行批量操作:

java 复制代码
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    // redis命令
    return null;
});

这里结合我之前给的例子,给出他的实战代码:

java 复制代码
List<GalleryArtworkTag> tagList = galleryArtworkDTO.getTagIds().stream()
        .map(id -> GalleryArtworkTag.builder()
                .artworkId(galleryArtworkId)
                .tagId(id)
                .build())
        .collect(Collectors.toList());

if (!tagList.isEmpty()) {
    stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        // key
        final byte[] keyBytes =
                RedisConstant.GALLERY_TAG_HOT_KEY.getBytes(StandardCharsets.UTF_8);
        // 批量准备数据
        tagList.forEach(tag -> {
            final byte[] memberBytes =
String.valueOf(tag.getTagId()).getBytes(StandardCharsets.UTF_8);
            // 执行zset的自增操作,这里的RedisCommandsProvider结构是zset
            connection.zSetCommands().zIncrBy(keyBytes, 1.0, memberBytes);
        });
        return null;
    });
}

2、RedisCommandsProvider 结构:

我们可以看源码,这里提供了很多种,意味着Pipeline 内可以操作 所有 Redis 数据结构

java 复制代码
connection.stringCommands()
connection.hashCommands()
connection.listCommands()
connection.setCommands()
connection.zSetCommands()

3、各数据结构 Pipeline 写法

String 批量写入

普通写法:

java 复制代码
for (User user : users) {
    stringRedisTemplate.opsForValue().set("user:" + user.getId(), user.getName());
}

Pipeline 写法:

java 复制代码
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    users.forEach(user -> {
        byte[] key = ("user:" + user.getId()).getBytes();
        byte[] value = user.getName().getBytes();
        connection.stringCommands().set(key, value);
    });
    return null;
});

当然还有一些其他常用的方法:

Hash 批量写入

普通写法:

java 复制代码
hashOps.put("user:1", "name", "Tom");
hashOps.put("user:1", "age", "18");

Pipeline 写法:

java 复制代码
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {

    byte[] key = "user:1".getBytes();

    connection.hashCommands().hSet(key, "name".getBytes(), "Tom".getBytes());
    connection.hashCommands().hSet(key, "age".getBytes(), "18".getBytes());

    return null;
});
List 批量写入

普通写法:

java 复制代码
listOps.rightPush("msg:list", msg);

Pipeline 写法:

java 复制代码
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    messages.forEach(msg -> {
        connection.listCommands().rPush(
                "msg:list".getBytes(),
                msg.getBytes()
        );
    });
    return null;
});
Set 批量写入

普通写法:

java 复制代码
setOps.add("online:user", userId);

Pipeline 写法:

java 复制代码
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    users.forEach(userId -> {
        connection.setCommands().sAdd(
                "online:user".getBytes(),
                userId.getBytes()
        );
    });
    return null;
});
ZSet 批量写入(排行榜)

普通写法:

java 复制代码
zSetOps.incrementScore("rank", userId, 1);

Pipeline 写法:

java 复制代码
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {

    users.forEach(userId -> {

        connection.zSetCommands().zIncrBy(
                "rank".getBytes(),
                1,
                userId.getBytes()
        );
    });

    return null;
});
原生写法写入

四、注意

  • Pipeline 不保证事务
    • 他不是事务,只是批量发送数据
    • 如果要保证需要MULTI、EXEC
  • 不适合超大批量
    • 如果大批量比如10w,客户端内存堆积、Redis阻塞
    • 建议是100 ~ 1000 一批
相关推荐
KaKa_大王8 分钟前
关于秒杀项目的一些理解
java·学习
tomla17 分钟前
使用Java VisualVM观察过期对象引用现象
java
M1A123 分钟前
Spring Boot YAML 配置读取完全指南:从基础到微服务
java
古法安卓29 分钟前
Android-SELinux 策略调试实战:从 AVC 日志到策略修复
android·java·android studio
todoitbo32 分钟前
飞算JavaAI的多租户权限隔离实测
java·springboot·ai编程·java开发·飞算javaai·java代码生成
星空32 分钟前
Springboot复习
java·spring boot·spring
Dicky-_-zhang42 分钟前
大模型部署架构:从推理引擎到弹性扩缩容的工程实践
java·jvm
vHelios1 小时前
【电商项目】商品搜索开发复盘(1):根据需求拆解搜索接口的设计逻辑
java·微服务·es
极创信息1 小时前
国产化信创适配认证高频术语:信创适配、软件自主可控、国产化率、代码溯源率、代码自主率、代码开源率是什么?
java·python·struts·eclipse·开源·php·hibernate
Data_Journal1 小时前
什么是 CAPTCHA,它是如何工作的?
java·大数据·服务器·前端·数据库