Redis學習之深入了解Bitmaps

本篇文章帶大家了解一下redis中的bitmaps,詳細介紹 bitmaps 概念,操作以及常見應用,希望對大家有所幫助!

Redis學習之深入了解Bitmaps

redis版本:6.2.6

一、簡單介紹 Bitmaps

位圖不是實際的數據類型,而是在 String 類型上定義的一組面向位的操作。由于字符串是二進制安全的 blob,并且它們的最大長度為 512 MB,因此它們適合設置多達 2^32 個不同的位。【相關推薦:Redis視頻教程

? ? ? ? 上述是Redis官網對 Bitmaps 的介紹,簡單理解 Bitmaps 就是 Redis 提供的一系列直接操作 String 的位的指令,比如我們現在有一個字符串 :“a”

127.0.0.1:6379>?set?k1?a OK 127.0.0.1:6379>?get?k1? "a"

a 的二進制是:0110 0001,我們可以利用 [ GETBIT key offset ]指令,獲取這個字符串對應 位 的數值:

127.0.0.1:6379>?getbit?k1?0 (integer)?0 127.0.0.1:6379>?getbit?k1?1 (integer)?1 127.0.0.1:6379>?getbit?k1?2 (integer)?1 127.0.0.1:6379>?getbit?k1?3 (integer)?0 127.0.0.1:6379>?getbit?k1?4 (integer)?0 127.0.0.1:6379>?getbit?k1?5 (integer)?0 127.0.0.1:6379>?getbit?k1?6 (integer)?0 127.0.0.1:6379>?getbit?k1?7 (integer)?1

這個指令中的 offset 表示偏移量,如上面展示可以看到,偏移 1 位,2 位,7 位的數值是 1,其他位是 0,對應的二進制就是:0110 0001,這是字符 a 的 ASCII 值。

接下來我們可以利用 [SETBIT key offset value ] 指令,去改變某一位的值,比如:

127.0.0.1:6379>?setbit?k1?6?1?//偏移6位,置為1 (integer)?0 127.0.0.1:6379>?get?k1 "c"

如上,我們設置偏移量為 6 的位置數值為 1,這樣這個二進制對象就變成了: 0110 0011,對應的就是字符 ”c“ ,我們通過 直接操作字符串的位 改變了字符串的值。

Bitmaps 在redis中是按位操作字符串的工具,根據這個工具,我們可以將字符串當作一組二進制數組來使用,我們舉一個簡單的例子:

如何記錄十億用戶的在線狀態?

這里我們 用一串二進制來記錄這十億用戶的登錄狀態,二進制的每一位都代表一個用戶,0 代表未登錄,1 代表已登錄,每次登錄或登出都利用 Bitmaps 去更新對應位的數值,最終形成的結果看起來就是這樣的:

Redis學習之深入了解Bitmaps

我們用上面的一串二進制記錄了這十億用戶的登錄狀態,為什么要這么做? 主要就是節省空間,讀取或更新速度快

我們來算一下這種存儲方式所需要的存儲空間大小:

十億用戶,每一個用戶占?1?bit 所需空間?=?1000000000?bit?=?1000000000?/?8?/?1024?/?1024?=?119?MB

以 MySQL 為例,存儲需要的空間大小:

假設僅有兩個字段:用戶id,在線狀態 用戶id為BIGINT類型,大小為:8?Bytes	 在線狀態使用TINYINT類型,大小為:1?Bytes	 所需空間?=?1000000000?*?(8?+?1)?Bytes?=?9000000000?Bytes?=?8583?MB

差距顯而易見,這也很好理解,使用 MySQL 或者Redis 的 Hash 存儲,最小單元都是 字節,和直接操作 位 自然無法比較。

以上是對 Redis 的 Bitmaps 的簡單介紹,接下來會介紹一下關于 Bitmaps 的基礎命令以及應用。

二、Bitmaps的操作

SETBIT

時間復雜度: O(1)

SETBIT?key?offset?value

更新指定 key 的 offset 位置處的值,value 只可以是 0 或 1

需要注意:

1、offset 表示偏移量,最大為 2 32-1((因為最大是512MB,符號占1位)。

2、分配內存,一次分配之后,后續相同的key不會再有分配開銷,官網描述:在 2010 款 MacBook Pro 上,設置位數 2 32-1(512MB 分配)大約需要 300 毫秒。

3、返回值,返回對應 offset 操作之前的值。

GETBIT

時間復雜度: O(1)

GETBIT?key?offset

返回存儲在key的字符串值中offset處的位值。

需要注意:

? ? ? ? 當 key 不存在,或者 offset 超出范圍時,返回整數 0

BITCOUNT

時間復雜度: O(n)

BITCOUNT?key?[start?end?[BYTE|BIT]]

計算字符串中 1 的數量

示例: 127.0.0.1:6379>?set?k1?a OK 127.0.0.1:6379>?BITCOUNT?k1? (integer)?3 127.0.0.1:6379>?set?k1?aa OK 127.0.0.1:6379>?BITCOUNT?k1 (integer)?6 127.0.0.1:6379>?BITCOUNT?k1?0?0? (integer)?3 127.0.0.1:6379>?BITCOUNT?k1?0?1 (integer)?6 127.0.0.1:6379>?BITCOUNT?k1?0?-1 (integer)?6

需要注意:

1、start 和 end 參數指的是Byte,不是bit,官網介紹在7.0版本之后才可以指定 Byte或bit。

2、如果key 不存在,統計出來是0

3、時間復雜度是 O(n),這個n是指start 和 end 參數之間的數據量,所以數據量過大時善用start 和 end,或者多建幾個key。

BITOP

時間復雜度: O(n)

BITOP?operation?destkey?key?[key?...]

在多個鍵(包含字符串值)之間執行按位運算并將結果存儲在目標鍵中

其中 operation有 :ANDORXORNOT

destkey是指目標key,將后面的多個 key 進行按位操作后,儲存在 destkey 中

//?AND,按位與 127.0.0.1:6379>?set?k1?a OK 127.0.0.1:6379>?set?k2?aa OK 127.0.0.1:6379>?set?k3?aaa OK 127.0.0.1:6379>?bitop?and?dk1?k1?k2?k3? (integer)?3 127.0.0.1:6379>?get?dk1 "ax00x00"

如上面示例,將 k1 ,k2,k3,進行按位與之后結果儲存在 dk1 中,dk1 后面的 x00 是十六進制, ax00x00 轉換成二進制就是: 0110 0001 0000 0000 0000 0000。

//?OR,按位或 127.0.0.1:6379>?bitop?or?dk2?k1?k2?k3? (integer)?3 127.0.0.1:6379>?get?dk2 "aaa" --------------------- //XOR?,按位異或 127.0.0.1:6379>?bitop?xor?dk3?k1?k2?k3? (integer)?3 127.0.0.1:6379>?get?dk3 "ax00a" --------------------- //NOT,取反?0110?0001?取反?->??1001?1110??->?十六進制?x9e 127.0.0.1:6379>?bitop?not?dk4?k1 (integer)?1 127.0.0.1:6379>?get?dk4 "x9e"

BITPOS

時間復雜度: O(N)

BITPOS?key?bit?[start?[end?[BYTE|BIT]]]

返回字符串中設置為 1 或 0 的第一位的位置。

示例 127.0.0.1:6379>?setbit?k1?4?1 (integer)?0 127.0.0.1:6379>?setbit?k1?13??1 (integer)?0 127.0.0.1:6379>?bitpos?k1?1? (integer)?4 127.0.0.1:6379>?bitpos?k1?1?0?0 (integer)?4 127.0.0.1:6379>?bitpos?k1?1?1?1 (integer)?13

需要注意:

1、這里的 start 、end 參數指的是 Byte,在7.0版本后可以指定 Byte或bit。

2、bitpos 、 setbit 、 getbit 遵循相同的位位置約定。

3、查詢 1 時,不存在的 key 或者 對應范圍的字符串全是 0 ,返回 -1。

4、查詢 0 時,有三種特殊情況:

k2?=?1111?1111??,?k3?不存在 --------------------------- //?不指定范圍或僅指定?start,且值全是1,這時候會查出來最右側的1的位置?+?1,可以視為右側填充了0? 127.0.0.1:6379>?BITPOS?k2?0 (integer)?8 --------------------------- //?不指定范圍或僅指定?start,且key不存在,返回0 127.0.0.1:6379>?BITPOS?k3?0 (integer)?0 --------------------------- //?指定范圍,且范圍內沒有0,返回?-1 127.0.0.1:6379>?BITPOS?k2?1?1 (integer)?-1

BITFIFLD

BITFIELD?key?[GET?encoding?offset]?[SET?encoding?offset?value]?[INCRBY?encoding?offset?increment]?[OVERFLOW?WRAP|SAT|FAIL]

該命令將 Redis 字符串視為位數組,并且能夠處理不同位寬和任意非(必要)對齊偏移量的特定整數字段,該命令有get、set、incrby操作

就是說可以利用這個命令,按位分段的處理字符串,舉個例子:

127.0.0.1:6379>?set?k1?aaa OK
a a a
0110 0001 0110 0001 0110 0001

k1的二進制如上表格所示,接下來我們使用BITFIFLD命令來操作 k1

GET:

//?u8?=?無符號?+?8?位???;??0?=?從第0位開始 //?獲取到的結果就是?:?0110?0001?,無符號轉換成十進制就是?97 127.0.0.1:6379>?BITFIELD?k1?get?u8?0?? 1)?(integer)?97
//?i8?=?有符號?+?8?位???;?1?=?從第一位開始 //?結果?=?1100?0010?,帶符號轉換成十進制就是?-62?(不理解為啥是-62可以看一下補碼) 127.0.0.1:6379>?BITFIELD?k1?get?i8?1 1)?(integer)?-62

SET:

//?將0-7位,變成98,也就是:?0110?0010?,這對應的就是b,所以第一個字符變成了?b 127.0.0.1:6379>?BITFIELD?k1?set?u8?0?98 1)?(integer)?97 127.0.0.1:6379>?get?k1 "baa" ------------------------------------------ 127.0.0.1:6379>?BITFIELD?k1?set?u8?#1?98???//?#1的意思是?從第二個?8?位開始 1)?(integer)?97 127.0.0.1:6379>?get?k1 "bba"

INCRBY:遞增或者遞減

//?-1?表示遞增或遞減的數值,k1?的0-7位?減1,結果是97,k1就變成了?"aba" 127.0.0.1:6379>?get?k1 "bba" 127.0.0.1:6379>?BITFIELD?k1?incrby?u8?0?-1 1)?(integer)?97 127.0.0.1:6379>?get?k1 "aba" 127.0.0.1:6379>?BITFIELD?k1?incrby?u8?#1?-1 1)?(integer)?97 127.0.0.1:6379>?get?k1 "aaa"

在使用 INCRBY 進行遞增或遞減操作時,有 溢出控制 ,而且 Redis 提供了三種行為來控制溢出:

WRAP :環繞,在無符號整數的情況下,換行就像對整數可以包含的最大值進行模運算

//?以?u8?為例,無符號,8位,那么最大值是?256 127.0.0.1:6379>?BITFIELD?k1?get?u8?0? 1)?(integer)?97 127.0.0.1:6379>?BITFIELD?k1?overflow?WRAP?incrby?u8?0?256 1)?(integer)?97 127.0.0.1:6379>?BITFIELD?k1?overflow?WRAP?incrby?u8?0?257??//?97?+?257?=?97+257-256?=?98 1)?(integer)?98 127.0.0.1:6379>?BITFIELD?k1?overflow?WRAP?incrby?u8?0?200?//?98?+?200?=?298?-?256?=?42 1)?(integer)?42

在有符號的情況下,向上溢出到負值,向下溢出到正值,以 i8 為例 127 + 1 到 -128

127.0.0.1:6379>?set?k1?aaa OK 127.0.0.1:6379>?BITFIELD?k1?get?u8?0? 1)?(integer)?97 127.0.0.1:6379>?BITFIELD?k1?incrby?i8?0?30 1)?(integer)?127 127.0.0.1:6379>?BITFIELD?k1?incrby?i8?0?1? 1)?(integer)?-128 127.0.0.1:6379>?BITFIELD?k1?incrby?i8?0?-1 1)?(integer)?127

SAT: 使用飽和算法,即下溢時設置為最小整數值,溢出時設置為最大整數值。

//?u8時,最大255?最小?0 127.0.0.1:6379>?set?k1?aaa OK 127.0.0.1:6379>?BITFIELD?k1?get?u8?0? 1)?(integer)?97 127.0.0.1:6379>?BITFIELD?k1?overflow?SAT?incrby?u8?0?20000 1)?(integer)?255 127.0.0.1:6379>?BITFIELD?k1?overflow?SAT?incrby?u8?0?-213123 1)?(integer)?0

FAIL:在此模式下,不會對檢測到的上溢或下溢執行任何操作。相應的返回值設置為 NULL 以向調用者發出條件信號。就是說,溢出就返回 NUll。

127.0.0.1:6379>?set?k1?aaa OK 127.0.0.1:6379>?BITFIELD?k1?get?u8?0 1)?(integer)?97 127.0.0.1:6379>?BITFIELD?k1?overflow?FAIL?incrby?u8?0?200 1)?(nil) 127.0.0.1:6379>?BITFIELD?k1?overflow?FAIL?incrby?u8?0?-98 1)?(nil)

另外,以上的 BITFIELD 命令可以多個一起使用:

127.0.0.1:6379>?BITFIELD?k1?overflow?FAIL?incrby?u8?0?-98??get?u8?0? 1)?(nil) 2)?(integer)?97

BITFIELD_RO

Redis視頻教程命令的只讀變體。它就像原始的Redis視頻教程一樣,但只接受GET子命令并且可以安全地用于只讀副本。

Bitmaps 的應用

? ? ? ? 在上面的描述中,介紹了 Bitmaps 可以記錄用戶的在線狀態,除此之外還可以用他做那些功能呢?

首先我們來總結一下Bitmaps的特點:

  • 只有 0 和 1 兩種狀態(描述的信息比較局限)
  • 占用的內存非常少
  • 存取速度極快 ?(set,get操作時間復雜度都是O(1))

基于這些特點,我們可以用 Bitmaps 來判斷數據是否存在于某個集合中、也可以用于記錄用戶的一些行為比如登錄或者某個頁面的查看,關閉,簽到等等,接下來我們舉幾個比較常見的例子。

日活統計

如何統計當前系統每天登錄的用戶數量?

使用Redis的Bitmaps,將 系統名+日期設置為key 如 zcy20211215,value中使用用戶的Id做offset,用 0 和 1 記錄用戶在當天是否登錄過,登錄將對應的位設置為1。

這樣做之后,每天可以得到一個Bitmaps,如果想獲取某天登錄過的用戶數量,直接使用 BITCOUNT 操作即可。

如果想統計過去 30 天都登錄過的用戶,可以使用 BITOP AND 操作,將那 30 天的Bitmaps進行 按位與 操作。

布隆過濾器

布隆過濾器的本質是 Hash映射算法 + Bitmaps。

Redis學習之深入了解Bitmaps

如圖,一個 value 進入布隆過濾器,經過多個Hash算法,獲取其映射在Bitmap上的位置,當所有的位置都為1時,則認為這個 value 在過濾器中,否則就認為不存在。

營銷數據統計

Bitmaps 在營銷系統中可以用到的場景很多:

用戶畫像信息的使用,一個用戶有很多中標簽并且無限擴展,比如 性別,是否是程序員,單身,租房,是否養寵物,我們都可以考慮用Bitmap儲存和使用。

用戶的行為,是否點擊了廣告,是否瀏覽到底部,是否領取優惠券等行為分別用一個Bitmap存儲,用法和上面的用戶畫像類似。

這里舉一個小例子,看一下Bitmaps在營銷系統中的使用:

? ? ? ? 假設我們有一個一億用戶的電商應用,產品提了這樣一個需求:

所有的男性用戶在進入應用首頁時,彈出一個 防脫發保健品 的廣告彈窗

? ? ? ? 需求很簡單,一個接口搞定,用戶進入時調用 獲取廣告 的接口,接口中查詢數據庫判斷是否為男性,是返回廣告內容,否返回空。

? ? ? ? 這時候產品覺得這種推送不夠精準,也未必男性都會掉頭發,所以增加了條件: 程序員,我們的需求就變成了:

所有的 男性 且職業為 程序員 的用戶在進入應用首頁時,彈出一個 防脫發保健品 的廣告彈窗

? ? ? ? 加了一個條件之后依然很簡單,用戶的 性別 和 職業 信息極有可能存在一張表,也是一次查詢即可。

? ? ? ? 然而,男性程序員脫發的概率很高,但是未必都買得起防脫發保健品,這時候需要推送的更精準一點,所以再新增一個條件:在平臺上月均消費超過五百 ,這個條件和上述的 男性程序員 這兩個條件大概率不在同一個表中,如果用上面的方案,那就是再增加一次DB查詢,速度慢且對數據庫開銷大,這個時候 Bitmaps 的效果就很明顯了。

? ? ? ? 現有的條件是:男性程序員在平臺上月均消費超過五百

? ? ? ? 在這個場景中,如果要使用 Bitmaps,首先要把數據加載到Redis里,可以用一種簡單粗暴的方法,直接遍歷數據庫,把需要用的標簽信息加載到Redis中:

????//?用戶標簽加載偽代碼 ????public?Boolean?loadUserLabel()?{ ????		//?用戶性別?bitmap?數據加載 ????????String?key?=?"user_label_sex_male"; ????????List<user>?users?=?userDao.queryUser(); ????????users.forEach( ????????????????t-&gt;{ ????????????????????if?(Objects.equals(t.getSex(),"male"))?{ ????????????????????????jedis.setbit(key,t.getId(),"1"); ????????????????????} ????????????????} ????????); ????????return?true; ????}</user>

? ? ? ? 經過上述代碼,將用戶的性別加載到了 redis 的 bitmap中,其他的標簽如 職業、消費大于五百,與此類似。

? ? ? ? 在Redis中有了我們所需的用戶標簽信息后,就可以開始使用了,像我們上述的需求,可以用 BITOP 命令的 AND操作,將男性、程序員、月均消費大于五百這三個Bitmap 生成一個 同時滿足這三個條件的 Bitmap,標簽過多的時候可以這樣做。在這里我們就三個條件,可以簡單一點直接在代碼里查三次:

????//?用戶首頁廣告獲取偽代碼 ????public?Response?getHomepageAds(User?user)?{ ????????//?這里寫死,實際使用中是動態配置 ????????String?maleKey?=?"user_label_sex_male"; ????????String?programmerKey?=?"user_label_occupation_programmer"; ????????String?spendMonth500Key?=?"user_label_spend_month_500"; ????????//去bitmap判斷,該位為1,則滿足條件 ????????if?(jedis.getbit(maleKey,user.getId())?&amp;&amp;?jedis.getbit(programmerKey,user.getId())? ????????????????&amp;&amp;?jedis.getbit(spendMonth500Key,user.getId()))?{ ????????????return?"內容"; ????????} ????????return??"沒有廣告"; ????}

? ? ? ? 上面就是一個Bitmap在營銷系統中應用的小例子,可以在這基礎上進行很多擴展,比如每個標簽的實時的增量加載,每個廣告和標簽的綁定關系的動態配置,標簽的自動生成等等等等。。。。

? ? ? ? 我們接下來可以看一下 Bitmaps 在用戶行為記錄中的應用,現在產品提了一個新的需求:

我想知道有多少用戶點進了我們的彈窗廣告

? ? ? ? 點擊彈窗廣告之后,前端發送請求,后端記錄用戶的點擊狀態:

????//?用戶點擊廣告行為記錄偽代碼 ????public?Response?getHomepageAds(User?user)?{ ????????String?adActionKey?=?"homepage_ad_action_open"; ????????jedis.setbit(adActionKey,user.getId(),"1"); ???????? ????}

? ? ? ? 后面如果想統計數量,可以直接用 BITCOUNT 命令。其他行為的記錄和這個相似,如是否拖動到底部,停留時間是否大于n秒等等,這里就不做贅述啦。

協同制圖

? ? ? ? 這個例子來源于Redis官網,是 Reddit 在 2017 年愚人節的一個游戲 r/place,這是一個非常有趣的 Bitmaps 的應用。

游戲介紹:

? ? ? ? 一個畫板,上面有1000 * 1000 個像素點,每個玩家每五分鐘可以編輯一個像素點(有十六種顏色提供選擇),參與的玩家數量不限,72 小時后截止。

游戲很簡單,每個人只要編輯像素點的顏色即可,17 年的活動最終形成了這個畫(這里是一部分):

Redis學習之深入了解Bitmaps

這里面有一百萬個像素點,據統計至少十萬人參與了這個游戲,并發量很高,如何保證可用性呢?Reddit 在這里就使用了 Redis 的 Bitmap:

? ? ? ? 先定義畫板中的像素的位置,用 x 表示橫坐標,y 表示縱坐標,每一個像素點的位置都對應 Bitmap 的一個offset。

	用戶編輯像素點時,將?位置信息(x,y)?和?顏色信息?用?Bitmap?儲存,讀取畫板數據也是直接利用?Bitmap。

思路很簡單,主要是解決下面兩個問題:

1、坐標和Bitmap之間的映射關系? 坐標如何轉換成 Bitmap的 offset,offset如何轉換成畫板中的 x,y 坐標。

2、如何直接利用 Bitmaps 儲存一個坐標點的信息? 這里就存顏色。

這個項目是這么做的:

1、由于總計像素點是 100 萬個,所以設計了公式: ?x + 1000y = offset

? ? ? ? 儲存時,將 (x,y) 轉換成 offset ,比如 (1,2) 位置,那么 offset = 1 + 2000 = 2001

? ? ? ? 讀取時,將 offset 轉換成(x,y),比如 offset = 9008,那么 x = 9008 % 1000 = 8,y = 9008 / 1000 = 9

2、利用 Bitmaps 的 BITFIELD 命令,進行分段操作:

玩家可選擇的顏色共 16 種,那么每個點至少需要 4 位,所以使用 BITFIELD 時,操作的位數字段應該是 u4

看起來就是這樣的:

Redis學習之深入了解Bitmaps

上面是這個游戲對于 Bitmaps 應用的簡單介紹,具體實現及源碼見文末Reddit 團隊的博客。

利用 BITFIELD 命令,還可以判斷數據是否重復,比如有 10 億個整數,如何找出其中重復的數據? 每個數可以給 2 位,01表示出現一次,10表示有重復。

四、小擴展

當用戶 Id 不是自增 Id,該如何使用 Bitmaps?

? ? ? ? 在實際開發中,用戶的Id有可能不是自增的,比如使用雪花算法,或UUID工具生成的Id,這種情況直接使用 Bitmaps 是不行的,偏移量過大。這時候可以參考 布隆過濾器 ,設計一個Id的映射算法,將用戶Id映射在一定范圍內。

Bitmaps 進行持久化存儲時,如何節省空間?

? ? ? ? 使用壓縮算法,如 RLE算法

在使用Bitmaps時,會有大量連續的位置數據重復,這些重復就有壓縮的空間,比如前 1000 位都是 0,那只用存一句 1000個0即可,而不是 00000…這樣存一千個。

更多編程相關知識,請訪問:Redis視頻教程!!

? 版權聲明
THE END
喜歡就支持一下吧
點贊12 分享