成都網(wǎng)站建設(shè)小程序整站seo外包
1、String 類型的內(nèi)存空間消耗問題,以及選擇節(jié)省內(nèi)存開銷的數(shù)據(jù)類型的解決方案。
為什么 String 類型內(nèi)存開銷大?
圖片 ID 和圖片存儲對象 ID 都是 10 位數(shù),我們可以用兩個 8 字節(jié)的 Long 類型表示這兩個 ID。因?yàn)?8 字節(jié)的 Long 類型最大可以表示 2 的 64 次方的數(shù)值,所以肯定可以表示 10 位數(shù)。但是,為什么 String 類型卻用了 64 字節(jié)呢?
除了記錄實(shí)際數(shù)據(jù),String 類型還需要額外的內(nèi)存空間記錄數(shù)據(jù)長度、空間使用等信息,這些信息也叫作元數(shù)據(jù)。當(dāng)實(shí)際保存的數(shù)據(jù)較小時,元數(shù)據(jù)的空間開銷就顯得比較大了,有點(diǎn)“喧賓奪主”的意思。
當(dāng)你保存 64 位有符號整數(shù)時,String 類型會把它保存為一個 8 字節(jié)的 Long 類型整數(shù),這種保存方式通常也叫作 int 編碼方式。
但是,當(dāng)你保存的數(shù)據(jù)中包含字符時,String 類型就會用簡單動態(tài)字符串(Simple Dynamic String,SDS)結(jié)構(gòu)體來保存,如下圖所示:
可以看到,在 SDS 中,buf 保存實(shí)際數(shù)據(jù),而 len 和 alloc 本身其實(shí)是 SDS 結(jié)構(gòu)體的額外開銷。
另外,對于 String 類型來說,除了 SDS 的額外開銷,還有一個來自于 RedisObject 結(jié)構(gòu)體的開銷。因?yàn)?Redis 的數(shù)據(jù)類型有很多,而且,不同數(shù)據(jù)類型都有些相同的元數(shù)據(jù)要記錄(比如最后一次訪問的時間、被引用的次數(shù)等),所以,Redis 會用一個 RedisObject 結(jié)構(gòu)體來統(tǒng)一記錄這些元數(shù)據(jù),同時指向?qū)嶋H數(shù)據(jù)。
一個 RedisObject 包含了 8 字節(jié)的元數(shù)據(jù)和一個 8 字節(jié)指針,這個指針再進(jìn)一步指向具體數(shù)據(jù)類型的實(shí)際數(shù)據(jù)所在,例如指向 String 類型的 SDS 結(jié)構(gòu)所在的內(nèi)存地址,可以看一下下面的示意圖。關(guān)于 RedisObject 的具體結(jié)構(gòu)細(xì)節(jié),我會在后面的課程中詳細(xì)介紹,現(xiàn)在你只要了解它的基本結(jié)構(gòu)和元數(shù)據(jù)開銷就行了。
為了節(jié)省內(nèi)存空間,Redis 還對 Long 類型整數(shù)和 SDS 的內(nèi)存布局做了專門的設(shè)計(jì)。
一方面,當(dāng)保存的是 Long 類型整數(shù)時,RedisObject 中的指針就直接賦值為整數(shù)數(shù)據(jù)了,這樣就不用額外的指針再指向整數(shù)了,節(jié)省了指針的空間開銷。
另一方面,當(dāng)保存的是字符串?dāng)?shù)據(jù),并且字符串小于等于 44 字節(jié)時,RedisObject 中的元數(shù)據(jù)、指針和 SDS 是一塊連續(xù)的內(nèi)存區(qū)域,這樣就可以避免內(nèi)存碎片。這種布局方式也被稱為 embstr 編碼方式。
當(dāng)然,當(dāng)字符串大于 44 字節(jié)時,SDS 的數(shù)據(jù)量就開始變多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨(dú)立的空間,并用指針指向 SDS 結(jié)構(gòu)。這種布局方式被稱為 raw 編碼模式。
因?yàn)?10 位數(shù)的圖片 ID 和圖片存儲對象 ID 是 Long 類型整數(shù),所以可以直接用 int 編碼的 RedisObject 保存。每個 int 編碼的 RedisObject 元數(shù)據(jù)部分占 8 字節(jié),指針部分被直接賦值為 8 字節(jié)的整數(shù)了。此時,每個 ID 會使用 16 字節(jié),加起來一共是 32 字節(jié)。但是,另外的 32 字節(jié)去哪兒了呢?
Redis 會使用一個全局哈希表保存所有鍵值對,哈希表的每一項(xiàng)是一個 dictEntry 的結(jié)構(gòu)體,用來指向一個鍵值對。dictEntry 結(jié)構(gòu)中有三個 8 字節(jié)的指針,分別指向 key、value 以及下一個 dictEntry,三個指針共 24 字節(jié),如下圖所示:
但是,這三個指針只有 24 字節(jié),為什么會占用了 32 字節(jié)呢?這就要提到 Redis 使用的內(nèi)存分配庫 jemalloc 了。
jemalloc 在分配內(nèi)存時,會根據(jù)我們申請的字節(jié)數(shù) N,找一個比 N 大,但是最接近 N 的 2 的冪次數(shù)作為分配的空間,這樣可以減少頻繁分配的次數(shù)。所以,在我們剛剛說的場景里,dictEntry 結(jié)構(gòu)就占用了 32 字節(jié)。
用什么數(shù)據(jù)結(jié)構(gòu)可以節(jié)省內(nèi)存?
Redis 有一種底層數(shù)據(jù)結(jié)構(gòu),叫壓縮列表(ziplist),這是一種非常節(jié)省內(nèi)存的結(jié)構(gòu)。我們先回顧下壓縮列表的構(gòu)成。表頭有三個字段 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量,以及列表中的 entry 個數(shù)。壓縮列表尾還有一個 zlend,表示列表結(jié)束。
壓縮列表之所以能節(jié)省內(nèi)存,就在于它是用一系列連續(xù)的 entry 保存數(shù)據(jù)。每個 entry 的元數(shù)據(jù)包括下面幾部分。這些 entry 會挨個兒放置在內(nèi)存中,不需要再用額外的指針進(jìn)行連接,這樣就可以節(jié)省指針?biāo)加玫目臻g。
Redis 基于壓縮列表實(shí)現(xiàn)了 List、Hash 和 Sorted Set 這樣的集合類型,這樣做的最大好處就是節(jié)省了 dictEntry 的開銷。當(dāng)你用 String 類型時,一個鍵值對就有一個 dictEntry,要用 32 字節(jié)空間。但采用集合類型時,一個 key 就對應(yīng)一個集合的數(shù)據(jù),能保存的數(shù)據(jù)多了很多,但也只用了一個 dictEntry,這樣就節(jié)省了內(nèi)存。
如何用集合類型保存單值的鍵值對?
在保存單值的鍵值對時,可以采用基于 Hash 類型的二級編碼方法。這里說的二級編碼,就是把一個單值的數(shù)據(jù)拆分成兩部分,前一部分作為 Hash 集合的 key,后一部分作為 Hash 集合的 value,這樣一來,我們就可以把單值數(shù)據(jù)保存到 Hash 集合中了。
以圖片 ID 1101000060 和圖片存儲對象 ID 3302000080 為例,我們可以把圖片 ID 的前 7 位(1101000)作為 Hash 類型的鍵,把圖片 ID 的最后 3 位(060)和圖片存儲對象 ID 分別作為 Hash 類型值中的 key 和 value。按照這種設(shè)計(jì)方法,我在 Redis 中插入了一組圖片 ID 及其存儲對象 ID 的記錄,并且用 info 命令查看了內(nèi)存開銷,我發(fā)現(xiàn),增加一條記錄后,內(nèi)存占用只增加了 16 字
(實(shí)測老師的例子,**長度7位數(shù),共100萬條數(shù)據(jù)。使用string占用70mb,使用hash ziplist只占用9mb。**效果非常明顯。redis版本6.0.6)
2 有一億個keys要統(tǒng)計(jì),應(yīng)該用哪種集合?
在 Web 和移動應(yīng)用的業(yè)務(wù)場景中,我們經(jīng)常需要保存這樣一種信息:一個 key 對應(yīng)了一個數(shù)據(jù)集合
手機(jī) App 中的每天的用戶登錄信息:一天對應(yīng)一系列用戶 ID 一個網(wǎng)頁對應(yīng)一系列的訪問點(diǎn)擊。在這些場景中,除了記錄信息,我們往往還需要對集合中的數(shù)據(jù)進(jìn)行統(tǒng)計(jì),例如:
在移動應(yīng)用中,需要統(tǒng)計(jì)每天的新增用戶數(shù)和第二天的留存用戶數(shù);
在電商網(wǎng)站的商品評論中,需要統(tǒng)計(jì)評論列表中的最新評論
通常情況下,我們面臨的用戶數(shù)量以及訪問量都是巨大的,比如百萬、千萬級別的用戶數(shù)量,或者千萬級別、甚至億級別的訪問信息。所以,我們必須要選擇能夠非常高效地統(tǒng)計(jì)大量數(shù)據(jù)(例如億級)的集合類型。
要想選擇合適的集合,我們就得了解常用的集合統(tǒng)計(jì)模式。介紹集合類型常見的四種統(tǒng)計(jì)模式,包括聚合統(tǒng)計(jì)、排序統(tǒng)計(jì)、二值狀態(tài)統(tǒng)計(jì)和基數(shù)統(tǒng)計(jì)。以剛剛提到的這四個場景為例,來聊聊在這些統(tǒng)計(jì)模式下,什么集合類型能夠更快速地完成統(tǒng)計(jì),而且還節(jié)省內(nèi)存空間。
聚合統(tǒng)計(jì)
所謂的聚合統(tǒng)計(jì),就是指統(tǒng)計(jì)多個集合元素的聚合結(jié)果,包括:統(tǒng)計(jì)多個集合的共有元素(交集統(tǒng)計(jì));把兩個集合相比,統(tǒng)計(jì)其中一個集合獨(dú)有的元素(差集統(tǒng)計(jì));統(tǒng)計(jì)多個集合的所有元素(并集統(tǒng)計(jì))。在剛才提到的場景中,統(tǒng)計(jì)手機(jī) App 每天的新增用戶數(shù)和第二天的留存用戶數(shù),正好對應(yīng)了聚合統(tǒng)計(jì)。
要完成這個統(tǒng)計(jì)任務(wù),我們可以用一個集合記錄所有登錄過 App 的用戶 ID,同時,用另一個集合記錄每一天登錄過 App 的用戶 ID。然后,再對這兩個集合做聚合統(tǒng)計(jì)。
執(zhí)行 SDIFFSTORE 命令計(jì)算累計(jì)用戶 Set 和 user🆔20200804 Set 的差集,結(jié)果保存在 key 為 user:new 的 Set 中,如下所示:
SDIFFSTORE user:new user🆔20200804 user:id
可以看到,這個差集中的用戶 ID 在 user🆔20200804 的 Set 中存在,但是不在累計(jì)用戶 Set 中。所以,user:new 這個 Set 中記錄的就是 8 月 4 日的新增用戶。
當(dāng)要計(jì)算 8 月 4 日的留存用戶時,我們只需要再計(jì)算 user🆔20200803 和 user🆔20200804 兩個 Set 的交集,就可以得到同時在這兩個集合中的用戶 ID 了,這些就是在 8 月 3 日登錄,并且在 8 月 4 日留存的用戶。執(zhí)行的命令如下:
SINTERSTORE user🆔rem user🆔20200803 user🆔20200804
Set 的差集、并集和交集的計(jì)算復(fù)雜度較高,在數(shù)據(jù)量較大的情況下,如果直接執(zhí)行這些計(jì)算,會導(dǎo)致 Redis 實(shí)例阻塞。所以,我給你分享一個小建議:你可以從主從集群中選擇一個從庫,讓它專門負(fù)責(zé)聚合計(jì)算,或者是把數(shù)據(jù)讀取到客戶端,在客戶端來完成聚合統(tǒng)計(jì),這樣就可以規(guī)避阻塞主庫實(shí)例和其他從庫實(shí)例的風(fēng)險了。
排序統(tǒng)計(jì)
這就要求集合類型能對元素保序,也就是說,集合中的元素可以按序排列,這種對元素保序的集合類型叫作有序集合。
在 Redis 常用的 4 個集合類型中(List、Hash、Set、Sorted Set),List 和 Sorted Set 就屬于有序集合。List 是按照元素進(jìn)入 List 的順序進(jìn)行排序的,而 Sorted Set 可以根據(jù)元素的權(quán)重來排序,我們可以自己來決定每個元素的權(quán)重值。
比如說,我們可以根據(jù)元素插入 Sorted Set 的時間確定權(quán)重值,先插入的元素權(quán)重小,后插入的元素權(quán)重大。看起來好像都可以滿足需求,我們該怎么選擇呢?
我先說說用 List 的情況。每個商品對應(yīng)一個 List,這個 List 包含了對這個商品的所有評論,而且會按照評論時間保存這些評論,每來一個新評論,就用 LPUSH 命令把它插入 List 的隊(duì)頭。在只有一頁評論的時候,我們可以很清晰地看到最新的評論,但是,在實(shí)際應(yīng)用中,網(wǎng)站一般會分頁顯示最新的評論列表,一旦涉及到分頁操作,List 就可能會出現(xiàn)問題了。 List 是通過元素在 List 中的位置來排序的,當(dāng)有一個新元素插入時,原先的元素在 List 中的位置都后移了一位,比如說原來在第 1 位的元素現(xiàn)在排在了第 2 位。
和 List 相比,Sorted Set 就不存在這個問題,因?yàn)樗歉鶕?jù)元素的實(shí)際權(quán)重來排序和獲取數(shù)據(jù)的。我們可以按評論時間的先后給每條評論設(shè)置一個權(quán)重值,然后再把評論保存到 Sorted Set 中。
Sorted Set 的 ZRANGEBYSCORE 命令就可以按權(quán)重排序后返回元素。這樣的話,即使集合中的元素頻繁更新,Sorted Set 也能通過 ZRANGEBYSCORE 命令準(zhǔn)確地獲取到按序排列的數(shù)據(jù)。
設(shè)越新的評論權(quán)重越大,目前最新評論的權(quán)重是 N,我們執(zhí)行下面的命令時,就可以獲得最新的 10 條評論:
ZRANGEBYSCORE comments N-9 N
所以,在面對需要展示最新列表、排行榜等場景時,如果數(shù)據(jù)更新頻繁或者需要分頁顯示,建議你優(yōu)先考慮使用 Sorted Set。
二值狀態(tài)統(tǒng)計(jì)
現(xiàn)在,我們再來分析下第三個場景:二值狀態(tài)統(tǒng)計(jì)。這里的二值狀態(tài)就是指集合元素的取值就只有 0 和 1 兩種。在簽到打卡的場景中,我們只用記錄簽到(1)或未簽到(0),所以它就是非常典型的二值狀態(tài),
在簽到統(tǒng)計(jì)時,每個用戶一天的簽到用 1 個 bit 位就能表示,一個月(假設(shè)是 31 天)的簽到情況用 31 個 bit 位就可以,而一年的簽到也只需要用 365 個 bit 位,根本不用太復(fù)雜的集合類型。這個時候,我們就可以選擇 Bitmap。
SETBIT GETBIT BITCOUNT。注意是從0開始的,所以SETBIT uid:sign:3000:202008 2 1 是設(shè)置8月3號已經(jīng)簽到。
在統(tǒng)計(jì) 1 億個用戶連續(xù) 10 天的簽到情況時,你可以把每天的日期作為 key,每個 key 對應(yīng)一個 1 億位的 Bitmap,每一個 bit 對應(yīng)一個用戶當(dāng)天的簽到情況。接下來,我們對 10 個 Bitmap 做“與”操作,得到的結(jié)果也是一個 Bitmap。最后,我們可以用 BITCOUNT 統(tǒng)計(jì)下 Bitmap 中的 1 的個數(shù),這就是連續(xù)簽到 10 天的用戶總數(shù)了。
不過,在實(shí)際應(yīng)用時,最好對 Bitmap 設(shè)置過期時間,讓 Redis 自動刪除不再需要的簽到記錄,以節(jié)省內(nèi)存開銷
基數(shù)統(tǒng)計(jì)
最后,我們再來看一個統(tǒng)計(jì)場景:基數(shù)統(tǒng)計(jì)?;鶖?shù)統(tǒng)計(jì)就是指統(tǒng)計(jì)一個集合中不重復(fù)的元素個數(shù)。對應(yīng)到我們剛才介紹的場景中,就是統(tǒng)計(jì)網(wǎng)頁的 UV。網(wǎng)頁 UV 的統(tǒng)計(jì)有個獨(dú)特的地方,就是需要去重,一個用戶一天內(nèi)的多次訪問只能算作一次。在 Redis 的集合類型中,Set 類型默認(rèn)支持去重,所以看到有去重需求時,我們可能第一時間就會想到用 Set 類型。當(dāng)你需要統(tǒng)計(jì) UV 時,可以直接用 SCARD 命令,這個命令會返回一個集合中的元素個數(shù)。
但是,如果 page1 非?;鸨?#xff0c;UV 達(dá)到了千萬,這個時候,一個 Set 就要記錄千萬個用戶 ID。對于一個搞大促的電商網(wǎng)站而言,這樣的頁面可能有成千上萬個,如果每個頁面都用這樣的一個 Set,就會消耗很大的內(nèi)存空間。
這時候,就要用到 Redis 提供的 HyperLogLog 了。HyperLogLog 是一種用于統(tǒng)計(jì)基數(shù)的數(shù)據(jù)集合類型,它的最大優(yōu)勢就在于,當(dāng)集合元素數(shù)量非常多時,它計(jì)算基數(shù)所需的空間總是固定的,而且還很小。不過,有一點(diǎn)需要你注意一下,HyperLogLog 的統(tǒng)計(jì)規(guī)則是基于概率完成的,所以它給出的統(tǒng)計(jì)結(jié)果是有一定誤差的,標(biāo)準(zhǔn)誤算率是 0.81%。
面向 LBS 應(yīng)用的 GEO 數(shù)據(jù)類型
一輛車(或一個用戶)對應(yīng)一組經(jīng)緯度,并且隨著車(或用戶)的位置移動,相應(yīng)的經(jīng)緯度也會變化。這種數(shù)據(jù)記錄模式屬于一個 key(例如車 ID)對應(yīng)一個 value(一組經(jīng)緯度)
當(dāng)有很多車輛信息要保存時,就需要有一個集合來保存一系列的 key 和 value。Hash 集合類型可以快速存取一系列的 key 和 value,正好可以用來記錄一系列車輛 ID 和經(jīng)緯度的對應(yīng)關(guān)系,所以,我們可以把不同車輛的 ID 和它們對應(yīng)的經(jīng)緯度信息存在 Hash 集合中。同時,Hash 類型的 HSET 操作命令,會根據(jù) key 來設(shè)置相應(yīng)的 value 值,所以,我們可以用它來快速地更新車輛變化的經(jīng)緯度信息。到這里,Hash 類型看起來是一個不錯的選擇。
但問題是,對于一個 LBS 應(yīng)用來說,除了記錄經(jīng)緯度信息,還需要根據(jù)用戶的經(jīng)緯度信息在車輛的 Hash 集合中進(jìn)行范圍查詢。一旦涉及到范圍查詢,就意味著集合中的元素需要有序,但 Hash 類型的元素是無序的,顯然不能滿足我們的要求。
Sorted Set 類型也支持一個 key 對應(yīng)一個 value 的記錄模式,其中,key 就是 Sorted Set 中的元素,而 value 則是元素的權(quán)重分?jǐn)?shù)。更重要的是,Sorted Set 可以根據(jù)元素的權(quán)重分?jǐn)?shù)排序,支持范圍查詢。這就能滿足 LBS 服務(wù)中查找相鄰位置的需求了。實(shí)際上,**GEO 類型的底層數(shù)據(jù)結(jié)構(gòu)就是用 Sorted Set 來實(shí)現(xiàn)的。**這時問題來了,Sorted Set 元素的權(quán)重分?jǐn)?shù)是一個浮點(diǎn)數(shù)(float 類型),而一組經(jīng)緯度包含的是經(jīng)度和緯度兩個值,是沒法直接保存為一個浮點(diǎn)數(shù)的,那具體該怎么進(jìn)行保存呢?
這就要用到 GEO 類型中的 GeoHash 編碼了。
如何定義新的數(shù)據(jù)類型
首先,我們需要了解 Redis 的基本對象結(jié)構(gòu) RedisObject,因?yàn)?Redis 鍵值對中的每一個值都是用 RedisObject 保存的。
RedisObject 的內(nèi)部組成包括了 type,、encoding,、lru 和 refcount 4 個元數(shù)據(jù),以及 1 個*ptr指針。
首先,我們需要為新數(shù)據(jù)類型定義好它的底層結(jié)構(gòu)、type 和 encoding 屬性值,然后再實(shí)現(xiàn)新數(shù)據(jù)類型的創(chuàng)建、釋放函數(shù)和基本命令。
如何在Redis中保存時間序列數(shù)據(jù)?
在實(shí)際應(yīng)用中,時間序列數(shù)據(jù)通常是持續(xù)高并發(fā)寫入的,例如,需要連續(xù)記錄數(shù)萬個設(shè)備的實(shí)時狀態(tài)值。同時,時間序列數(shù)據(jù)的寫入主要就是插入新數(shù)據(jù),而不是更新一個已存在的數(shù)據(jù),也就是說,一個時間序列數(shù)據(jù)被記錄后通常就不會變了,因?yàn)樗痛砹艘粋€設(shè)備在某個時刻的狀態(tài)值。
所以,這種數(shù)據(jù)的寫入特點(diǎn)很簡單,就是插入數(shù)據(jù)快,這就要求我們選擇的數(shù)據(jù)類型,在進(jìn)行數(shù)據(jù)插入時,復(fù)雜度要低,盡量不要阻塞??吹竭@兒,你可能第一時間會想到用 Redis 的 String、Hash 類型來保存,因?yàn)樗鼈兊牟迦霃?fù)雜度都是 O(1),是個不錯的選擇。但是,String 類型在記錄小數(shù)據(jù)時(例如剛才例子中的設(shè)備溫度值),元數(shù)據(jù)的內(nèi)存開銷比較大,不太適合保存大量數(shù)據(jù)。
基于 Hash 和 Sorted Set 保存時間序列數(shù)據(jù)
關(guān)于 Hash 類型,我們都知道,它有一個特點(diǎn)是,可以實(shí)現(xiàn)對單鍵的快速查詢。這就滿足了時間序列數(shù)據(jù)的單鍵查詢需求。我們可以把時間戳作為 Hash 集合的 key,把記錄的設(shè)備狀態(tài)值作為 Hash 集合的 value。當(dāng)我們想要查詢某個時間點(diǎn)或者是多個時間點(diǎn)上的溫度數(shù)據(jù)時,直接使用 HGET 命令或者 HMGET 命令,就可以分別獲得 Hash 集合中的一個 key 和多個 key 的 value 值了。
但是,Hash 類型有個短板:它并不支持對數(shù)據(jù)進(jìn)行范圍查詢。
為了能同時支持按時間戳范圍的查詢,可以用 Sorted Set 來保存時間序列數(shù)據(jù),因?yàn)樗軌蚋鶕?jù)元素的權(quán)重分?jǐn)?shù)來排序。我們可以把時間戳作為 Sorted Set 集合的元素分?jǐn)?shù),把時間點(diǎn)上記錄的數(shù)據(jù)作為元素本身。
如何保證寫入 Hash 和 Sorted Set 是一個原子性的操作呢?
所謂“原子性的操作”,就是指我們執(zhí)行多個寫命令操作時(例如用 HSET 命令和 ZADD 命令分別把數(shù)據(jù)寫入 Hash 和 Sorted Set),這些命令操作要么全部完成,要么都不完成。這里就涉及到了 Redis 用來實(shí)現(xiàn)簡單的事務(wù)的 MULTI 和 EXEC 命令。當(dāng)多個命令及其參數(shù)本身無誤時,MULTI 和 EXEC 命令可以保證執(zhí)行這些命令時的原子性(相當(dāng)于mysql事務(wù)的begin commit)
接下來,我們需要繼續(xù)解決第三個問題:如何對時間序列數(shù)據(jù)進(jìn)行聚合計(jì)算?
因?yàn)?Sorted Set 只支持范圍查詢,無法直接進(jìn)行聚合計(jì)算,所以,我們只能先把時間范圍內(nèi)的數(shù)據(jù)取回到客戶端,然后在客戶端自行完成聚合計(jì)算。為了避免客戶端和 Redis 實(shí)例間頻繁的大量數(shù)據(jù)傳輸,我們可以使用 RedisTimeSeries 來保存時間序列數(shù)據(jù)
所以,如果我們只需要進(jìn)行單個時間點(diǎn)查詢或是對某個時間范圍查詢的話,適合使用 Hash 和 Sorted Set 的組合,它們都是 Redis 的內(nèi)在數(shù)據(jù)結(jié)構(gòu),性能好,穩(wěn)定性高。但是,如果我們需要進(jìn)行大量的聚合計(jì)算,同時網(wǎng)絡(luò)帶寬條件不是太好時,Hash 和 Sorted Set 的組合就不太適合了。此時,使用 RedisTimeSeries 就更加合適一些。
消息隊(duì)列的考驗(yàn):Redis有哪些解決方案?
現(xiàn)在的互聯(lián)網(wǎng)應(yīng)用基本上都是采用分布式系統(tǒng)架構(gòu)進(jìn)行設(shè)計(jì)的,而很多分布式系統(tǒng)必備的一個基礎(chǔ)軟件就是消息隊(duì)列。
消息隊(duì)列的消息存取需求
不過,消息隊(duì)列在存取消息時,必須要滿足三個需求,分別是消息保序、處理重復(fù)的消息(消費(fèi)者從消息隊(duì)列讀取消息時,有時會因?yàn)榫W(wǎng)絡(luò)堵塞而出現(xiàn)消息重傳的情況。此時,消費(fèi)者可能會收到多條重復(fù)的消息。)和保證消息可靠性(當(dāng)消費(fèi)者重啟后,可以重新讀取消息再次進(jìn)行處理,否則,就會出現(xiàn)消息漏處理的問題了。)。
基于 List 的消息隊(duì)列解決方案
List 本身就是按先進(jìn)先出的順序?qū)?shù)據(jù)進(jìn)行存取的,所以,如果使用 List 作為消息隊(duì)列保存消息的話,就已經(jīng)能滿足消息保序的需求了。
,如果消費(fèi)者想要及時處理消息,就需要在程序中不停地調(diào)用 RPOP 命令(比如使用一個 while(1) 循環(huán))。如果有新消息寫入,RPOP 命令就會返回結(jié)果,否則,RPOP 命令返回空值,再繼續(xù)循環(huán)。為了解決這個問題,Redis 提供了 BRPOP 命令。BRPOP 命令也稱為阻塞式讀取,客戶端在沒有讀到隊(duì)列數(shù)據(jù)時,自動阻塞,直到有新的數(shù)據(jù)寫入隊(duì)列,再開始讀取新數(shù)據(jù)。
消息保序的問題解決了,接下來,我們還需要考慮解決重復(fù)消息處理的問題,這里其實(shí)有一個要求:消費(fèi)者程序本身能對重復(fù)消息進(jìn)行判斷。
一方面,消息隊(duì)列要能給每一個消息提供全局唯一的 ID 號;另一方面,消費(fèi)者程序要把已經(jīng)處理過的消息的 ID 號記錄下來。
當(dāng)消費(fèi)者程序從 List 中讀取一條消息后,List 就不會再留存這條消息了。所以,如果消費(fèi)者程序在處理消息的過程出現(xiàn)了故障或宕機(jī),就會導(dǎo)致消息沒有處理完成,那么,消費(fèi)者程序再次啟動后,就沒法再次從 List 中讀取消息了。
為了留存消息,List 類型提供了 BRPOPLPUSH 命令,這個命令的作用是讓消費(fèi)者程序從一個 List 中讀取消息,同時,Redis 會把這個消息再插入到另一個 List(可以叫作備份 List)留存。這樣一來,如果消費(fèi)者程序讀了消息但沒能正常處理,等它重啟后,就可以從備份 List 中重新讀取消息并進(jìn)行處理了。
這就要說到 Redis 從 5.0 版本開始提供的 Streams 數(shù)據(jù)類型了。和 List 相比,Streams 同樣能夠滿足消息隊(duì)列的三大需求。而且,它還支持消費(fèi)組形式的消息讀取。
其實(shí),關(guān)于 Redis 是否適合做消息隊(duì)列,業(yè)界一直是有爭論的。很多人認(rèn)為,要使用消息隊(duì)列,就應(yīng)該采用 Kafka、RabbitMQ 這些專門面向消息隊(duì)列場景的軟件,而 Redis 更加適合做緩存。
我的看法是:Redis 是一個非常輕量級的鍵值數(shù)據(jù)庫,部署一個 Redis 實(shí)例就是啟動一個進(jìn)程,部署 Redis 集群,也就是部署多個 Redis 實(shí)例。而 Kafka、RabbitMQ 部署時,涉及額外的組件,例如 Kafka 的運(yùn)行就需要再部署 ZooKeeper。相比 Redis 來說,Kafka 和 RabbitMQ 一般被認(rèn)為是重量級的消息隊(duì)列。