說清楚幾個問題:
1.bitmap的原理、用法。
2.bitmap的優勢、限制。
3.bitmap空間、時間粗略計算方式。
4.bitmap的使用場景。
5.使用bitmap過程中可能會遇到的坑。
6.bitmap進階用法(思考)。
一、bitmap的原理、用法
8bit = 1b = 0.001kb
bitmap就是通過最小的單位bit來進行0或者1的設置,表示某個元素對應的值或者狀態。
一個bit的值,或者是0,或者是1;也就是說一個bit能存儲的最多信息是2。
Redis提供了以下幾個指令用於操作BitMap:
命令 說明 可用版本 時間復雜度
SETBIT 對 key 所儲存的字符串值,設置或清除指定偏移量上的位(bit)。 >= 2.2.0 O(1)
GETBIT 對 key 所儲存的字符串值,獲取指定偏移量上的位(bit)。 >= 2.2.0 O(1)
BITCOUNT 計算給定字符串中,被設置為 1 的比特位的數量。 >= 2.6.0 O(N)
BITPOS 返回位圖中第一個值為 bit 的二進制位的位置。 >= 2.8.7 O(N)
BITOP 對一個或多個保存二進制位的字符串 key 進行位元操作。 >= 2.6.0 O(N)
BITFIELD BITFIELD 命令可以在一次調用中同時對多個位范圍進行操作。 >= 3.2.0 O(1)
常用的幾個方法:
setBit
說明:給一個指定key的值得第offset位 賦值為value。
參數:key offset value: bool or int (1 or 0)
返回值:LONG: 0 or 1
getBit
說明:返回一個指定key的二進制信息
參數:key offset
返回值:LONG
bitCount
說明:返回一個指定key中位的值為1的個數(是以byte為單位不是bit)
參數:key start offset
返回值:LONG
bitOp
說明:對不同的二進制存儲數據進行位運算(AND、OR、NOT、XOR)
參數:operation destkey key [key …]
返回值:LONG
二、bitmap的優勢、限制
優勢
1.基於最小的單位bit進行存儲,所以非常省空間。
2.設置時候時間復雜度O(1)、讀取時候時間復雜度O(n),操作是非常快的。
3.二進制數據的存儲,進行相關計算的時候非常快。
4.方便擴容
限制
redis中bit映射被限制在512MB之內,所以最大是2^32位。建議每個key的位數都控制下,因為讀取時候時間復雜度O(n),越大的串讀的時間花銷越多。
bitmap空間、時間粗略計算方式
在一台2010MacBook Pro上,offset為232-1(分配512MB)需要~300ms,offset為230-1(分配128MB)需要~80ms,offset為228-1(分配32MB)需要~30ms,offset為226-1(分配8MB)需要8ms。<來自官方文檔>
大概的空間占用計算公式是:($offset/8/1024/1024)MB
三、bitmap的使用場景
使用方式很多,根據不同的業務需求來,但是總的來說就兩種,以用戶為例子:
1.一種是某一用戶的橫向擴展,即此個key值中記錄這當前用戶的各種狀態值,允許無限擴展(2^32內)
點評:這種用法基本上是很少用的,因為每個key攜帶uid信息,如果存儲的key的空間大於value,從空間角度看有一定的優化空間,如果是記錄長尾的則可以考慮。
2.一種是某一用戶的縱向擴展,即每個key只記錄當前業務屬性的狀態,每個uid當作bit位來記錄信息(用戶超過2^32內需要分片存儲)
點評:基本上項目使用的場景都是基於這種方式的,按業務區分方便回收資源,key值就一個,將uid的存儲轉為了位的存儲,十分巧妙的通過uid即可找到相應的值,主要存儲量在value上,符合預期。
案例說明:
1.視頻屬性的無限延伸
需求分析:
一個擁有億級數據量的短視頻app,視頻存在各種屬性(是否加鎖、是否特效等等),需要做各種標記。
可能想到的解決方案:
1.存儲在mysql中,肯定不行,一個是隨着業務增長屬性一直增加,並且存在有時間限制的屬性,直接對數據庫進行加減字段是非常不合理的做法。即使是存在一個字段中用json等壓縮技術存儲也存在讀效率的問題,並且對於大幾億的數據來說,廢棄的字段回收起來非常麻煩。
2.直接記錄在redis中,根據業務屬性+uid為key來存儲。讀寫效率角度沒毛病,但是存儲的角度來說key的數據量都大於value了,太耗費空間了。即使是用json等壓縮技術來存儲。也存在問題,解壓需要時間,並且大幾億的數據回收也是難題。
設計方案:
使用redis的bitmap進行存儲
key由屬性id+視頻分片id組成。value按照視頻id對分片范圍取模來決定偏移量offset。10億視頻一個屬性約120m還是挺划算的。
偽代碼:
function set($business_id , $media_id , $switch_status=1){ $switch_status = $switch_status ? 1 : 0; $key = $this->_getKey($business_id, $media_id); $offset = $this->_getOffset($media_id); return $this->redis->setBit($key, $offse, $switch_status); } function get($business_id , $media_id){ $key = $this->_getKey($business_id,$media_id); $offset = $this->_getOffset($media_id); return $this->redis->getBit($key , $offset); } function _getKey($business_id, $media_id){ return 'm:'.$business_id.':'.intval($media_id/10000); } function _getOffset($media_id){ return $media_id % 10000; }
這樣基本實現了屬性的存儲,后續增加新屬性也只是business_id再增加一個值。
至於為什么分片呢?分片的粒度怎么衡量?
分片有兩個原因:1.讀取的時候時間復雜度是O(n)存儲越長讀取時間越多 2.bitmap有長度限制2^32。
分片粒度怎么衡量:1.如果主鍵id存在的斷層那么請盡可能選擇的粒度可以避開此段id范圍,防止空間浪費,因為來一個00000…9999個0…01,那么因為存一個屬性而存了全部的,就浪費了。2.分片粒度可參考某一單位時間的增長值來判斷,這樣也有利於預算占了多少空間,雖然空間不會占很多。
2.用戶在線狀態
需求分析:
需要對子項目提供一個接口,來提供某用戶是否在線?
設計方案:
使用bitmap是一個節約空間效率又高的一種方法,只需要一個key,然后用戶id為偏移量offset,如果在線就設置為1,不在線就設置為0,3億用戶只需要36MB的空間。
偽代碼:
$status = 1; $redis->setBit('online', $uid, $status); $redis->getBit('online', $uid);
需要加上如例子1一樣分片的方式。10億真的太多了。10w分一片。
3.統計活躍用戶
需求分析:
需要計算活躍用戶的數據情況。
設計方案:
使用時間作為緩存的key,然后用戶id為offset,如果當日活躍過就設置為1。之后通過bitOp進行二進制計算算出在某段時間內用戶的活躍情況。
偽代碼:
$status = 1; $redis->setBit('active_20170708', $uid, $status); $redis->setBit('active_20170709', $uid, $status); $redis->bitOp('AND', 'active', 'active_20170708', 'active_20170709');
上億用戶需要加上如例子1一樣分片的方式。幾十萬或者以下,可無需分片省的業務變復雜。
4.用戶簽到
需求分析:
用戶需要進行簽到,對於簽到的數據需要進行分析與相應的運運營策略。
設計方案:
使用redis的bitmap,由於是長尾的記錄,所以key主要由uid組成,設定一個初始時間,往后沒加一天即對應value中的offset的位置。
偽代碼:
$start_date = '20170708'; $end_date = '20170709'; $offset = floor((strtotime($start_date) - strtotime($end_date)) / 86400); $redis->setBit('sign_123456', $offset, 1); //算活躍天數 $redis->bitCount('sign_123456', 0, -1)
無需分片,一年365天,3億用戶約占300000000*365/8/1000/1000/1000=13.68g。存儲成本是不是很低。
再例如:考慮到每月要重置連續簽到次數,最簡單的方式是按用戶每月存一條簽到數據。Key的格式為 u:sign:{uid}:{yyyMM},而Value則采用長度為4個字節的(32位)的BitMap(最大月份只有31天)。BitMap的每一位代表一天的簽到,1表示已簽,0表示未簽。
例如 u:sign:1225:202101 表示ID=1225的用戶在2021年1月的簽到記錄
# 用戶1月6號簽到 SETBIT u:sign:1225:202101 5 1 # 偏移量是從0開始,所以要把6減1 # 檢查1月6號是否簽到 GETBIT u:sign:1225:202101 5 # 偏移量是從0開始,所以要把6減1 # 統計1月份的簽到次數 BITCOUNT u:sign:1225:202101 # 獲取1月份前31天的簽到數據 BITFIELD u:sign:1225:202101 get u31 0 # 獲取1月份首次簽到的日期 BITPOS u:sign:1225:202101 1 # 返回的首次簽到的偏移量,加上1即為當月的某一天
上面都需要增加過期時間,redisTemplate.expireAt(key, ttl)
使用bitmap過程中可能會遇到的坑
1.bitcout的陷阱
如果你有仔細看前文的用法,會發現有這么一個備注“返回一個指定key中位的值為1的個數(是以byte為單位不是bit)”,這就是坑的所在。
有圖有真相:
所以bitcount 0 0 那么就應該是第一個字節中1的數量的,注意是字節,第一個字節也就是1,2,3,4,5,6,7,8這八個位置上。
bitmap進階用法(思考)
以下內容來自此文的筆記:http://www.infoq.com/cn/articles/the-secret-of-bitmap/
1.空間
redis的bitmap已經是最小單位的存儲了,有沒有辦法對二進制存儲的信息再進行壓縮呢?進一步省空間?
答案是有的。
可以對記錄的二進制數據進行壓縮。常見的二進制壓縮技術都是基於RLE(Run Length Encoding,詳見http://en.wikipedia.org/wiki/Run-length_encoding)。
RLE編碼很簡單,比較適合有很多連續字符的數據,比如以下邊的Bitmap為例:
可以編碼為0,8,2,11,1,2,3,11
其意思是:第一位為0,連續有8個,接下來是2個1,11個0,1個1,2個0,3個1,最后是11個0(當然此處只是對RLE的基本原理解釋,實際應用中的編碼並不完全是這樣的)。
可以預見,對於一個很大的Bitmap,如果里邊的數據分布很稀疏(說明有很多大片連續的0),采用RLE編碼后,占用的空間會比原始的Bitmap小很多。
2.時間
redis雖然是在內存操作,但是超過redis指定存儲在內存的閥值之后,會被搞到磁盤中。要是進行大范圍的計算還需要從磁盤中取出到內存在計算比較耗時,效率也不高,有沒有辦法盡可能內存中多放一些數據,縮短時間?
答案是有的。
基於第一點同時引入一些對齊的技術,可以讓采用RLE編碼的Bitmap不需要進行解壓縮,就可以直接進行AND/OR/XOR等各類計算;因此采用這類壓縮技術的Bitmap,加載到內存后還是以壓縮的方式存在,從而可以保證計算時候的低內存消耗;而采用word(計算機的字長,64位系統就是64bit)對齊等技術又保證了對CPU資源的高效利用。因此采用這類壓縮技術的Bitmap,保持了Bitmap數據結構最重要的一個特性,就是高效的針對每個bit的邏輯運算。
常見的壓縮技術包括BBC(有專利保護,WAH(http://code.google.com/p/compressedbitset/)和EWAH(http://code.google.com/p/javaewah/)
原文鏈接:https://blog.csdn.net/u011957758/article/details/74783347
https://blog.csdn.net/Ainanaya/article/details/115324205
