您好,登錄后才能下訂單哦!
前言
如果要統計一篇文章的閱讀量,可以直接使用 Redis 的 incr 指令來完成。如果要求閱讀量必須按用戶去重,那就可以使用 set 來記錄閱讀了這篇文章的所有用戶 id,獲取 set 集合的長度就是去重閱讀量。但是如果爆款文章閱讀量太大,set 會浪費太多存儲空間。這時候我們就要使用 Redis 提供的 HyperLogLog 數據結構來代替 set,它只會占用最多 12k 的存儲空間就可以完成海量的去重統計。但是它犧牲了準確度,它是模糊計數,誤差率約為 0.81%。
那么有沒有一種不怎么浪費空間的精確計數方法呢?我們首先想到的就是位圖,可以使用位圖的一個位來表示一個用戶id。如果一個用戶id是32字節,那么使用位圖就只需要占用 1/256 的空間就可以完成精確計數。但是如何將用戶id映射到位圖的位置呢?如果用戶id是連續的整數這很好辦,但是通常用戶系統的用戶id并不是整數,而是字符串或者是有一定隨機性的大整數。
我們可以強行給每個用戶id賦予一個整數序列,然后將用戶id和整數的對應關系存在redis中。
$next_user_id = incr user_id_seq set user_id_xxx $next_user_id $next_user_id = incr user_id_seq set user_id_yyy $next_user_id $next_user_id = incr user_id_seq set user_id_zzz $next_user_id
這里你也許會提出疑問,你說是為了節省空間,這里存儲用戶id和整數的映射關系就不浪費空間了么?這個問題提的很好,但是同時我們也要看到這個映射關系是可以復用的,它可以統計所有文章的閱讀量,還可以統計簽到用戶的日活、月活,還可以用在很多其它的需要用戶去重的統計場合中。所謂「功在當代,利在千秋」就是這個意思。
有了這個映射關系,我們就很容易構造出每一篇文章的閱讀打點位圖,來一個用戶,就將相應位圖中相應的位置為一。如果位從0變成1,那么就可以給閱讀數加1。這樣就可以很方便的獲得文章的閱讀數。
而且我們還可以動態計算閱讀了兩篇文章的公共用戶量有多少?將兩個位圖做一下 AND 計算,然后統計位圖中位 1 的個數。同樣,還可以有 OR 計算、XOR 計算等等都是可行的。
問題又來了!Redis 的位圖是密集位圖,什么意思呢?如果有一個很大的位圖,它只有最后一個位是 1,其它都是零,這個位圖還是會占用全部的內存空間,這就不是一般的浪費了。你可以想象大部分文章的閱讀量都不大,但是它們的占用空間卻是很接近的,和哪些爆款文章占據的內存差不多。
看來這個方案行不通,我們需要想想其它方案!這時咆哮位圖(RoaringBitmap)來了。
它將整個大位圖進行了分塊,如果整個塊都是零,那么這整個塊就不用存了。但是如果位1比較分散,每個塊里面都有1,雖然單個塊里的1很少,這樣只進行分塊還是不夠的,那該怎么辦呢?我們再想想,對于單個塊,是不是可以繼續優化?如果單個塊內部位 1 個數量很少,我們可以只存儲所有位1的塊內偏移量(整數),也就是存一個整數列表,那么塊內的存儲也可以降下來。這就是單個塊位圖的稀疏存儲形式 —— 存儲偏移量整數列表。只有單塊內的位1超過了一個閾值,才會一次性將稀疏存儲轉換為密集存儲。
咆哮位圖除了可以大幅節約空間之外,還會降低 AND、OR 等位運算的計算效率。以前需要計算整個位圖,現在只需要計算部分塊。如果塊內非常稀疏,那么只需要對這些小整數列表進行集合的 AND、OR 運算,如是計算量還能繼續減輕。
這里既不是用空間換時間,也沒有用時間換空間,而是用邏輯的復雜度同時換取了空間和時間。
咆哮位圖的位長最大為 2^32,對應的空間為 512M(普通位圖),位偏移被分割成高 16 位和低 16 位,高 16 位表示塊偏移,低16位表示塊內位置,單個塊可以表達 64k 的位長,也就是 8K 字節。最多會有64k個塊。現代處理器的 L1 緩存普遍要大于 8K,這樣可以保證單個塊都可以全部放入 L1 Cache,可以顯著提升性能。
如果單個塊所有的位全是零,那么它就不需要存儲。具體某個塊是否存在也可以是用位圖來表達,當塊很少時,用整數列表表示,當塊多了就可以轉換成普通位圖。整數列表占用的空間少,它還有類似于 ArrayList 的動態擴容機制避免反復擴容復制數組內容。當列表中的數字超出4096個時,會立即轉變成普通位圖。
用來表達塊是否存在的數據結構和表達單個塊數據的結構可以是同一個,因為塊是否存在本質上也是 0 和 1,就是普通的位標志。
但是 Redis 并沒有原生支持咆哮位圖這個數據結構啊?我們該如何使用呢?
Redis 確實沒有原生的,但是咆哮位圖的 Redis Module 有。
github.com/aviggiano/r…
這個項目的 star 數量并不是很多,我們來看看它的官方性能對比
OP | TIME/OP (us) | ST.DEV. (us) |
---|---|---|
R.SETBIT | 31.89 | 28.49 |
SETBIT | 29.98 | 29.23 |
R.GETBIT | 29.90 | 14.60 |
GETBIT | 28.63 | 14.58 |
R.BITCOUNT | 32.13 | 0.10 |
BITCOUNT | 192.38 | 0.96 |
R.BITPOS | 70.27 | 0.14 |
BITPOS | 87.70 | 0.62 |
R.BITOP NOT | 156.66 | 3.15 |
BITOP NOT | 364.46 | 5.62 |
R.BITOP AND | 81.56 | 0.48 |
BITOP AND | 492.97 | 8.32 |
R.BITOP OR | 107.03 | 2.44 |
BITOP OR | 461.68 | 8.42 |
R.BITOP XOR | 69.07 | 2.82 |
BITOP XOR | 440.75 | 7.90 |
很明顯這里對比的是稀疏位圖,只有稀疏位圖才可以呈現出這樣好看的數字。如果是密集位圖,咆哮位圖的性能肯定要稍弱于普通位圖,但是通常也不會弱太多。
下面我們來觀察一下源代碼看看它的內部結構是怎樣的
// 單個塊 typedef struct roaring_array_s { int32_t size; int32_t allocation_size; void **containers; // 指向整數數組或者普通位圖 uint16_t *keys; uint8_t *typecodes; uint8_t flags; } roaring_array_t; // 所有塊 typedef struct roaring_bitmap_s { roaring_array_t high_low_container; } roaring_bitmap_t;
很明顯可以看到塊存在與否和塊內數據都是使用同樣的數據結構表達的,它們都是 roaring_bitmap_t。這個結構里面有多種編碼形式,類型使用 typecodes 字段來表示。
#define BITSET_CONTAINER_TYPE_CODE 1 #define ARRAY_CONTAINER_TYPE_CODE 2 #define RUN_CONTAINER_TYPE_CODE 3 #define SHARED_CONTAINER_TYPE_CODE 4
看到這里的類型定義,我們發現它不止前面提到的普通位圖和數組列表兩種形式,還有 RUN 和 SHARED 這兩種類型。RUN 形式是位圖的壓縮形式,比如連續的幾個位 101,102,103,104,105,106,107,108,109 表示成 RUN 后就是 101,8(1 后面是 8 個自增的整數),這樣在空間上就可以明顯壓縮不少。在正常情況下咆哮位圖內部沒有 RUN 類型的塊。只有顯示調用了咆哮位圖的優化 API 才會轉換成 RUN 格式,這個 API 是 roaring_bitmap_run_optimize。
而 SHARED 類型用于在多個咆哮位圖之間共享塊,它還提供了寫復制功能。當這個塊被修改時將會復制出新的一份。
咆哮位圖的計算邏輯還有更多的細節,我們后面有空再繼續介紹。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。