如何用 redis 实现批量 pop?

最近在业务上遇到需要批量pop的场景,起初以为是个很简单的方案,然而在实现过程中走了很多弯路,和大家分享一下,避免踩坑

1.简单的循环 pop

这是最简单的方案,伪代码如下所示:

var values []string
for(i:=0;i<num;i++){
  values=append(values,redis.rpop())
}
复制代码

这也是我们最初的方案,后来随着业务量上来后,大量的网络开销导致耗时严重,业务逐渐不能接受

总结

优点

  • 最简单的方案

缺点

  • 串行,耗时严重

2.易实现的并发 pop

通过并发 pop,进而缩短网络耗时,伪代码如下所示:

var values []string
locker:=new(sync.mutux)
wg:=new(sync.WaitGroup)
for(i:=0;i<n;i++){
  wg.Add(1)
  go func(){
    locker.Lock()
    values=append(values,redis.rpop())
    locker.Unlock()
    wg.Done()
  }
}
wg.Wait()
复制代码

最终耗时≈网络中最慢的一次请求

总结

优点

  • 最容易想到的优化方案
  • 性能提升显著

缺点

  • 需要处理并发
  • 有大量的网络开销,server端处理网络连接的压力较大

3.lrange+ltrim+txpipeline

开始有同事提到用 lrange 来批量获取队列中的消息,但后来发现 lrange 只能查看,而不能同时移除消息,这就令我们很头疼了

上网上查了下,说是接着用 ltrim,同时用 txpipelline 来保证原子性,如下所示:

p:=redis.txpipeline()
// 查询 0~n-1 之间的元素
p.lrange(key, 0, n - 1) 
// 保留 n~最后(-1)之间的元素,换言之删除 0~n-1 之间的元素
p.ltrim(key, n, -1) 
values:=p.execute()
复制代码

一眼看上去确实很完美,一个打包命令实现了批量pop,避免了大量网络开销,但是真的是这样吗?

txpipeline的原子性

pipeline

首先,pipeline本身非原子性,其只是将一批命令一口气发到server,server依次处理这些命令,中间可能穿插执行其他 client 传来的命令,所以只用 pipeline的话是无法保证pop正确运行的,存在pop同一个元素的风险,或者多删元素等并发风险

transaction

其次,我们来了解下redis的事务,其主要由multi 和exec构成,用来封装一个原子执行单位,其实本质上,不算是真正的事务:

  1. 不能回滚
  2. 部分命令出错后,仍然能继续执行

该事务能显性检查一些语法错误,从而避免执行事务;但对于一些单条语法上无法辨别的错误是无法check的,例如对set进行pop,这就导致了上述 2 的发送

txpipeline

所以 txpipeline 就是在 pipeline前后加入了事务操作,从而保证了这批命令的原子性。从这个角度上看上述实现是没有问题的,但由于事务开销比较大,会阻塞其他命令,导致server响应较慢

先进先出

在使用lrange批量取元素时,首先要明白元素在队列中是如何排列的,这里分为rpush 和 lpush 两种不同的排列方式:

rpush lpush.png

我们可以看到在rpush中,index 与 我们推送元素的顺序是一致,这时候通过lrange取元素的时候,就直接从头取就可以了:

p.lrange(key, 0, n - 1)

取出来的顺序(a,b,c)也是我们推送的顺序,保证了先进先出

但是在lpush中,index 与 我们推送元素是相反的,这时候通过lrange取元素的时候,需要从尾部取:

p.lrange(key, -n, -1)

首先index的写法与rpush不一致,其次拿出来顺序会是(c,b,a),为保证先进先出,我们需要逆序消费

总结

优点

  • 一次网络请求,网络开销小

缺点

  • 需要事务操作,server存在阻塞风险
  • 根据rpush与lpush不同,批量处理方式不同,细节较多,需要考虑清楚

4.pipeline+pop

在了解完 pipeline 后,我们很自然的想到了这种方案:

p:=redis.txpipeline()
for(i:=0;i<n;i++){
  p.rpop()
}
values:=p.execute()
复制代码

该方案本质上就是将一堆pop一口气打包到了server端,让server依次处理,避免了网络开销和事务问题

实质上该方案与方案3,有一个性能的平衡点,即该方案的大量pop带来的性能损耗 == 方案3的事务操作

总结

优点

  • 易实现

缺点

  • 需要考虑pop的数量,无限制的增长会带来大量的性能损耗(相对于方案3,因为每次pop都是查+写)

参考

如何从 Redis 的列表中一次性 pop 多条数据?

Redis系列十:Pipeline详解

Redis中的pipeline和事务的区别是什么?

如何用好redis pipeline

Redis事务与Pipeline功能

猜你喜欢

转载自juejin.im/post/7031571425622392840
pop