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 一批
相关推荐
UAq6wn76j11 分钟前
.NET源码生成器使用SyntaxTree生成代码及简化语法
java·开发语言·.net
火飞鹰12 分钟前
封装MinIO为starter
java·数据库·spring boot
@atweiwei13 分钟前
Go语言并发编程面试题精讲(上)
java·开发语言·面试·golang·channel
_MyFavorite_18 分钟前
JAVA重点基础、进阶知识及易错点总结(36)Lombok 实战 + 阶段总结
java·开发语言
spencer_tseng32 分钟前
AffineTransform cannot be resolved
java
freejackman33 分钟前
Java从0到1---基础篇
java·开发语言·后端·idea
CQU_JIAKE37 分钟前
4.4【Q】
java·前端·javascript
2301_7717172139 分钟前
Java自定义注解创建详解
java·开发语言
艾莉丝努力练剑1 小时前
【Linux系统:信号】线程安全不等于可重入:深度拆解变量作用域与原子操作
java·linux·运维·服务器·开发语言·c++·学习