100 more of those BITFIELDs

Salvatore Sanfilippo

再來 100 個那樣的 BITFIELD

今天 Redis 滿 7 週年,所以為了稍微紀念這個日子,我花了最近幾天進行了一場有趣的程式開發馬拉松,實作了一個名為 BITFIELD 的瘋狂新指令。

這個指令的核心概念並不新,過去我和其他人也曾提過,但從來沒有認真看待過,這個想法總是有點奇怪。我們在 Redis 中已經有位元運算功能:某些使用者非常喜愛,這是將大量資料以緊湊方式呈現的好方法。然而到目前為止,我們都是分別處理每一個位元,設定、測試、取得位元,計算某個範圍內被設定的位元數量等等。

那如果實作位元欄位呢?無論是短或長、任意大小的整數,位於任意的偏移量上,讓我可以把一個 Redis 字串當成由 5 位元有號整數組成的陣列來使用,一點空間都不浪費。

幾天前,來自 Redis Labs 的 Yoav Steinberg(約阿夫·史坦伯格)以更認真的方式,針對儲存在位元偏移量上的任意大小整數提出了一組指令。當我讀到那封郵件時會心一笑,因為這有點算是我秘密的夢想。從約阿夫·史坦伯格的提案出發,並結合其他 Redis Labs 工程師的回饋,我撰寫了一份單一指令搭配子指令的初步規格,使用簡短的名稱來定義型別,並加入對溢位語意非常細緻的控制。

我在幾分鐘前剛完成第一個實作版本,因為原本的計畫就是在今天發布,希望 Redis 能感受到我們在它生日這天真的有在做事。

最終完成的 BITFIELD 指令支援不同的子指令:

SET <type> <offset> <value> — 設定指定的值並回傳其舊值。

GET <type> <offset> — 取得指定的值。

INCRBY <type> <offset> <increment> — 遞增指定的計數器。

還有一個額外的中繼指令叫做 OVERFLOW,用來設定接下來指令的溢位語意(所以 OVERFLOW 可以被指定多次):

OVERFLOW SAT — 飽和模式,因此無論往哪個方向溢位,都會讓整數在溢位方向上飽和至其最大值。

OVERFLOW WRAP — 這是常見的環繞模式,但有趣的是,這對有號整數也同樣有效,會朝最負或最正的值方向環繞。

OVERFLOW FAIL — 在此模式下,如果該值會造成溢位,則完全不會執行操作。

整數型別可以用「u」或「i」前綴加上位元數來指定,例如 u8、i5、u20 和 i53 都是有效的型別。有一項限制:目前無法指定 u64,因為 Redis 協定目前無法回傳 64 位元無號整數。

是時候來看幾個範例了:為了遞增一個 8 位元的無號整數,我可以使用:

127.0.0.1:6379> BITFIELD mykey incrby u8 100 1
1) (integer) 3

這是在遞增一個位於偏移量 100(也就是點陣圖中的第 101 個位元)的 8 位元無號整數。

然而,還有另一種指定偏移量的方式,就是在偏移量前加上「#」,意思是:「將字串視為指定大小的計數器陣列,並設定第 N 個計數器」。基本上這只代表,如果我對 8 位元型別使用 #10,偏移量就是以 8*10 計算而得,這樣我就可以獨立定址多個計數器,而不需要自行計算偏移量:

127.0.0.1:6379> BITFIELD mykey incrby u8 #0 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u8 #0 1
1) (integer) 2
127.0.0.1:6379> BITFIELD mykey incrby u8 #1 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u8 #1 1
1) (integer) 2

控制溢位的能力也很有趣。例如,一個 1 位元的無號計數器在預設的「wrap」溢位策略下,實際上會在 0 和 1 之間切換:

127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 0
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 0

如你所見,它會在 0 和 1 之間交替。

飽和模式也可能很有用:

127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -3
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -6
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -8
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -8

如你所見,遞減 3 永遠不會低於 -8。

請注意,你可以在單一指令中執行多個操作。它總是會回傳一個結果陣列:

127.0.0.1:6379> BITFIELD mykey get i4 100 set u8 200 123 incrby u8 300 1
1) (integer) -8
2) (integer) 123
3) (integer) 7

這個指令的預期用途是即時分析、A/B 測試,透過利用整數的溢位,每次向使用者顯示稍微不同的內容。將如此多的小型計數器以共享且節省記憶體的方式打包在一起,可以有許多應用方式,但這就留給 Redis 社群中才華洋溢的程式設計師們去發揮了。

這個指令將在接下來幾週內被回溯移植到 Redis 的穩定版本中,所以很快就可以使用了。

對實作感到好奇嗎?它可能比你預期的還要複雜: https://github.com/antirez/redis/commit/70af626d613ebd88123f87a941b0dd3570f9e7d2

原文由 Salvatore Sanfilippo 發布

本文章由 muse-spark-1.2-contributor 進行翻譯