Redis - Pipeline执行多个命令的方式
单个命令多次执行
没有使用Pipeline的交互如下:
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"time"
)
func main() {
// 创建Redis客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
// 单个命令执行
start := time.Now()
for i := 0; i < 1000; i++ {
rdb.Ping(context.Background()).Result()
}
elapsed := time.Since(start)
fmt.Printf("Elapsed time for single command execution: %s\n", elapsed)
}
一次性使用管道(Pipelined)
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"time"
)
func main() {
// 创建Redis客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
// 一次性使用管道(Pipelined)
start := time.Now()
pipe := rdb.Pipeline()
for i := 0; i < 1000; i++ {
pipe.Ping(context.Background())
}
_, err := pipe.Exec(context.Background())
if err != nil {
panic(err)
}
elapsed := time.Since(start)
fmt.Printf("Elapsed time for pipelined execution: %s\n", elapsed)
}
事务管道(TxPipelined)
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"time"
)
func main() {
// 创建Redis客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
// 事务管道(TxPipelined)
start := time.Now()
pipe := rdb.TxPipeline()
for i := 0; i < 1000; i++ {
pipe.Ping(context.Background())
}
_, err := pipe.Exec(context.Background())
if err != nil {
panic(err)
}
elapsed := time.Since(start)
fmt.Printf("Elapsed time for transactional pipelined execution: %s\n", elapsed)
}
对比压测:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
const (
redisAddr = "localhost:6379"
)
func singleCommand() {
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: redisAddr,
})
start := time.Now()
for i := 0; i < 1000; i++ {
client.Set(ctx, fmt.Sprintf("key_one_%d", i), "value1", 0)
}
elapsed := time.Since(start)
fmt.Printf("Single command time: %v\n", elapsed)
}
func pipelinedCommand() {
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: redisAddr,
})
start := time.Now()
pipe := client.Pipeline()
for i := 0; i < 1000; i++ {
pipe.Set(ctx, fmt.Sprintf("key_two_%d", i), "value2", 0)
}
_, err := pipe.Exec(ctx)
if err != nil {
fmt.Println(err)
}
elapsed := time.Since(start)
fmt.Printf("Pipelined command time: %v\n", elapsed)
}
func transactionCommand() {
ctx := context.Background()
client := redis.NewClient(&redis.Options{
Addr: redisAddr,
})
start := time.Now()
tx := client.TxPipeline()
for i := 0; i < 1000; i++ {
tx.Set(ctx, fmt.Sprintf("key_three_%d", i), "value3", 0)
}
_, err := tx.Exec(ctx)
if err != nil {
fmt.Println(err)
}
elapsed := time.Since(start)
fmt.Printf("TxPipelined command time: %v\n", elapsed)
}
func main() {
singleCommand()
pipelinedCommand()
transactionCommand()
}
管道与事务管道的区别
Pipelined和TxPipelined都用于批量处理Redis命令,但有一些关键区别:
1)管道(Pipelined):
Pipelined用于打包一组Redis命令,但它们并不是作为一个事务一起发送的。而是在一个网络连接上,将所有命令一次性发送给Redis服务器,并尽可能快地获取所有命令的结果。
管道在客户端和服务器之间建立了一条通道,通过这个通道可以连续发送和接收多个命令,而无需等待上一个命令的响应。
管道执行的命令不具有原子性,如果其中某个命令失败,它不会影响其他命令的执行。
2)事务管道(TxPipelined):
TxPipelined用于将一组Redis命令打包成一个事务(Transaction),然后一次性地将这个事务发送给Redis服务器。
事务是一种原子性操作,要么所有命令都成功执行,要么全部失败回滚,保证了原子性。
在执行事务期间,其他客户端无法插入命令到事务之间,确保了事务的完整性。
事务执行完成后,可以通过Exec方法来执行事务并获取结果。
Pipeline 限制
在使用 Redis 管道技术时,要注意一些限制,避免踩坑:
- Pipeline 不能保证原子性 - Pipeline 只是将客户端发送命令的方式改为批量发送,而服务端在接收到 Pipeline 发来的命令后,将其拆解为一条条命令,然后依然是串行执行。执行过程中,服务端有可能执行其他客户端的命令,所以无法保证原子性。如需保证原子性,可以考虑使用事务或 Lua 脚本。
- Pipeline 不支持回滚 - Pipeline 没有事务的特性,如果待执行命令的前后存在依赖关系,请勿使用 Pipeline。
- Pipeline 命令不宜过大 - 使用管道发送命令时,Redis Server 会将部分请求放到缓存队列中(占用内存),执行完毕后一次性发送结果。如果需要发送大量的命令,会占用大量的内存,因此应该按照合理数量分批次的处理。
- Pipeline 不支持跨 slot 访问 - Redis 集群模式下禁止跨 slot 操作。
总结
1)Pipeline 主要是一种网络优化。本质上意味着客户端缓冲一堆命令并一次性将它们发送到服务器。这些命令不能保证在事物中执行,这样做的好处是节省每个命令的网络往返时间(RTT)。管道比单个命令执行要快很多,Redis 的事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作。在 Redis 事务中,如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。这是因为 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。需要注意的是,如果 Redis 事务中的命令执行失败,可能会导致数据不一致或其他问题。
2)管道与事务管道的主要区别在于:事务通道提供了原子性操作,而管道则只是在客户端和服务器之间建立了更有效的通信通道。在需要保证一组操作的原子性时,应该使用事务通道。而在需要提高吞吐量和降低延迟的情况下,使用管道是一个不错的选择。
3)在Redis中,如果客户端使用管道发送了多条命令,那么服务器就会将多条命令放入一个队列中,这一操作会消耗一定的内存,所以管道中命令的数量并不是越大越好(太大容易撑爆内存),而是应该有一个合理的值。