作者 | 丕天
Redis是目前最受歡迎的kv類數(shù)據(jù)庫(kù),當(dāng)然它的功能越來(lái)越多,早已不限定在kv場(chǎng)景,消息隊(duì)列就是Redis中一個(gè)重要的功能。
Redis從2010年發(fā)布1.0版本就具備一個(gè)消息隊(duì)列的雛形,隨著10多年的迭代,其消息隊(duì)列的功能也越來(lái)越完善,作為一個(gè)全內(nèi)存的消息隊(duì)列,適合應(yīng)用與要求高吞吐、低延時(shí)的場(chǎng)景。
我們來(lái)盤一下Redis消息隊(duì)列功能的發(fā)展歷程,歷史版本有哪些不足,后續(xù)版本是如何來(lái)解決這些問(wèn)題的。
一、Redis 1.0 list
從廣義上來(lái)講消息隊(duì)列就是一個(gè)隊(duì)列的數(shù)據(jù)結(jié)構(gòu),生產(chǎn)者從隊(duì)列一端放入消息,消費(fèi)者從另一端讀取消息,消息保證先入先出的順序,一個(gè)本地的list數(shù)據(jù)結(jié)構(gòu)就是一個(gè)進(jìn)程維度的消息隊(duì)列,它可以讓模塊A寫入消息,模塊B消費(fèi)消息,做到模塊A/B的解耦與異步化。但想要做到應(yīng)用級(jí)別的解耦和異步還需要一個(gè)消息隊(duì)列的服務(wù)。
1.list的特性
Redis 1.0發(fā)布時(shí)就具備了list數(shù)據(jù)結(jié)構(gòu),應(yīng)用A可以通過(guò)lpush寫入消息,應(yīng)用B通過(guò)rpop從隊(duì)列中讀取消息,每個(gè)消息只會(huì)被讀取一次,而且是按照l(shuí)push寫入的順序讀到。同時(shí)Redis的接口是并發(fā)安全的,可以同時(shí)有多個(gè)生產(chǎn)者向一個(gè)list中生產(chǎn)消息,多個(gè)消費(fèi)者從list中讀取消息。
這里還有個(gè)問(wèn)題,消費(fèi)者要如何知道list中有消息了,需要不斷輪詢?nèi)ゲ樵儐?。輪詢無(wú)法保證消息被及時(shí)的處理,會(huì)增加延時(shí),而且當(dāng)list為空時(shí),大部分輪詢的請(qǐng)求都是無(wú)效請(qǐng)求,這種方式大量浪費(fèi)了系統(tǒng)資源。好在Redis有brpop接口,該接口有一個(gè)參數(shù)是超時(shí)時(shí)間,如果list為空,那么Redis服務(wù)端不會(huì)立刻返回結(jié)果,它會(huì)等待list中有新數(shù)據(jù)后在返回或是等待最多一個(gè)超時(shí)時(shí)間后返回空。通過(guò)brpop接口實(shí)現(xiàn)了長(zhǎng)輪詢,該效果等同于服務(wù)端推送,消費(fèi)者能立刻感知到新的消息,而且通過(guò)設(shè)置合理的超時(shí)時(shí)間,使系統(tǒng)資源的消耗降到很低。
#基于list完成消息的生產(chǎn)和消費(fèi)
#生產(chǎn)者生產(chǎn)消息msg1
lpush listA msg1
(integer) 1
#消費(fèi)者讀取到消息msg1
rpop listA
"msg1"
#消費(fèi)者阻塞式讀取listA,如果有數(shù)據(jù)立刻返回,否則最多等待10秒
brpop listA 10
1) "listA"
2) "msg1"
使用rpop或brpop這樣接口消費(fèi)消息會(huì)先從隊(duì)列中刪除消息,然后再由應(yīng)用消費(fèi),如果應(yīng)用應(yīng)用在處理消息前異常宕機(jī)了,消息就丟失了。但如果使用lindex這樣的只讀命令先讀取消息處理完畢后在刪除,又需要額外的機(jī)制來(lái)保證一條消息不會(huì)被其他消費(fèi)者重復(fù)讀到。好在list有rpoplpush或brpoplpush這樣的接口,可以原子性的從一個(gè)list中移除一個(gè)消息并加入另一個(gè)list。
應(yīng)用程序可以通過(guò)2個(gè)list組和來(lái)完成消息的消費(fèi)和確認(rèn)功能,使用rpoplpush從list A中消費(fèi)消息并移入list B,等消息處理完畢后在從list B中刪除消息,如果在處理消息過(guò)程中應(yīng)用異常宕機(jī),恢復(fù)后應(yīng)用可以重新從list B中讀取未處理的消息并處理。這種方式為消息的消費(fèi)增加了ack機(jī)制。
#基于2個(gè)list完成消息消費(fèi)和確認(rèn)
#從listA中讀取消息并寫入listB
rpoplpush listA listB
"msg1"
#業(yè)務(wù)邏輯處理msg1完畢后,從listB中刪除msg1,完成消息的確認(rèn)
lrem listB 1 msg1
(integer) 1
2.list的不足之處
通過(guò)Redis 1.0就引入的list結(jié)構(gòu)我們就能實(shí)現(xiàn)一個(gè)分布式的消息隊(duì)列,滿足一些簡(jiǎn)單的業(yè)務(wù)需求。但list結(jié)構(gòu)作為消息隊(duì)列服務(wù)有一個(gè)很致命的問(wèn)題,它沒(méi)有廣播功能,一個(gè)消息只能被消費(fèi)一次。而在大型系統(tǒng)中,通常一個(gè)消息會(huì)被下游多個(gè)應(yīng)用同時(shí)訂閱和消費(fèi),例如當(dāng)用戶完成一個(gè)訂單的支付操作時(shí),需要通知商家發(fā)貨,要更新物流狀態(tài),可能還會(huì)提高用戶的積分和等級(jí),這些都是不同的下游子系統(tǒng),他們?nèi)繒?huì)訂閱支付完成的操作,而list一個(gè)消息只能被消費(fèi)一次在這樣復(fù)雜的大型系統(tǒng)面前就捉襟見(jiàn)肘了。
可能你會(huì)說(shuō)那弄多個(gè)list,生產(chǎn)者向每個(gè)list中都投遞消息,每個(gè)消費(fèi)者處理自己的list不就行了嗎。這樣第一是性能不會(huì)太好,因?yàn)橥粋€(gè)消息需要被重復(fù)的投遞,第二是這樣的設(shè)計(jì)違反了生產(chǎn)者和消費(fèi)者解耦的原則,這個(gè)設(shè)計(jì)下生產(chǎn)者需要知道下游有哪些消費(fèi)者,如果業(yè)務(wù)發(fā)生變化,需要額外增加一個(gè)消費(fèi)者,生產(chǎn)者的代碼也需要修改。
3.總結(jié)
優(yōu)勢(shì)
模型簡(jiǎn)單,和使用本地list基本相同,適配容易
通過(guò)brpop做到消息處理的實(shí)時(shí)性
通過(guò)rpoplpush來(lái)聯(lián)動(dòng)2個(gè)list,可以做到消息先消費(fèi)后確認(rèn),避免消費(fèi)者應(yīng)用異常情況下消息丟失
不足
消息只能被消費(fèi)一次,缺乏廣播機(jī)制
二、Redis 2.0 pubsub
list作為消息隊(duì)列應(yīng)用場(chǎng)景受到限制很重要的原因在于沒(méi)有廣播,所以Redis 2.0中引入了一個(gè)新的數(shù)據(jù)結(jié)構(gòu)pubsub。pubsub雖然不能算作是list的替代品,但它確實(shí)能解決一些list不能解決的問(wèn)題。
1.pubsub特性
pubsub引入一個(gè)概念叫channel,生產(chǎn)者通過(guò)publish接口投遞消息時(shí)會(huì)指定channel,消費(fèi)者通過(guò)subscribe接口訂閱它關(guān)心的channel,調(diào)用subscribe后這條連接會(huì)進(jìn)入一個(gè)特殊的狀態(tài),通常不能在發(fā)送其他請(qǐng)求,當(dāng)有消息投遞到這個(gè)channel時(shí)Redis服務(wù)端會(huì)立刻通過(guò)該連接將消息推送到消費(fèi)者。這里一個(gè)channel可以被多個(gè)應(yīng)用訂閱,消息會(huì)同時(shí)投遞到每個(gè)訂閱者,做到了消息的廣播。
另一方面,消費(fèi)者可以會(huì)訂閱一批channel,例如一個(gè)用戶訂閱了浙江的新聞的推送,但浙江新聞還會(huì)進(jìn)行細(xì)分,例如“浙江杭州xx”、“浙江溫州xx”,這里訂閱者不需要獲取浙江的所有子類在挨個(gè)訂閱,只需要調(diào)用psubscribe“浙江*”就能訂閱所有以浙江開頭的新聞推送了,這里psubscribe傳入一個(gè)通配符表達(dá)的channel,Redis服務(wù)端按照規(guī)則推送所有匹配channel的消息給對(duì)應(yīng)的客戶端。
#基于pubsub完成channel的匹配和消息的廣播
#消費(fèi)者1訂閱channel1
subscribe channel1
1) "subscribe"
2) "channel1"
3) (integer) 1
#收到消息推送
1) "message"
2) "channel1"
3) "msg1"
#消費(fèi)者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"
#生產(chǎn)者發(fā)布消息msg1和msg2
publish channel1 msg1
(integer) 2
publish channel2 msg2
(integer) 1
在Redfis 2.8時(shí)加入了keyspace notifications功能,此時(shí)pubsub除了通知用戶自定義消息,也可以通知系統(tǒng)內(nèi)部消息。keyspace notifications引入了2個(gè)特殊的channel分別是__keyevent@__:和__keyspace@__:,通過(guò)訂閱__keyevent客戶端可以收到某個(gè)具體命令調(diào)用的回調(diào)通知,通過(guò)訂閱__keyspace客戶端可以收到目標(biāo)key的增刪改操作以及過(guò)期事件。使用這個(gè)功能還需要開啟配置notify-keyspace-events。
#通過(guò)keyspace notifications功能獲取系統(tǒng)事件
#寫入請(qǐng)求
set testkey v EX 1
#訂閱key級(jí)別的事件
psubscribe __keyspace@0__:testkey
1) "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的簡(jiǎn)單正則匹配,功能上已經(jīng)能滿足大部分業(yè)務(wù)的需求,而且這個(gè)接口發(fā)布的時(shí)間很早,在2011年Redis 2.0發(fā)布時(shí)就已經(jīng)具備,用戶基礎(chǔ)很廣泛,所以現(xiàn)在很多業(yè)務(wù)都有用到這個(gè)功能。但你要深入了解pubsub的原理后,是肯定不敢把它作為一個(gè)一致性要求較高,數(shù)據(jù)量較大系統(tǒng)的消息服務(wù)的。
首先,pubsub的消息數(shù)據(jù)是瞬時(shí)的,它在Redis服務(wù)端不做保存,publish發(fā)送到Redis的消息會(huì)立刻推送到所有當(dāng)時(shí)subscribe連接的客戶端,如果當(dāng)時(shí)客戶端因?yàn)榫W(wǎng)絡(luò)問(wèn)題斷連,那么就會(huì)錯(cuò)過(guò)這條消息,當(dāng)客戶端重連后,它沒(méi)法重新獲取之前那條消息,甚至無(wú)法判斷是否有消息丟失。
其次,pubsub中消費(fèi)者獲取消息是一個(gè)推送模型,這意味著Redis會(huì)按消息生產(chǎn)的速度給所有的消費(fèi)者推送消息,不管消費(fèi)者處理能力如何,如果消費(fèi)者應(yīng)用處理能力不足,消息就會(huì)在Redis的client buf中堆積,當(dāng)堆積數(shù)據(jù)超過(guò)一個(gè)閾值后會(huì)斷開這條連接,這意味著這些消息全部丟失了,在也找不回來(lái)了。如果同時(shí)有多個(gè)消費(fèi)者的client buf堆積數(shù)據(jù)但又還沒(méi)達(dá)到斷開連接的閾值,那么Redis服務(wù)端的內(nèi)存會(huì)膨脹,進(jìn)程可能因?yàn)閛om而被殺掉,這導(dǎo)致了整個(gè)服務(wù)中斷。
3.總結(jié)
優(yōu)勢(shì)
- 消息具備廣播能力
- psubscribe能按字符串通配符匹配,給予了業(yè)務(wù)邏輯的靈活性
- 能訂閱特定key或特定命令的系統(tǒng)消息
不足
- Redis異常、客戶端斷連都會(huì)導(dǎo)致消息丟失
- 消息缺乏堆積能力,不能削峰填谷。推送的方式缺乏背壓機(jī)制,沒(méi)有考慮消費(fèi)者處理能力,推送的消息超過(guò)消費(fèi)者處理能力后可能導(dǎo)致消息丟失或服務(wù)異常
三、Redis 5.0 stream
消息丟失、消息服務(wù)不穩(wěn)定的問(wèn)題嚴(yán)重限制了pubsub的應(yīng)用場(chǎng)景,所以Redis需要重新設(shè)計(jì)一套機(jī)制,來(lái)解決這些問(wèn)題,這就有了后來(lái)的stream結(jié)構(gòu)。
1.stream特性
一個(gè)穩(wěn)定的消息服務(wù)需要具備幾個(gè)要點(diǎn),要保證消息不會(huì)丟失,至少被消費(fèi)一次,要具備削峰填谷的能力,來(lái)匹配生產(chǎn)者和消費(fèi)者吞吐的差異。在2018年Redis 5.0加入了stream結(jié)構(gòu),這次考慮了list、pubsub在應(yīng)用場(chǎng)景下的缺陷,對(duì)標(biāo)kafka的模型重新設(shè)計(jì)全內(nèi)存消息隊(duì)列結(jié)構(gòu),從這時(shí)開始Redis消息隊(duì)列功能算是能和主流消息隊(duì)列產(chǎn)品pk一把了。
stream的改進(jìn)分為多個(gè)方面
成本:
- 存儲(chǔ)message數(shù)據(jù)使用了listpack結(jié)構(gòu),這是一個(gè)緊湊型的數(shù)據(jù)結(jié)構(gòu),不同于list的雙向鏈表每個(gè)節(jié)點(diǎn)都要額外占用2個(gè)指針的存儲(chǔ)空間,這使得小msg情況下stream的空間利用率更高。
功能:
- stream引入了消費(fèi)者組的概念,一個(gè)消費(fèi)者組內(nèi)可以有多個(gè)消費(fèi)者,同一個(gè)組內(nèi)的消費(fèi)者共享一個(gè)消息位點(diǎn)(last_delivered_id),這使得消費(fèi)者能夠水平的擴(kuò)容,可以在一個(gè)組內(nèi)加入多個(gè)消費(fèi)者來(lái)線性的提升吞吐,對(duì)于一個(gè)消費(fèi)者組,每條msg只會(huì)被其中一個(gè)消費(fèi)者獲取和處理,這是pubsub的廣播模型不具備的。
- 不同消費(fèi)者組之前是相互隔離的,他們各自維護(hù)自己的位點(diǎn),這使得一條msg能被多個(gè)不同的消費(fèi)者組重復(fù)消費(fèi),做到了消息廣播的能力。
- stream中消費(fèi)者采用拉取的方式,并能設(shè)置timeout在沒(méi)有消息時(shí)阻塞,通過(guò)這種長(zhǎng)輪詢機(jī)制保證了消息的實(shí)時(shí)性,而且消費(fèi)速率是和消費(fèi)者自身吞吐相匹配。
消息不丟失:
- stream的數(shù)據(jù)會(huì)存儲(chǔ)在aof和rdb文件中,這使Redis重啟后能夠恢復(fù)stream的數(shù)據(jù)。而pubsub的數(shù)據(jù)是瞬時(shí)的,Redis重啟意味著消息全部丟失。
- stream中每個(gè)消費(fèi)者組會(huì)存儲(chǔ)一個(gè)last_delivered_id來(lái)標(biāo)識(shí)已經(jīng)讀取到的位點(diǎn),客戶端連接斷開后重連還是能從該位點(diǎn)繼續(xù)讀取,消息不會(huì)丟失。
- stream引入了ack機(jī)制保證消息至少被處理一次。考慮一種場(chǎng)景,如果消費(fèi)者應(yīng)用已經(jīng)讀取了消息,但還沒(méi)來(lái)得及處理應(yīng)用就宕機(jī)了,對(duì)于這種已經(jīng)讀取但沒(méi)有ack的消息,stream會(huì)標(biāo)示這條消息的狀態(tài)為pending,等客戶端重連后通過(guò)xpending命令可以重新讀取到pengind狀態(tài)的消息,繼續(xù)處理。如果這個(gè)應(yīng)用永久宕機(jī)了,那么該消費(fèi)者組內(nèi)的其他消費(fèi)者應(yīng)用也能讀取到這條消息,并通過(guò)xclaim命令將它歸屬到自己下面繼續(xù)處理。
#基于stream完成消息的生產(chǎn)和消費(fèi),并確保異常狀態(tài)下消息至少被消費(fèi)一次
#創(chuàng)建mystream,并且創(chuàng)建一個(gè)consumergroup為mygroup
XGROUP CREATE mystream mygroup $ MKSTREAM
OK
#寫入一條消息,由redis自動(dòng)生成消息id,消息的內(nèi)容是一個(gè)kv數(shù)組,這里包含field1 value1 field2 value2
XADD mystream * field1 value1 field2 value2
"1645517760385-0"
#消費(fèi)者組mygroup中的消費(fèi)者consumer1從mystream讀取一條消息,>表示讀取一條該消費(fèi)者組從未讀取過(guò)的消息
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"
#消費(fèi)完成后ack確認(rèn)消息
xack mystream mygroup 1645517760385-0
(integer) 1
#如果消費(fèi)者應(yīng)用在ack前異常宕機(jī),恢復(fù)后重新獲取未處理的消息id。
XPENDING mystream mygroup - + 10
1) 1) "1645517760385-0"
2) "consumer1"
3) (integer) 305356
4) (integer) 1
#如果consumer1永遠(yuǎn)宕機(jī),其他消費(fèi)者可以把pending狀態(tài)的消息移動(dòng)到自己名下后繼續(xù)消費(fèi)
#將消息id 1645517760385-0移動(dòng)到consumer2下
XCLAIM mystream mygroup consumer2 0 1645517760385-0
1) 1) "1645517760385-0"
2) 1) "field1"
2) "value1"
3) "field2"
4) "value2"
Redis stream保證了消息至少被處理一次,但如果想做到每條消息僅被處理一次還需要應(yīng)用邏輯的介入。
消息被重復(fù)處理要么是生產(chǎn)者重復(fù)投遞,要么是消費(fèi)者重復(fù)消費(fèi)。
- 對(duì)于生產(chǎn)者重復(fù)投遞問(wèn)題,Redis stream為每個(gè)消息都設(shè)置了一個(gè)唯一遞增的id,通過(guò)參數(shù)可以讓Redis自動(dòng)生成id或者應(yīng)用自己指定id,應(yīng)用可以根據(jù)業(yè)務(wù)邏輯為每個(gè)msg生成id,當(dāng)xadd超時(shí)后應(yīng)用并不能確定消息是否投遞成功,可以通過(guò)xread查詢?cè)搃d的消息是否存在,存在就說(shuō)明已經(jīng)投遞成功,不存在則重新投遞,而且stream限制了id必須遞增,這意味了已經(jīng)存在的消息重復(fù)投遞會(huì)被拒絕。這套機(jī)制保證了每個(gè)消息可以僅被投遞一次。
- 對(duì)于消費(fèi)者重復(fù)消費(fèi)的問(wèn)題,考慮一個(gè)場(chǎng)景,消費(fèi)者讀取消息后業(yè)務(wù)處理完畢,但還沒(méi)來(lái)得及ack就發(fā)生了異常,應(yīng)用恢復(fù)后對(duì)于這條沒(méi)有ack的消息進(jìn)行了重復(fù)消費(fèi)。這個(gè)問(wèn)題因?yàn)閍ck和消費(fèi)消息的業(yè)務(wù)邏輯發(fā)生在2個(gè)系統(tǒng),沒(méi)法做到事務(wù)性,需要業(yè)務(wù)來(lái)改造,保證消息處理的冪等性。
2.stream的不足
stream的模型做到了消息的高效分發(fā),而且保證了消息至少被處理一次,通過(guò)應(yīng)用邏輯的改造能做到消息僅被處理一次,它的能力對(duì)標(biāo)kafka,但吞吐高于kafka,在高吞吐場(chǎng)景下成本比kafka低,那它又有哪些不足了。
首先消息隊(duì)列很重要的一個(gè)功能就是削峰填谷,來(lái)匹配生產(chǎn)者和消費(fèi)者吞吐的差異,生產(chǎn)者和消費(fèi)者吞吐差異越大,持續(xù)時(shí)間越長(zhǎng),就意味著steam中需要堆積更多的消息,而Redis作為一個(gè)全內(nèi)存的產(chǎn)品,數(shù)據(jù)堆積的成本比磁盤高。
其次stream通過(guò)ack機(jī)制保證了消息至少被消費(fèi)一次,但這有個(gè)前提就是存儲(chǔ)在Redis中的消息本身不會(huì)丟失。Redis數(shù)據(jù)的持久化依賴aof和rdb文件,aof落盤方式有幾種,通過(guò)配置appendfsync決定,通常我們不會(huì)配置為always來(lái)讓每條命令執(zhí)行完后都做一次fsync,線上配置一般為everysec,每秒做一次fsync,而rdb是全量備份時(shí)生成,這意味了宕機(jī)恢復(fù)可能會(huì)丟掉最近一秒的數(shù)據(jù)。另一方面線上生產(chǎn)環(huán)境的Redis都是高可用架構(gòu),當(dāng)主節(jié)點(diǎn)宕機(jī)后通常不會(huì)走恢復(fù)邏輯,而是直接切換到備節(jié)點(diǎn)繼續(xù)提供服務(wù),而Redis的同步方式是異步同步,這意味著主節(jié)點(diǎn)上新寫入的數(shù)據(jù)可能還沒(méi)同步到備節(jié)點(diǎn),在切換后這部分?jǐn)?shù)據(jù)就丟失了。所以在故障恢復(fù)中Redis中的數(shù)據(jù)可能會(huì)丟失一部分,在這樣的背景下無(wú)論stream的接口設(shè)計(jì)的多么完善,都不能保證消息至少被消費(fèi)一次。
3.總結(jié)
優(yōu)勢(shì)
- 在成本、功能上做了很多改進(jìn),支持了緊湊的存儲(chǔ)小消息、具備廣播能力、消費(fèi)者能水平擴(kuò)容、具備背壓機(jī)制
- 通過(guò)ack機(jī)制保證了Redis服務(wù)端正常情況下消息至少被處理一次的能力
不足
- 內(nèi)存型消息隊(duì)列,數(shù)據(jù)堆積成本高
- Redis本身rpo>0,故障恢復(fù)可能會(huì)丟數(shù)據(jù),所以stream在Redis發(fā)生故障恢復(fù)后也不能保證消息至少被消費(fèi)一次。
四、Tair持久內(nèi)存版 stream
Redis stream的不足也是內(nèi)存型數(shù)據(jù)庫(kù)特性帶來(lái)的,它擁有高吞吐、低延時(shí),但大容量下成本會(huì)比較高,而應(yīng)用的場(chǎng)景也不完全是絕對(duì)的大容量低吞吐或小容量高吞吐,有時(shí)應(yīng)用的場(chǎng)景會(huì)介于二者之間,需要平衡容量和吞吐的關(guān)系,所以需要一個(gè)產(chǎn)品它的存儲(chǔ)成本低于Redis stream,但它的性能又高于磁盤型消息隊(duì)列。
另一方面Redis stream在Redis故障場(chǎng)景下不能保證消息的不丟失,這導(dǎo)致業(yè)務(wù)需要自己實(shí)現(xiàn)一些復(fù)雜的機(jī)制來(lái)回補(bǔ)這段數(shù)據(jù),同時(shí)也限制了它應(yīng)用在一些對(duì)一致性要求較高的場(chǎng)景。為了讓業(yè)務(wù)邏輯更簡(jiǎn)單,stream應(yīng)用范圍更廣,需要保證故障場(chǎng)景下的消息持久化。
兼顧成本、性能、持久化,這就有了Tair持久內(nèi)存版。
1.Tair持久內(nèi)存版特性
更大空間,更低成本
Tair持久內(nèi)存版引入了Intel傲騰持久內(nèi)存(下面稱作AEP),它的性能略低于內(nèi)存,但相同容量下成本低于內(nèi)存。Tair持久內(nèi)存版將主要數(shù)據(jù)存儲(chǔ)在AEP上,使得相同容量下,成本更低,這使同樣單價(jià)下stream能堆積更多的消息。
兼容社區(qū)版
Tair持久內(nèi)存版兼容原生Redis絕大部分的數(shù)據(jù)結(jié)構(gòu)和接口,對(duì)于stream相關(guān)接口做到了100%兼容,如果你之前使用了社區(qū)版stream,那么不需要修改任何代碼,只需要換一個(gè)連接地址就能切換到持久內(nèi)存版。并且通過(guò)工具完成社區(qū)版和持久內(nèi)存版數(shù)據(jù)的雙向遷移。
數(shù)據(jù)的實(shí)時(shí)持久化
Tair持久內(nèi)存版并不是簡(jiǎn)單將Redis中的數(shù)據(jù)換了一個(gè)介質(zhì)存儲(chǔ),因?yàn)檫@樣僅能通過(guò)AEP降低成本,但沒(méi)用到AEP斷電數(shù)據(jù)不丟失的特性,對(duì)持久化能力沒(méi)有任何提升。
開源Redis通過(guò)在磁盤上記錄AppendOnlyLog來(lái)持久化數(shù)據(jù),AppendOnlyLog記錄了所有的寫操作,相當(dāng)于redolog,在宕機(jī)恢復(fù)時(shí)通過(guò)回放這些log恢復(fù)數(shù)據(jù)。但受限于磁盤介質(zhì)的高延時(shí)和Redis內(nèi)存數(shù)據(jù)庫(kù)使用場(chǎng)景下對(duì)低延時(shí)的要求,并不能在每次寫操作后fsync持久化log,最新寫入的數(shù)據(jù)可能并沒(méi)有持久化到磁盤,這也是數(shù)據(jù)可能丟失的根因。
Tair持久內(nèi)存版的數(shù)據(jù)恢復(fù)沒(méi)有使用AppendOnlyLog來(lái)完成, 而是將將redis數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在AEP上,這樣宕機(jī)后這些數(shù)據(jù)結(jié)構(gòu)并不會(huì)丟失,并且對(duì)這些數(shù)據(jù)結(jié)構(gòu)增加了一些額外的描述信息,宕機(jī)后在recovery時(shí)能夠讀到這些額外的描述信息,讓這些redis數(shù)據(jù)結(jié)構(gòu)重新被識(shí)別和索引,將狀態(tài)恢復(fù)到宕機(jī)前的樣子。Tair通過(guò)將redis數(shù)據(jù)結(jié)構(gòu)和描述信息實(shí)時(shí)寫入AEP,保證了寫入數(shù)據(jù)的實(shí)時(shí)持久化。
HA數(shù)據(jù)不丟失
Tair持久內(nèi)存版保證了數(shù)據(jù)的持久化,但生產(chǎn)環(huán)境中都是高可用架構(gòu),多數(shù)情況下當(dāng)主節(jié)點(diǎn)異常宕機(jī)后并不會(huì)等主節(jié)點(diǎn)重啟恢復(fù),而是切換到備節(jié)點(diǎn)繼續(xù)提供服務(wù),然后給新的主節(jié)點(diǎn)添加一個(gè)新的備節(jié)點(diǎn)。所以在故障發(fā)生時(shí)如果有數(shù)據(jù)還沒(méi)從主節(jié)點(diǎn)同步到備節(jié)點(diǎn),這部分?jǐn)?shù)據(jù)就會(huì)丟失。
Redis采用的異步同步,當(dāng)客戶端寫入數(shù)據(jù)并返回成功時(shí)對(duì)Redis的修改可能還沒(méi)同步到備節(jié)點(diǎn),如果此時(shí)主節(jié)點(diǎn)宕機(jī)數(shù)據(jù)就會(huì)丟失。為了避免在HA過(guò)程中數(shù)據(jù)丟失,Tair持久內(nèi)存版引入了半同步機(jī)制,確保寫入請(qǐng)求返回成功前相關(guān)的修改已經(jīng)同步到備節(jié)點(diǎn)。
可以發(fā)現(xiàn)開啟半同步功能后寫入請(qǐng)求的RT會(huì)變高,多出主備同步的耗時(shí),這部分耗時(shí)大概在幾十微秒。但通過(guò)一些異步化的技術(shù),雖然寫請(qǐng)求的RT會(huì)變高,但對(duì)實(shí)例的最大寫吞吐影響很小。
當(dāng)開啟半同步后生成者通過(guò)xadd投遞消息,如果返回成功,消息一定同步到備節(jié)點(diǎn),此時(shí)發(fā)生HA,消費(fèi)者也能在備節(jié)點(diǎn)上讀到這條消息。如果xadd請(qǐng)求超時(shí),此時(shí)消息可能同步到備節(jié)點(diǎn)也可能沒(méi)有,生產(chǎn)者沒(méi)法確定,此時(shí)通過(guò)再次投遞消息,可以保證該消息至少被消費(fèi)一次。如果要嚴(yán)格保證消息僅被消費(fèi)一次,那么生產(chǎn)者可以通過(guò)xread接口查詢消息是否存在,對(duì)于不存在的場(chǎng)景重新投遞。
2.總結(jié)
優(yōu)勢(shì)
- 引入了AEP作為存儲(chǔ)介質(zhì),目前Tair持久內(nèi)存版價(jià)格是社區(qū)版的70%。
- 保證了數(shù)據(jù)的實(shí)時(shí)持久化,并且通過(guò)半同步技術(shù)保證了HA不丟數(shù)據(jù),大多數(shù)情況下做到消息不丟失(備庫(kù)故障或主備網(wǎng)絡(luò)異常時(shí)會(huì)降級(jí)為異步同步,優(yōu)先保障可用性),消息至少被消費(fèi)一次或僅被消費(fèi)一次。
五、未來(lái)
消息隊(duì)列主要是為了解決3類問(wèn)題,應(yīng)用模塊的解耦、消息的異步化、削峰填谷。目前主流的消息隊(duì)列都能滿足這些需求,所以在實(shí)際選型時(shí)還會(huì)考慮一些特殊的功能是否滿足,產(chǎn)品的性能如何,具體業(yè)務(wù)場(chǎng)景下的成本怎么樣,開發(fā)的復(fù)雜度等。
Redis的消息隊(duì)列功能并不是最全面的,它不希望做成一個(gè)大而全的產(chǎn)品,而是做一個(gè)小而美的產(chǎn)品,服務(wù)好一部分用戶在某些場(chǎng)景下的需求。目前用戶選型Redis作為消息隊(duì)列服務(wù)的原因,主要有Redis在相同成本下吞吐更高、Redis的延時(shí)更低、應(yīng)用需要一個(gè)消息服務(wù)但又不想額外引入一堆依賴等。
未來(lái)Tair持久內(nèi)存版會(huì)針對(duì)這些述求,把這些優(yōu)勢(shì)繼續(xù)放大。
吞吐
通過(guò)優(yōu)化持久內(nèi)存版的持久化流程,讓吞吐接近內(nèi)存版甚至超過(guò)內(nèi)存版吞吐。
延時(shí)
通過(guò)rdma在多副本間同步數(shù)據(jù),降低半同步下寫入數(shù)據(jù)的延時(shí)。