在日常工作中,有一些细小问题恰巧是我们忽略的,或者是遇到一些问题不知如何实践,今天我们就探讨下,批处理优化。
在很多时候,特别是在热点数据预热的时候,我们都会一股脑的把大量数据导入到redis缓存中,这种暴力方式真的好吗?
如何优雅的完成海量数据的导入呢?往下来
单体批处理-pipeline
如果大量数据导入redis,一般来说想到的有两种做法,一种是单个命令一次性全部导入,一种是分多次导入。我们分别看看:
- 单个命令执行流程:
- 多次导入的执行流程:
redis处理指令是很快的,主要花费的时候在于网络传输。于是乎很容易想到将多条指令批量的传输给redis
虽然,redis提供了很多mxxx这样的命令,比如mset、hmset,可以实现批量插入数据。但是只能操作部分数据类型,如果有复杂的数据类型处理,就需要用到pipeline了。
简介
Redis Pipeline 是一种用于在客户端与 Redis 服务器之间批量执行多个命令的机制。它旨在减少通信开销,提高性能,特别适用于需要大量命令执行的情况。
通信开销减少
在普通的 Redis 操作中,每个命令都需要客户端与服务器之间的来回通信,这会引入额外的延迟。Pipeline 允许客户端将多个命令一次性发送到服务器,然后服务器会立即返回响应,而不必等待每个命令的响应,从而减少通信开销。
批量操作
Pipeline 非常适用于需要批量处理多个 Redis 命令的场景,例如在一次性设置多个键的值、增加多个计数器等。
原子性
尽管 Pipeline 允许一次性发送多个命令,但它并不提供事务的原子性。如果需要事务支持,Redis 提供了 MULTI 和 EXEC 命令来实现事务。
go语言demo
client := initRedis()
pipe := client.Pipeline()
// 使用Pipeline执行多个命令
pipe.Set(context.Background(), "user:1:name", "miloyang", 0)
pipe.Set(context.Background(), "user:1:age", "30", 0)
pipe.Incr(context.Background(), "counter")
// 执行Pipeline中的所有命令
_, err := pipe.Exec(context.Background())
if err != nil {
fmt.Println("Pipeline execution error:", err)
return
}
做个实验:
func normal(ctx context.Context, redisClient *redis.Client) {
defer RunTimeCalc("normal")()
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("user:%d:name", i)
value := fmt.Sprintf("zhang %d", i)
redisClient.Set(ctx, key, value, 0)
}
}
func pipeline(ctx context.Context, client *redis.Client) {
pipe := client.Pipeline()
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("user:%d:name", i)
value := fmt.Sprintf("zhang %d", i)
pipe.Set(ctx, key, value, 0)
}
_, err := pipe.Exec(context.Background())
if err != nil {
fmt.Println("Pipeline execution error:", err)
return
}
}
同样存储1000个数据:
2023/10/16 10:20:35 normal run time second : 40.197
2023/10/16 10:22:21 pipeline run time second : 0.2693197
完全没有可比性。
集群批处理
mset和pipeline,都是绑定redis节点的,如果是夸节点的集群部署下,那么是根据key来分配哈希槽点,如果不能保证批处理的key在同一槽点,是不能使用mset或者pipeline的。 此时一般来说,会有以下几种解决方案,除了直接普通的那种for循环方式。
串行slot
在客户端计算每个key的slot,将slot一致分为一组,每组都利用pipeline批处理,串行执行各组命令。 这种的耗时是:m次网络耗时 + N次命令耗时 ,其中m为key的插槽数。 优点是好事较短,但是实现比较复杂,slot越多,耗时越久。
并行slot
与串行类似,都是计算出slot分组,但是是并行执行各组命令。 他比串行耗时短些
hash_tag
之前介绍过,redis计算出slot,如果有{},则直接计算{}之中的值,这就是tag。
那么我们可以把所有热点key的name设置相同的hash_tag,那么他们的key肯定是在同一个slot。
这种方式的耗时最短,且实现也比较简单。但是如果说之前没有这么设计,旧项目维护的话,要注意是否有漏改的情况。
缺点就是容易出现数据倾斜。