一看就懂系列之 詳解redis的bitmap在億級項目中的應用

前言

這是一篇拖了很久的總結,項目中引入了redis的bitmap的用法,感覺挺高大上的,刨根問底,故留下總結一篇當作紀念。
說清楚幾個問題:
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。

用法

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爲2^32-1(分配512MB)需要~300ms,offset爲2^30-1(分配128MB)需要~80ms,offset爲2^28-1(分配32MB)需要~30ms,offset爲2^26-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。存儲成本是不是很低。

使用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/)

擴展閱讀

Bitmap的祕密
Redis內存壓縮實戰
Redis中bitmap的妙用
Redis中BitMap是如何儲存的,以及PHP如何處理
使用redis的setbit和bitcount來進行區間統計的坑

發佈了93 篇原創文章 · 獲贊 152 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章