來源:antirez程序員
翻譯:Kevin (公衆號:中間件小哥)redis
Redis 5 中引入了一個名爲 Streams 的新的 Redis 數據結構,吸引了社區極大的興趣。接下來,我會在社區裏進行調查,同用戶們談談他們在實際生產中的使用場景,而後寫個博客記錄一下。數據庫
今天我想解決另外一個問題:我有點懷疑許多用戶僅僅把Streams 做爲解決相似 Kafka 所要解決的問題的一個手段。實際上,這個數據結構,在當初設計的時候,在生產者/消費者消息通訊的場景下,也是能夠用起來的。並且我意識到 Streams 是很擅長這個場景的,用法也很簡潔。Streaming 是一個很好的模式和「思惟模型」,在被用來設計系統時,能夠得到巨大的成功。可是 Redis Streams 就像大多數 Redis 數據結構同樣,是比較通用的結構,能夠用來對許多不一樣的問題進行建模。在本篇博文中,我將聚焦在做爲純粹數據結構的 Streams,徹底忽略其阻塞式的操做、消費者羣組和全部和消息通信有關的部分。數據結構
做爲 CSV 文件增強版的 Streamsapp
若是你要把一系列結構化的數據項記錄下來,而且以爲用數據庫畢竟有點「殺雞用牛刀」,那麼你可能會說:讓咱們以「僅追加」(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 文件的一些限制:編碼
從另一個角度看,這些 CSV 條目的日誌也有好的方面:他們沒有固定的結構,數據列能夠變化,容易生成,並且畢竟其結構也是比較緊湊的。Redis Streams 的設計理念就是取長補短,其結果就是一個和 Redis Sorted sets 很是相似的混合型數據結構:他們看起來像是一個基礎數據結構,爲了達到這樣一個效果,在底層他們有多種表現形式。spa
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"
經過這個簡單的操做,咱們獲得了:
在 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 的使用場景之間有着很強的關聯性,就好像這個數據結構只擅長這種場景同樣,但其實不是這樣的。:-)
多優質中間件技術資訊/原創/翻譯文章/資料/乾貨,請關注「中間件小哥」公衆號!