redis批处理任务,多线程 or pipeline

前言

redis批处理任务,多线程 or pipeline_第1张图片

工作中使用redis的时候遇到一个问题,如果我们要对存储在redis中的一批数据进行操作,为了加快处理的速度,我们一般有两种方式:

  • 建立多个线程,使用多个连接发送请求
  • 使用redis提供的pipeline

引发了一个小思考:这两种方式通常哪个更好点呢?
这个周末有点空,做个调研总结下。写篇博客,希望可以便人便己。

redis批处理任务,使用多线程还是 pipeline引发的思考

  • 前言
    • 一、关于pipeline方式
    • 二、关于多线程方式
    • 三、pipeline vs 多线程
    • 四、其他
    • 五、总结
  • 参考

一、关于pipeline方式

对于redis这种性能瓶颈主要取决于网络IO的网络服务器来说,客户端的每一次网络请求都是对服务端的无情损伤。如果业务场景需要对多个redis数据进行操作,原始的那种“ping-pong” 交互就太慢了。因为每一个操作都需要一个RoundTrip。

因此Redis在2.0版本中引入了Pipeline,在一次请求中携带多个执行命令,这样请求只需要顺着网络跑一次,就可以执行多条命令,大大提高了网络性能。

由下图可以清晰的看出区别来。
redis批处理任务,多线程 or pipeline_第2张图片

使用pipeline也有一些注意点,这里简单说明一下:

  • 不要往pipeline里塞过多的命令,否则会撑满客户端的缓存。因为pipeline 在exec之前都是缓存再客户端的。
  • pipeline里的命令是依次执行的,但是在服务端执行的时候可能会穿插着其他客户端的请求。
  • pipeline缓冲的指令不能保证原子性,如果执行中间某一个指令发生异常,将会继续执行后续的指令。
  • cluster并不支持pipeline操作

二、关于多线程方式

redis批处理任务,多线程 or pipeline_第3张图片

不仅仅局限于对redis的请求,多线程是解决批处理任务的通用方案。 具体在redis的批处理任务中,使用多个线程,建立多个对redis的访问连接,在每个连接中发起一个或多个对redis key的操作。这样也可以提高整个批处理任务的处理效率。

同学会不会有个疑问 ?
redis6.0版本之前,redis处理逻辑不是单线程的吗(不考虑4.0版本中引入的异步处理的那种伪多线程)?请求发送到服务端,不还是一个一个处理的吗? 这样真的可以提高效率吗?

说的没错,以前版本的redis在处理任务使用的是单线程。但是我这里说的不是服务端的多线程,而是客户端的多线程。充分利用连接池来加快整个批处理任务的处理速度。

不明白?
来,举个栗子。你有10个银行业务需要办理,但是银行目前只有一个工作人员提供服务。
你可以先派一个人拿着一个任务单去银行处理,回来之后再派一个人去处理第二个任务… 这讲的是普通的“ping-pong”方式。
你也可以一下派10个人,每人拿着一个任务单去银行。虽然他们到银行的时候,服务人员也只能一项一项的处理。但是服务人员处理完第一个任务之后就可以理解处理第二个任务,避免了前一种方式来回路途的消耗。 这就是多线程的方式。

效率高低,立见分明。

ps: 当然,你也可以只派出一个人,让他拿着所有的任务。去一次把所有的任务干完才回来。这也就是上面说的pipeline的方式。

三、pipeline vs 多线程

redis批处理任务,多线程 or pipeline_第4张图片

那么对于处理批处理任务来说,哪个好点呢?

这里简单做了个性能对比测试。代码如下所示:

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. 多线程批处理确实可以提高任务的处理速度。从编号(1,4),(2,5),(3,6)这几组对比实验可以明显的看出来。
  2. pipeline也可以提高任务的处理效率。从编号(1,7),(2,8),(3,9)这几组对比实验可以看出来。
  3. 一般来说,使用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设计与实现》

你可能感兴趣的:(redis,缓存,数据库)