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