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