前言
工作中使用redis的时候遇到一个问题,如果我们要对存储在redis中的一批数据进行操作,为了加快处理的速度,我们一般有两种方式:
- 建立多个线程,使用多个连接发送请求
- 使用redis提供的pipeline
引发了一个小思考:这两种方式通常哪个更好点呢?
这个周末有点空,做个调研总结下。写篇博客,希望可以便人便己。
redis批处理任务,使用多线程还是 pipeline引发的思考
- 前言
- 一、关于pipeline方式
- 二、关于多线程方式
- 三、pipeline vs 多线程
- 四、其他
- 五、总结
- 参考
一、关于pipeline方式
对于redis这种性能瓶颈主要取决于网络IO的网络服务器来说,客户端的每一次网络请求都是对服务端的无情损伤。如果业务场景需要对多个redis数据进行操作,原始的那种“ping-pong” 交互就太慢了。因为每一个操作都需要一个RoundTrip。
因此Redis在2.0版本中引入了Pipeline,在一次请求中携带多个执行命令,这样请求只需要顺着网络跑一次,就可以执行多条命令,大大提高了网络性能。
由下图可以清晰的看出区别来。
使用pipeline也有一些注意点,这里简单说明一下:
- 不要往pipeline里塞过多的命令,否则会撑满客户端的缓存。因为pipeline 在exec之前都是缓存再客户端的。
- pipeline里的命令是依次执行的,但是在服务端执行的时候可能会穿插着其他客户端的请求。
- pipeline缓冲的指令不能保证原子性,如果执行中间某一个指令发生异常,将会继续执行后续的指令。
- cluster并不支持pipeline操作
二、关于多线程方式
不仅仅局限于对redis的请求,多线程是解决批处理任务的通用方案。 具体在redis的批处理任务中,使用多个线程,建立多个对redis的访问连接,在每个连接中发起一个或多个对redis key的操作。这样也可以提高整个批处理任务的处理效率。
同学会不会有个疑问 ?
redis6.0版本之前,redis处理逻辑不是单线程的吗(不考虑4.0版本中引入的异步处理的那种伪多线程)?请求发送到服务端,不还是一个一个处理的吗? 这样真的可以提高效率吗?
说的没错,以前版本的redis在处理任务使用的是单线程。但是我这里说的不是服务端的多线程,而是客户端的多线程。充分利用连接池来加快整个批处理任务的处理速度。
不明白?
来,举个栗子。你有10个银行业务需要办理,但是银行目前只有一个工作人员提供服务。
你可以先派一个人拿着一个任务单去银行处理,回来之后再派一个人去处理第二个任务… 这讲的是普通的“ping-pong”方式。
你也可以一下派10个人,每人拿着一个任务单去银行。虽然他们到银行的时候,服务人员也只能一项一项的处理。但是服务人员处理完第一个任务之后就可以理解处理第二个任务,避免了前一种方式来回路途的消耗。 这就是多线程的方式。
效率高低,立见分明。
ps: 当然,你也可以只派出一个人,让他拿着所有的任务。去一次把所有的任务干完才回来。这也就是上面说的pipeline的方式。
三、pipeline vs 多线程
那么对于处理批处理任务来说,哪个好点呢?
这里简单做了个性能对比测试。代码如下所示:
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 |
我们来分析一下实验结果:
- 多线程批处理确实可以提高任务的处理速度。从编号(1,4),(2,5),(3,6)这几组对比实验可以明显的看出来。
- pipeline也可以提高任务的处理效率。从编号(1,7),(2,8),(3,9)这几组对比实验可以看出来。
- 一般来说,使用pipeline的效果要比多线程好点。比如说从(4,7),(5,8),(6,9)可以看出端倪。 当然这也不是绝对的。当pipeline中的任务极其少的情况下,pipeline因为有队列缓存的相关的处理,因此效率反而比多线程要低。比如说看(4,10)这组对比实验。
四、其他
主菜上了,来点辅料?
- 这里讨论的是客户端利用多线程(连接池)或者pipeline来提高整个批处理请求的处理速度。对于redis服务端来说,也可以通过多线程的方式来提高服务器性能。在redis6.0之后,提供了多线程的方式来进行IO的解析回包(但是命令的执行还是单线程的)等,以此来提高整体的性能。 具体请参照redis多线程的相关资料,这里我就暂且不赘述了。
- 多线程可以重复利用cpu多核的优势,但是有一个经典的问题就是数据的同步问题(涉及到加锁和阻塞)。因此如果可能的话,尽量把每个线程共享数据减到最少。每个线程的请求数据最好隔离开。
五、总结
总的来说,一般情况下,如果要使用批处理任务的话,pipeline效率要更高点。当然马克思主义告诉我们要辩证的看待问题,实际问题还要实际分析,具体选用什么方式还得看业务需求。 (好久没写博客了,略显生疏。 这篇博客竟写了一天多╮(╯▽╰)╭)
参考
-
Redis Pipeline介绍及应用
-
Redis Pipeline这一篇就够了
-
Redis 多线程网络模型全面揭秘
-
为什么 Redis 选择单线程模型
-
做对这 10 点,让你的 Redis 性能更上一层楼!
-
《Redis设计与实现》