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。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM