工作中使用redis的时候遇到一个问题,如果我们要对存储在redis中的一批数据进行操作,为了加快处理的速度,我们一般有两种方式:
引发了一个小思考:这两种方式通常哪个更好点呢?
这个周末有点空,做个调研总结下。写篇博客,希望可以便人便己。
对于redis这种性能瓶颈主要取决于网络IO的网络服务器来说,客户端的每一次网络请求都是对服务端的无情损伤。如果业务场景需要对多个redis数据进行操作,原始的那种“ping-pong” 交互就太慢了。因为每一个操作都需要一个RoundTrip。
因此Redis在2.0版本中引入了Pipeline,在一次请求中携带多个执行命令,这样请求只需要顺着网络跑一次,就可以执行多条命令,大大提高了网络性能。
使用pipeline也有一些注意点,这里简单说明一下:
不仅仅局限于对redis的请求,多线程是解决批处理任务的通用方案。 具体在redis的批处理任务中,使用多个线程,建立多个对redis的访问连接,在每个连接中发起一个或多个对redis key的操作。这样也可以提高整个批处理任务的处理效率。
同学会不会有个疑问 ?
redis6.0版本之前,redis处理逻辑不是单线程的吗(不考虑4.0版本中引入的异步处理的那种伪多线程)?请求发送到服务端,不还是一个一个处理的吗? 这样真的可以提高效率吗?
说的没错,以前版本的redis在处理任务使用的是单线程。但是我这里说的不是服务端的多线程,而是客户端的多线程。充分利用连接池来加快整个批处理任务的处理速度。
不明白?
来,举个栗子。你有10个银行业务需要办理,但是银行目前只有一个工作人员提供服务。
你可以先派一个人拿着一个任务单去银行处理,回来之后再派一个人去处理第二个任务… 这讲的是普通的“ping-pong”方式。
你也可以一下派10个人,每人拿着一个任务单去银行。虽然他们到银行的时候,服务人员也只能一项一项的处理。但是服务人员处理完第一个任务之后就可以理解处理第二个任务,避免了前一种方式来回路途的消耗。 这就是多线程的方式。
效率高低,立见分明。
ps: 当然,你也可以只派出一个人,让他拿着所有的任务。去一次把所有的任务干完才回来。这也就是上面说的pipeline的方式。
那么对于处理批处理任务来说,哪个好点呢?
这里简单做了个性能对比测试。代码如下所示:
func TestMultiThread(threadNum uint32, batchSize uint32, dataNum uint32) {
ctx := context.Background()
taskList := make([]func() error, threadNum)
var index uint32
for index = 0; index < threadNum; index++ {
currentIndex := index
taskList[currentIndex] = func() error {
beg := currentIndex * batchSize
end := (currentIndex + 1) * batchSize
for keyIndex := beg; keyIndex < end; keyIndex++ {
key := fmt.Sprintf("key_%d", keyIndex)
val := fmt.Sprintf("val_%d", keyIndex)
resutl, err := redisClient.Set(ctx, key, val, 0).Result()
if err != nil {
log.WarnContext(ctx, "Set %s fail,err=%v,result=%s", key, err, resutl)
}
}
return nil
}
}
//并发执行所有的taskList,并等待所有的go rountine执行结束
GoAndWait(taskList...)
}
func TestPipeline(loopNum uint32, batchSize uint32, dataNum uint32) {
ctx := context.Background()
pineline := redisClient.Pipeline()
var index uint32
for index = 0; index < loopNum; index++ {
beg := index * batchSize
end := (index + 1) * batchSize
for keyIndex := beg; keyIndex < end; keyIndex++ {
key := fmt.Sprintf("key-%d", keyIndex)
val := fmt.Sprintf("val-%d", keyIndex)
pineline.Set(ctx, key, val, 0)
}
results, err := pineline.Exec(ctx)
if err != nil {
log.WarnContext(ctx, "exec pipeline fail, index=%d,err=%v", index, err)
}
for _, result := range results {
if result.Err() != nil {
log.WarnContext(ctx, "exec set wrong ,result=%v", results)
}
}
}
}
简单介绍一下测试的代码逻辑:对于多线程来说,是发起threadNum个go routine,在每个go routine里执行batchSize个redis的set操作,每个set操作的key不重复。对于pipeline来说,是进行 loopNum次循环,在每个循环中执行 pipeline, 发送batchSize个set操作。 同样的每个请求的key是不重复的。
测试的服务器环境如下:
redis版本 | 服务器CPU数目 | 服务器内存 |
---|---|---|
5.0.7 | 2核 | 4G |
实验结果如下:
多线程:
编号 | threadNum | batchSize | cost |
---|---|---|---|
1 | 1 | 10 | 427.901081ms |
2 | 1 | 100 | 3.823393576s |
3 | 1 | 1000 | 32.557640544s |
4 | 10 | 1 | 122.325158ms |
5 | 10 | 10 | 475.484844ms |
6 | 10 | 100 | 3.985830379s |
pipeline:
编号 | loopNum | batchSize | cost |
---|---|---|---|
7 | 1 | 10 | 116.58025ms |
8 | 1 | 100 | 110.203864ms |
9 | 1 | 1000 | 154.173445ms |
10 | 10 | 1 | 461.689637ms |
11 | 10 | 10 | 424.263616ms |
12 | 10 | 100 | 584.375894ms |
我们来分析一下实验结果:
主菜上了,来点辅料?
总的来说,一般情况下,如果要使用批处理任务的话,pipeline效率要更高点。当然马克思主义告诉我们要辩证的看待问题,实际问题还要实际分析,具体选用什么方式还得看业务需求。 (好久没写博客了,略显生疏。 这篇博客竟写了一天多╮(╯▽╰)╭)
Redis Pipeline介绍及应用
Redis Pipeline这一篇就够了
Redis 多线程网络模型全面揭秘
为什么 Redis 选择单线程模型
做对这 10 点,让你的 Redis 性能更上一层楼!
《Redis设计与实现》