简介:Redis是目前最受欢迎的kv类数据库,当然它的性能越来越多,早已不限定在kv场景,音讯队列就是Redis中一个重要的性能。Redis从2010年公布1.0版本就具备一个音讯队列的雏形,随着10多年的迭代,其音讯队列的性能也越来越欠缺,作为一个全内存的音讯队列,适宜利用与要求高吞吐、低延时的场景。本文未来盘一下Redis音讯队列性能的倒退历程,历史版本有哪些有余,后续版本是如何来解决这些问题的。

作者 | 丕天
起源 | 阿里技术公众号

Redis是目前最受欢迎的kv类数据库,当然它的性能越来越多,早已不限定在kv场景,音讯队列就是Redis中一个重要的性能。

Redis从2010年公布1.0版本就具备一个音讯队列的雏形,随着10多年的迭代,其音讯队列的性能也越来越欠缺,作为一个全内存的音讯队列,适宜利用与要求高吞吐、低延时的场景。

咱们来盘一下Redis音讯队列性能的倒退历程,历史版本有哪些有余,后续版本是如何来解决这些问题的。

一 Redis 1.0 list

从狭义上来讲音讯队列就是一个队列的数据结构,生产者从队列一端放入音讯,消费者从另一端读取音讯,音讯保障先入先出的程序,一个本地的list数据结构就是一个过程维度的音讯队列,它能够让模块A写入音讯,模块B生产音讯,做到模块A/B的解耦与异步化。但想要做到利用级别的解耦和异步还须要一个音讯队列的服务。

1 list的个性

Redis 1.0公布时就具备了list数据结构,利用A能够通过lpush写入音讯,利用B通过rpop从队列中读取音讯,每个音讯只会被读取一次,而且是依照lpush写入的程序读到。同时Redis的接口是并发平安的,能够同时有多个生产者向一个list中生产音讯,多个消费者从list中读取音讯。

这里还有个问题,消费者要如何晓得list中有音讯了,须要一直轮询去查问吗。轮询无奈保障音讯被及时的解决,会减少延时,而且当list为空时,大部分轮询的申请都是有效申请,这种形式大量节约了系统资源。好在Redis有brpop接口,该接口有一个参数是超时工夫,如果list为空,那么Redis服务端不会立即返回后果,它会期待list中有新数据后在返回或是期待最多一个超时工夫后返回空。通过brpop接口实现了长轮询,该成果等同于服务端推送,消费者能立即感知到新的音讯,而且通过设置正当的超时工夫,使系统资源的耗费降到很低。

#基于list实现音讯的生产和生产#生产者生产音讯msg1lpush listA msg1(integer) 1#消费者读取到音讯msg1rpop listA"msg1"#消费者阻塞式读取listA,如果有数据立即返回,否则最多期待10秒brpop listA 10 1) "listA"2) "msg1"

应用rpop或brpop这样接口生产音讯会先从队列中删除音讯,而后再由利用生产,如果利用利用在解决音讯前异样宕机了,音讯就失落了。但如果应用lindex这样的只读命令先读取音讯处理完毕后在删除,又须要额定的机制来保障一条音讯不会被其余消费者反复读到。好在list有rpoplpush或brpoplpush这样的接口,能够原子性的从一个list中移除一个音讯并退出另一个list。

应用程序能够通过2个list组和来实现音讯的生产和确认性能,应用rpoplpush从list A中生产音讯并移入list B,等音讯处理完毕后在从list B中删除音讯,如果在解决音讯过程中利用异样宕机,复原后利用能够从新从list B中读取未解决的音讯并解决。这种形式为音讯的生产减少了ack机制。

#基于2个list实现音讯生产和确认#从listA中读取音讯并写入listBrpoplpush listA listB"msg1"#业务逻辑解决msg1结束后,从listB中删除msg1,实现音讯的确认lrem listB 1 msg1(integer) 1

2 list的不足之处

通过Redis 1.0就引入的list构造咱们就能实现一个分布式的音讯队列,满足一些简略的业务需要。但list构造作为音讯队列服务有一个很致命的问题,它没有播送性能,一个音讯只能被生产一次。而在大型零碎中,通常一个音讯会被上游多个利用同时订阅和生产,例如当用户实现一个订单的领取操作时,须要告诉商家发货,要更新物流状态,可能还会进步用户的积分和等级,这些都是不同的上游子系统,他们全副会订阅领取实现的操作,而list一个音讯只能被生产一次在这样简单的大型零碎背后就顾此失彼了。

可能你会说那弄多个list,生产者向每个list中都投递音讯,每个消费者解决本人的list不就行了吗。这样第一是性能不会太好,因为同一个音讯须要被反复的投递,第二是这样的设计违反了生产者和消费者解耦的准则,这个设计下生产者须要晓得上游有哪些消费者,如果业务发生变化,须要额定减少一个消费者,生产者的代码也须要批改。

3 总结

劣势

  • 模型简略,和应用本地list基本相同,适配容易
  • 通过brpop做到音讯解决的实时性
  • 通过rpoplpush来联动2个list,能够做到音讯先生产后确认,防止消费者利用异常情况下音讯失落

有余

音讯只能被生产一次,不足播送机制

二 Redis 2.0 pubsub

list作为音讯队列利用场景受到限制很重要的起因在于没有播送,所以Redis 2.0中引入了一个新的数据结构pubsub。pubsub尽管不能算作是list的替代品,但它的确能解决一些list不能解决的问题。

1 pubsub个性

pubsub引入一个概念叫channel,生产者通过publish接口投递音讯时会指定channel,消费者通过subscribe接口订阅它关怀的channel,调用subscribe后这条连贯会进入一个非凡的状态,通常不能在发送其余申请,当有音讯投递到这个channel时Redis服务端会立即通过该连贯将音讯推送到消费者。这里一个channel能够被多个利用订阅,音讯会同时投递到每个订阅者,做到了音讯的播送。

另一方面,消费者能够会订阅一批channel,例如一个用户订阅了浙江的新闻的推送,但浙江新闻还会进行细分,例如“浙江杭州xx”、“浙江温州xx”,这里订阅者不须要获取浙江的所有子类在挨个订阅,只须要调用psubscribe“浙江*”就能订阅所有以浙江结尾的新闻推送了,这里psubscribe传入一个通配符表白的channel,Redis服务端依照规定推送所有匹配channel的音讯给对应的客户端。

#基于pubsub实现channel的匹配和音讯的播送#消费者1订阅channel1subscribe channel11) "subscribe"2) "channel1"3) (integer) 1#收到音讯推送1) "message"2) "channel1"3) "msg1"#消费者2订阅channel*psubscribe channel*1) "psubscribe"2) "channel*"3) (integer) 1#收到音讯推送1) "pmessage"2) "channel*"3) "channel1"4) "msg1"1) "pmessage"2) "channel*"3) "channel2"4) "msg2"#生产者公布音讯msg1和msg2publish channel1 msg1(integer) 2publish channel2 msg2(integer) 1

在Redfis 2.8时退出了keyspace notifications性能,此时pubsub除了告诉用户自定义音讯,也能够告诉零碎内部消息。keyspace notifications引入了2个非凡的channel别离是__keyevent@__:和__keyspace@__:,通过订阅__keyevent客户端能够收到某个具体命令调用的回调告诉,通过订阅__keyspace客户端能够收到指标key的增删改操作以及过期事件。应用这个性能还须要开启配置notify-keyspace-events。

#通过keyspace notifications性能获取零碎事件#写入申请set testkey v EX 1#订阅key级别的事件psubscribe __keyspace@0__:testkey1) "psubscribe"2) "__keyspace@0__:testkey"3) (integer) 1#收到告诉1) "pmessage"2) "__keyspace@0__:testkey"3) "__keyspace@0__:testkey"4) "set"1) "pmessage"2) "__keyspace@0__:testkey"3) "__keyspace@0__:testkey"4) "expire"1) "pmessage"2) "__keyspace@0__:testkey"3) "__keyspace@0__:testkey"4) "expired"#订阅所有的命令事件psubscribe __keyevent@0__:*1) "psubscribe"2) "__keyevent@0__:*"3) (integer) 1#收到告诉1) "pmessage"2) "__keyevent@0__:*"3) "__keyevent@0__:set"4) "testkey"1) "pmessage"2) "__keyevent@0__:*"3) "__keyevent@0__:expire"4) "testkey"1) "pmessage"2) "__keyevent@0__:*"3) "__keyevent@0__:expired"4) "testkey"

2 pubsub的不足之处

pubsub既能单播又能播送,还反对channel的简略正则匹配,性能上曾经能满足大部分业务的需要,而且这个接口公布的工夫很早,在2011年Redis 2.0公布时就曾经具备,用户根底很宽泛,所以当初很多业务都有用到这个性能。但你要深刻理解pubsub的原理后,是必定不敢把它作为一个一致性要求较高,数据量较大零碎的音讯服务的。

首先,pubsub的音讯数据是刹时的,它在Redis服务端不做保留,publish发送到Redis的音讯会立即推送到所有过后subscribe连贯的客户端,如果过后客户端因为网络问题断连,那么就会错过这条音讯,当客户端重连后,它没法从新获取之前那条音讯,甚至无奈判断是否有音讯失落。

其次,pubsub中消费者获取音讯是一个推送模型,这意味着Redis会按音讯生产的速度给所有的消费者推送音讯,不论消费者解决能力如何,如果消费者利用解决能力有余,音讯就会在Redis的client buf中沉积,当沉积数据超过一个阈值后会断开这条连贯,这意味着这些音讯全副失落了,在也找不回来了。如果同时有多个消费者的client buf沉积数据但又还没达到断开连接的阈值,那么Redis服务端的内存会收缩,过程可能因为oom而被杀掉,这导致了整个服务中断。

3 总结

劣势

  • 音讯具备播送能力
  • psubscribe能按字符串通配符匹配,给予了业务逻辑的灵活性
  • 能订阅特定key或特定命令的零碎音讯

有余

  • Redis异样、客户端断连都会导致音讯失落
  • 音讯不足沉积能力,不能削峰填谷。推送的形式不足背压机制,没有思考消费者解决能力,推送的音讯超过消费者解决能力后可能导致音讯失落或服务异样

三 Redis 5.0 stream

音讯失落、音讯服务不稳固的问题重大限度了pubsub的利用场景,所以Redis须要从新设计一套机制,来解决这些问题,这就有了起初的stream构造。

1 stream个性

一个稳固的音讯服务须要具备几个要点,要保障音讯不会失落,至多被生产一次,要具备削峰填谷的能力,来匹配生产者和消费者吞吐的差别。在2018年Redis 5.0退出了stream构造,这次思考了list、pubsub在利用场景下的缺点,对标kafka的模型从新设计全内存音讯队列构造,从这时开始Redis音讯队列性能算是能和支流音讯队列产品pk一把了。

stream的改良分为多个方面

老本:

存储message数据应用了listpack构造,这是一个紧凑型的数据结构,不同于list的双向链表每个节点都要额定占用2个指针的存储空间,这使得小msg状况下stream的空间利用率更高。

性能:

  • stream引入了消费者组的概念,一个消费者组内能够有多个消费者,同一个组内的消费者共享一个音讯位点(last_delivered_id),这使得消费者可能程度的扩容,能够在一个组内退出多个消费者来线性的晋升吞吐,对于一个消费者组,每条msg只会被其中一个消费者获取和解决,这是pubsub的播送模型不具备的。
  • 不同消费者组之前是互相隔离的,他们各自保护本人的位点,这使得一条msg能被多个不同的消费者组反复生产,做到了音讯播送的能力。
  • stream中消费者采纳拉取的形式,并能设置timeout在没有音讯时阻塞,通过这种长轮询机制保障了音讯的实时性,而且生产速率是和消费者本身吞吐相匹配。

音讯不失落:

  • stream的数据会存储在aof和rdb文件中,这使Redis重启后可能复原stream的数据。而pubsub的数据是刹时的,Redis重启意味着音讯全副失落。
  • stream中每个消费者组会存储一个last_delivered_id来标识曾经读取到的位点,客户端连贯断开后重连还是能从该位点持续读取,音讯不会失落。
  • stream引入了ack机制保障音讯至多被解决一次。思考一种场景,如果消费者利用曾经读取了音讯,但还没来得及解决利用就宕机了,对于这种曾经读取但没有ack的音讯,stream会标示这条音讯的状态为pending,等客户端重连后通过xpending命令能够从新读取到pengind状态的音讯,持续解决。如果这个利用永恒宕机了,那么该消费者组内的其余消费者利用也能读取到这条音讯,并通过xclaim命令将它归属到本人上面持续解决。
#基于stream实现音讯的生产和生产,并确保异样状态下音讯至多被生产一次#创立mystream,并且创立一个consumergroup为mygroupXGROUP CREATE mystream mygroup $ MKSTREAMOK#写入一条音讯,由redis主动生成音讯id,音讯的内容是一个kv数组,这里蕴含field1 value1 field2 value2XADD mystream * field1 value1 field2 value2"1645517760385-0"#消费者组mygroup中的消费者consumer1从mystream读取一条音讯,>示意读取一条该消费者组从未读取过的音讯XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1645517760385-0"         2) 1) "field1"            2) "value1"            3) "field2"            4) "value2"            #生产实现后ack确认音讯xack mystream mygroup 1645517760385-0(integer) 1#如果消费者利用在ack前异样宕机,复原后从新获取未解决的音讯id。XPENDING mystream mygroup - + 10 1) 1) "1645517760385-0"   2) "consumer1"   3) (integer) 305356   4) (integer) 1    #如果consumer1永远宕机,其余消费者能够把pending状态的音讯挪动到本人名下后持续生产 #将音讯id 1645517760385-0挪动到consumer2下 XCLAIM mystream mygroup consumer2 0 1645517760385-0 1) 1) "1645517760385-0"   2) 1) "field1"      2) "value1"      3) "field2"      4) "value2"

Redis stream保障了音讯至多被解决一次,但如果想做到每条音讯仅被解决一次还须要应用逻辑的染指。

音讯被反复解决要么是生产者反复投递,要么是消费者反复生产。

  • 对于生产者反复投递问题,Redis stream为每个音讯都设置了一个惟一递增的id,通过参数能够让Redis主动生成id或者利用本人指定id,利用能够依据业务逻辑为每个msg生成id,当xadd超时后利用并不能确定音讯是否投递胜利,能够通过xread查问该id的音讯是否存在,存在就阐明曾经投递胜利,不存在则从新投递,而且stream限度了id必须递增,这象征了曾经存在的音讯反复投递会被回绝。这套机制保障了每个音讯能够仅被投递一次。
  • 对于消费者反复生产的问题,思考一个场景,消费者读取音讯后业务处理完毕,但还没来得及ack就产生了异样,利用复原后对于这条没有ack的音讯进行了反复生产。这个问题因为ack和生产音讯的业务逻辑产生在2个零碎,没法做到事务性,须要业务来革新,保障音讯解决的幂等性。

2 stream的有余

stream的模型做到了音讯的高效散发,而且保障了音讯至多被解决一次,通过应用逻辑的革新能做到音讯仅被解决一次,它的能力对标kafka,但吞吐高于kafka,在高吞吐场景下老本比kafka低,那它又有哪些有余了。

首先音讯队列很重要的一个性能就是削峰填谷,来匹配生产者和消费者吞吐的差别,生产者和消费者吞吐差别越大,持续时间越长,就意味着steam中须要沉积更多的音讯,而Redis作为一个全内存的产品,数据沉积的老本比磁盘高。

其次stream通过ack机制保障了音讯至多被生产一次,但这有个前提就是存储在Redis中的音讯自身不会失落。Redis数据的长久化依赖aof和rdb文件,aof落盘形式有几种,通过配置appendfsync决定,通常咱们不会配置为always来让每条命令执行完后都做一次fsync,线上配置个别为everysec,每秒做一次fsync,而rdb是全量备份时生成,这象征了宕机复原可能会丢掉最近一秒的数据。另一方面线上生产环境的Redis都是高可用架构,当主节点宕机后通常不会走复原逻辑,而是间接切换到备节点持续提供服务,而Redis的同步形式是异步同步,这意味着主节点上新写入的数据可能还没同步到备节点,在切换后这部分数据就失落了。所以在故障复原中Redis中的数据可能会失落一部分,在这样的背景下无论stream的接口设计的如许欠缺,都不能保障音讯至多被生产一次。

3 总结

劣势

  • 在老本、性能上做了很多改良,反对了紧凑的存储小音讯、具备播送能力、消费者能程度扩容、具备背压机制
  • 通过ack机制保障了Redis服务端失常状况下音讯至多被解决一次的能力

有余

  • 内存型音讯队列,数据沉积老本高
  • Redis自身rpo>0,故障复原可能会丢数据,所以stream在Redis产生故障复原后也不能保障音讯至多被生产一次。

四 Tair长久内存版 stream

Redis stream的有余也是内存型数据库个性带来的,它领有高吞吐、低延时,但大容量下老本会比拟高,而利用的场景也不齐全是相对的大容量低吞吐或小容量高吞吐,有时利用的场景会介于二者之间,须要均衡容量和吞吐的关系,所以须要一个产品它的存储老本低于Redis stream,但它的性能又高于磁盘型音讯队列。

另一方面Redis stream在Redis故障场景下不能保障音讯的不失落,这导致业务须要本人实现一些简单的机制来回补这段数据,同时也限度了它利用在一些对一致性要求较高的场景。为了让业务逻辑更简略,stream利用范畴更广,须要保障故障场景下的音讯长久化。

兼顾老本、性能、长久化,这就有了Tair长久内存版。

1 Tair长久内存版个性

更大空间,更低成本

Tair长久内存版引入了Intel傲腾长久内存(上面称作AEP),它的性能略低于内存,但雷同容量下老本低于内存。Tair长久内存版将次要数据存储在AEP上,使得雷同容量下,老本更低,这使同样单价下stream能沉积更多的音讯。

兼容社区版

Tair长久内存版兼容原生Redis绝大部分的数据结构和接口,对于stream相干接口做到了100%兼容,如果你之前应用了社区版stream,那么不须要批改任何代码,只须要换一个连贯地址就能切换到长久内存版。并且通过工具实现社区版和长久内存版数据的双向迁徙。

数据的实时长久化

Tair长久内存版并不是简略将Redis中的数据换了一个介质存储,因为这样仅能通过AEP降低成本,但没用到AEP断电数据不失落的个性,对长久化能力没有任何晋升。

开源Redis通过在磁盘上记录AppendOnlyLog来长久化数据,AppendOnlyLog记录了所有的写操作,相当于redolog,在宕机复原时通过回放这些log复原数据。但受限于磁盘介质的高延时和Redis内存数据库应用场景下对低延时的要求,并不能在每次写操作后fsync长久化log,最新写入的数据可能并没有长久化到磁盘,这也是数据可能失落的根因。

Tair长久内存版的数据恢复没有应用AppendOnlyLog来实现, 而是将将redis数据结构存储在AEP上,这样宕机后这些数据结构并不会失落,并且对这些数据结构减少了一些额定的形容信息,宕机后在recovery时可能读到这些额定的形容信息,让这些redis数据结构从新被辨认和索引,将状态复原到宕机前的样子。Tair通过将redis数据结构和形容信息实时写入AEP,保障了写入数据的实时长久化。

HA数据不失落

Tair长久内存版保障了数据的长久化,但生产环境中都是高可用架构,少数状况下当主节点异样宕机后并不会等主节点重启复原,而是切换到备节点持续提供服务,而后给新的主节点增加一个新的备节点。所以在故障产生时如果有数据还没从主节点同步到备节点,这部分数据就会失落。

Redis采纳的异步同步,当客户端写入数据并返回胜利时对Redis的批改可能还没同步到备节点,如果此时主节点宕机数据就会失落。为了防止在HA过程中数据失落,Tair长久内存版引入了半同步机制,确保写入申请返回胜利前相干的批改曾经同步到备节点。

能够发现开启半同步性能后写入申请的RT会变高,多出主备同步的耗时,这部分耗时大略在几十微秒。但通过一些异步化的技术,尽管写申请的RT会变高,但对实例的最大写吞吐影响很小。

当开启半同步后生成者通过xadd投递音讯,如果返回胜利,音讯肯定同步到备节点,此时产生HA,消费者也能在备节点上读到这条音讯。如果xadd申请超时,此时音讯可能同步到备节点也可能没有,生产者没法确定,此时通过再次投递音讯,能够保障该音讯至多被生产一次。如果要严格保障音讯仅被生产一次,那么生产者能够通过xread接口查问音讯是否存在,对于不存在的场景从新投递。

2 总结

劣势

  • 引入了AEP作为存储介质,目前Tair长久内存版价格是社区版的70%。
  • 保障了数据的实时长久化,并且通过半同步技术保障了HA不丢数据,大多数状况下做到音讯不失落(备库故障或主备网络异样时会降级为异步同步,优先保障可用性),音讯至多被生产一次或仅被生产一次。

五 将来

音讯队列次要是为了解决3类问题,利用模块的解耦、音讯的异步化、削峰填谷。目前支流的音讯队列都能满足这些需要,所以在理论选型时还会思考一些非凡的性能是否满足,产品的性能如何,具体业务场景下的老本怎么样,开发的复杂度等。

Redis的音讯队列性能并不是最全面的,它不心愿做成一个大而全的产品,而是做一个小而美的产品,服务好一部分用户在某些场景下的需要。目前用户选型Redis作为音讯队列服务的起因,次要有Redis在雷同老本下吞吐更高、Redis的延时更低、利用须要一个音讯服务但又不想额定引入一堆依赖等。

将来Tair长久内存版会针对这些述求,把这些劣势持续放大。

  • 吞吐

通过优化长久内存版的长久化流程,让吞吐靠近内存版甚至超过内存版吞吐。

  • 延时

通过rdma在多正本间同步数据,升高半同步下写入数据的延时。

原文链接
本文为阿里云原创内容,未经容许不得转载。