您好,登錄后才能下訂單哦!
來源:antirez
翻譯:Kevin (公眾號:中間件小哥)
Redis 5 中引入了一個名為 Streams 的新的 Redis 數(shù)據(jù)結(jié)構(gòu),吸引了社區(qū)極大的興趣。接下來,我會在社區(qū)里進(jìn)行調(diào)查,同用戶們談?wù)勊麄冊趯嶋H生產(chǎn)中的使用場景,然后寫個博客記錄一下。
今天我想解決另一個問題:我有點懷疑許多用戶僅僅把Streams 作為解決類似 Kafka 所要解決的問題的一個手段。實際上,這個數(shù)據(jù)結(jié)構(gòu),在當(dāng)初設(shè)計的時候,在生產(chǎn)者/消費者消息通信的場景下,也是可以用起來的。而且我意識到 Streams 是很擅長這個場景的,用法也很簡潔。Streaming 是一個很好的模式和“思維模型”,在被用來設(shè)計系統(tǒng)時,可以獲得巨大的成功。但是 Redis Streams 就像大多數(shù) Redis 數(shù)據(jù)結(jié)構(gòu)一樣,是比較通用的結(jié)構(gòu),可以用來對許多不同的問題進(jìn)行建模。在本篇博文中,我將聚焦在作為純粹數(shù)據(jù)結(jié)構(gòu)的?Streams,完全忽略其阻塞式的操作、消費者群組和所有和消息通訊有關(guān)的部分。
作為?CSV?文件加強(qiáng)版的?Streams
如果你要把一系列結(jié)構(gòu)化的數(shù)據(jù)項記錄下來,并且覺得用數(shù)據(jù)庫畢竟有點“殺雞用牛刀”,那么你可能會說:讓我們以“僅追加”(append only)模式打開一個文件,然后把每一行作為 CSV(逗號分隔的值)格式記錄下來:
(以 append only 模式打開 data.csv 文件)
time=1553096724033,cpu_temp=23.4,load=2.3
time=1553096725029,cpu_temp=23.2,load=2.1
看起來是很簡單的,是吧,人們一直也是這么做的:這是一個一致的模式,如果你知道你在做什么的話。但是和這個(文件)模式對等的 in-memory(內(nèi)存)模式是怎樣的呢?內(nèi)存比 append only 文件更強(qiáng)大,自然也就沒有類似 CSV 文件的一些限制:
做范圍查詢比較難(效率低);
太多冗余信息:每條記錄中的時間差不多是一樣的,而且許多列都是重復(fù)的。同時,在你想切換到不同的一組列時,如果移除這些冗余信息,這會使得格式的靈活性更低。
數(shù)據(jù)項的位移就是文件中的字節(jié)位移:如果我們改變文件的結(jié)構(gòu),那么位移值就會是錯的,所以實際上這里沒有真正的 primary Id 的概念。
我不能移除這些數(shù)據(jù)條目,在沒有 GC(垃圾收集)能力的情況下,只能將他們標(biāo)記為“失效”,如果不重寫 log(日志)的話。而且因為某些原因,日志重寫的性能很差,如果能夠避免的話,就再好不過了。
從另外一個角度看,這些 CSV 條目的日志也有好的方面:他們沒有固定的結(jié)構(gòu),數(shù)據(jù)列可以變化,容易生成,而且畢竟其結(jié)構(gòu)也是比較緊湊的。Redis Streams 的設(shè)計理念就是取長補(bǔ)短,其結(jié)果就是一個和 Redis Sorted sets 非常類似的混合型數(shù)據(jù)結(jié)構(gòu):他們看起來像是一個基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),為了達(dá)到這樣一個效果,在底層他們有多種表現(xiàn)形式。
Streams 101
(你可以跳過這個部分,如果你已經(jīng)了解 Redis Streams 的基礎(chǔ)的話)
Redis Streams 由差分壓縮(delta-compressed)的宏節(jié)點表示,這些節(jié)點通過基數(shù)樹(radix tree)連接在一起。其效果就是,可以非??斓倪M(jìn)行隨機(jī)查找、按需獲取范圍、刪除老的數(shù)據(jù)項,從而創(chuàng)建一個帶上限的 stream,等等。同時,給程序員的接口和 CSV 文件是非常類似的:
> XADD mystream * cpu-temp 23.4 load 2.3
"1553097561402-0"
> XADD mystream * cpu-temp 23.2 load 2.1
"1553097568315-0"
從上面的例子我們看到,XADD 命令自動產(chǎn)生和返回了記錄 ID,記錄 ID 是單調(diào)遞增的,由 2 個部分組成:<時間>-<計數(shù)器>,時間以毫秒表示,對于在同一毫秒中產(chǎn)生的記錄,計數(shù)器會遞增。
以“只追加(append only)CSV 文件”的思想作為基礎(chǔ),我們構(gòu)建的第一個新的抽象是:既然我們使用星號作為 XADD 命令的 ID 參數(shù),從服務(wù)側(cè)我們就可以免費得到記錄 ID。這個 ID 不僅可以用來指示一個 stream 中的某一條數(shù)據(jù)記錄,也關(guān)聯(lián)了這條記錄加入 stream 的時間。實際上,XRANGE 命令既可以做范圍查詢,也可以查詢單條記錄。
> XRANGE mystream 1553097561402-0 1553097561402-0
1) 1) "1553097561402-0"
?? 2) 1) "cpu-temp"
????? 2) "23.4"
????? 3) "load"
????? 4) "2.3"
在這個例子中,為了標(biāo)識單個元素,我使用了相同的 ID 作為范圍查詢的起止條件。但是,我也可以使用任何范圍條件,加上一個 COUNT 參數(shù)來限制查詢結(jié)果的個數(shù)。同樣的,也不必詳細(xì)指明完整的 ID 作為范圍條件,可以只用 ID 的 Unix 毫秒時間戳部分,來獲取給定時間范圍內(nèi)的元素。
> XRANGE mystream 1553097560000 1553097570000
1) 1) "1553097561402-0"
?? 2) 1) "cpu-temp"
????? 2) "23.4"
????? 3) "load"
????? 4) "2.3"
2) 1) "1553097568315-0"
?? 2) 1) "cpu-temp"
????? 2) "23.2"
????? 3) "load"
????? 4) "2.1"
現(xiàn)在,沒必要展示更多的 Streams API 了,詳細(xì)的內(nèi)容可以參考 Redis 文檔。讓我們聚焦在其使用模式上:XADD 用來添加元素,XRANGE(也包括 XREAD)是用來獲取范圍內(nèi)的元素(取決于你的目的),讓我們看下為什么我把 Streams 稱為一個如此強(qiáng)大的數(shù)據(jù)結(jié)構(gòu)。
如果你想對 Streams 及其 API 了解更多的話,請一定看下這篇教程:https://redis.io/topics/streams-intro
網(wǎng)球選手
幾天前我和一個最近正在學(xué)習(xí) Redis 的朋友一起對一個應(yīng)用進(jìn)行建模,這個應(yīng)用是用來記錄本地的網(wǎng)球場、本地的選手和比賽的。用來對選手建模的方法是顯而易見的:一個選手是一個小的對象,所以一個 hash 值加上選手:<id>的鍵就夠了。當(dāng)你使用 Redis 作為首要的應(yīng)用數(shù)據(jù)建模的手段,你會馬上意識到,你需要一個方法來記錄在一個給定網(wǎng)球俱樂部中舉行的比賽。如果選手 1 和選手 2 打了一場比賽,選手 1 贏了,我們可以在一個 stream 中記錄如下:
> XADD club:1234.matches * player-a 1 player-b 2 winner 1
"1553254144387-0"
通過這個簡單的操作,我們得到了:
一個唯一的比賽 ID:stream 中的 ID;
不需要為了標(biāo)識一場比賽而創(chuàng)建一個對象;
免費的范圍查詢可以對比賽記錄進(jìn)行分頁,也可以查看在過去一個給定時刻的比賽記錄;
在 Streams 出現(xiàn)前,我們需要創(chuàng)建一個按時間排序的 sorted set。sorted set 中的元素就是比賽的 ID,同時還需要作為 hash 值保存在一個不同的 key 中。這不僅意味著更多的工作,同時也帶來了難以想象的內(nèi)存浪費。還有更多的你能想到的情況(后面可以看到)。
目前,可以看到的一點是,Redis Streams 就是一種處于僅追加模式(append only)的 Sorted Set,以時間作為鍵,每個元素是一個小的 hash 值。在對 Redis 進(jìn)行建模的場景下,帶來革命性的一點就是他的簡潔。
內(nèi)存使用
上述用例不僅意味著一個從行為上看更為一致的模式。比起老的 Sorted set + hash 的方式,Stream 方案的內(nèi)存開銷是如此之低,以至于之前不具有可行性的東西,現(xiàn)在完全是可行的。
以下數(shù)字是按之前的配置計算的、保存 100 萬條比賽數(shù)據(jù)的開銷:
Sorted Set + Hash 內(nèi)存開銷 = 220 MB (242 RSS)
Stream 內(nèi)存開銷 = 16.8 MB (18.11 RSS)
這超過了一個數(shù)量級的差別(準(zhǔn)確的說是 13 倍的差別),而且這意味著那些之前在內(nèi)存中開銷太大的用例,現(xiàn)在完全是可行的。神奇的地方就在于 Redis Streams:宏節(jié)點可以包含多個以 listpack 數(shù)據(jù)結(jié)構(gòu)、非常緊湊的方式編碼的元素。例如,即使整數(shù)在語義上是字符串,但 listpack 可以把他們編碼為二進(jìn)制形式。在這個基礎(chǔ)上,我們可以進(jìn)行差分壓縮和“相同列”的壓縮。同時,因為宏節(jié)點在基數(shù)樹(在設(shè)計上僅占用很少的內(nèi)存)中鏈接在一起,我們也可以通過 ID 和時間進(jìn)行查詢。所有這些加在一起,使得內(nèi)存占用很少。有意思的是,在語義上,用戶看不到任何使得 Streams 如此高效的實現(xiàn)細(xì)節(jié)。
現(xiàn)在,讓我們做一個簡單的計算。如果我可以用 18MB 的內(nèi)存存儲 1 百萬條記錄,180MB 存 1 千萬條,1.8GB 存 1 億條記錄。如果有 18GB 內(nèi)存的話,可以存 10 億條記錄。
時間序列
依我看,我們需要重點關(guān)注的是,上述我們使用 Stream 表示網(wǎng)球比賽的用法,在語義上,同使用 Stream 處理一個時間序列是完全不同的。是的,邏輯上我們?nèi)匀辉谟涗浤撤N事件,但一個重要的區(qū)別是,在一種場景下,我們記錄和創(chuàng)建記錄條目來呈現(xiàn)對象;在時間序列場景下,我們只是測量某些外部發(fā)生的事情,而這并不會表示成一個對象。你可能認(rèn)為這個區(qū)別不重要,但其實不然。對于 Redis 用戶,重要的一點是需要建立一個概念,Redis Streams 可以用來創(chuàng)建具有全序的小對象,每個對象都有一個 ID。
時間序列是一個最基礎(chǔ)的使用場景,顯然,也是最重要的使用場景,但在 Streams 出現(xiàn)前,Redis 對這種場景是有些無能為力的。Streams 的內(nèi)存特性和靈活性,加上帶上限的 stream(capped stream)的能力(參考 XADD 命令的參數(shù)選項),在開發(fā)者的手中是一個非常有力的工具。
結(jié)論
Streams 是非常靈活的,而且有很多使用場景。好了,話不多說,上述的例子我想要傳達(dá)的一個關(guān)鍵信息就是關(guān)于內(nèi)存使用的分析,也許對于許多讀者來說這已經(jīng)很明顯了,但是最近幾個月和人們的交談給我一種感覺,在 Streams 和 Streams 的使用場景之間有著很強(qiáng)的關(guān)聯(lián)性,就好像這個數(shù)據(jù)結(jié)構(gòu)只擅長這種場景一樣,但其實不是這樣的。:-)
多優(yōu)質(zhì)中間件技術(shù)資訊/原創(chuàng)/翻譯文章/資料/干貨,請關(guān)注“中間件小哥”公眾號!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。