濮阳杆衣贸易有限公司

主頁(yè) > 知識(shí)庫(kù) > Redis精確去重計(jì)數(shù)方法(咆哮位圖)

Redis精確去重計(jì)數(shù)方法(咆哮位圖)

熱門標(biāo)簽:臺(tái)灣電銷 高碑店市地圖標(biāo)注app 400電話辦理的口碑 b2b外呼系統(tǒng) 南京手機(jī)外呼系統(tǒng)廠家 地圖標(biāo)注工廠入駐 廊坊外呼系統(tǒng)在哪買 一個(gè)地圖標(biāo)注多少錢 四川穩(wěn)定外呼系統(tǒng)軟件

前言

如果要統(tǒng)計(jì)一篇文章的閱讀量,可以直接使用 Redis 的 incr 指令來(lái)完成。如果要求閱讀量必須按用戶去重,那就可以使用 set 來(lái)記錄閱讀了這篇文章的所有用戶 id,獲取 set 集合的長(zhǎng)度就是去重閱讀量。但是如果爆款文章閱讀量太大,set 會(huì)浪費(fèi)太多存儲(chǔ)空間。這時(shí)候我們就要使用 Redis 提供的 HyperLogLog 數(shù)據(jù)結(jié)構(gòu)來(lái)代替 set,它只會(huì)占用最多 12k 的存儲(chǔ)空間就可以完成海量的去重統(tǒng)計(jì)。但是它犧牲了準(zhǔn)確度,它是模糊計(jì)數(shù),誤差率約為 0.81%。

那么有沒(méi)有一種不怎么浪費(fèi)空間的精確計(jì)數(shù)方法呢?我們首先想到的就是位圖,可以使用位圖的一個(gè)位來(lái)表示一個(gè)用戶id。如果一個(gè)用戶id是32字節(jié),那么使用位圖就只需要占用 1/256 的空間就可以完成精確計(jì)數(shù)。但是如何將用戶id映射到位圖的位置呢?如果用戶id是連續(xù)的整數(shù)這很好辦,但是通常用戶系統(tǒng)的用戶id并不是整數(shù),而是字符串或者是有一定隨機(jī)性的大整數(shù)。

我們可以強(qiáng)行給每個(gè)用戶id賦予一個(gè)整數(shù)序列,然后將用戶id和整數(shù)的對(duì)應(yīng)關(guān)系存在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

這里你也許會(huì)提出疑問(wèn),你說(shuō)是為了節(jié)省空間,這里存儲(chǔ)用戶id和整數(shù)的映射關(guān)系就不浪費(fèi)空間了么?這個(gè)問(wèn)題提的很好,但是同時(shí)我們也要看到這個(gè)映射關(guān)系是可以復(fù)用的,它可以統(tǒng)計(jì)所有文章的閱讀量,還可以統(tǒng)計(jì)簽到用戶的日活、月活,還可以用在很多其它的需要用戶去重的統(tǒng)計(jì)場(chǎng)合中。所謂「功在當(dāng)代,利在千秋」就是這個(gè)意思。

有了這個(gè)映射關(guān)系,我們就很容易構(gòu)造出每一篇文章的閱讀打點(diǎn)位圖,來(lái)一個(gè)用戶,就將相應(yīng)位圖中相應(yīng)的位置為一。如果位從0變成1,那么就可以給閱讀數(shù)加1。這樣就可以很方便的獲得文章的閱讀數(shù)。

而且我們還可以動(dòng)態(tài)計(jì)算閱讀了兩篇文章的公共用戶量有多少?將兩個(gè)位圖做一下 AND 計(jì)算,然后統(tǒng)計(jì)位圖中位 1 的個(gè)數(shù)。同樣,還可以有 OR 計(jì)算、XOR 計(jì)算等等都是可行的。

問(wèn)題又來(lái)了!Redis 的位圖是密集位圖,什么意思呢?如果有一個(gè)很大的位圖,它只有最后一個(gè)位是 1,其它都是零,這個(gè)位圖還是會(huì)占用全部的內(nèi)存空間,這就不是一般的浪費(fèi)了。你可以想象大部分文章的閱讀量都不大,但是它們的占用空間卻是很接近的,和哪些爆款文章占據(jù)的內(nèi)存差不多。

看來(lái)這個(gè)方案行不通,我們需要想想其它方案!這時(shí)咆哮位圖(RoaringBitmap)來(lái)了。

它將整個(gè)大位圖進(jìn)行了分塊,如果整個(gè)塊都是零,那么這整個(gè)塊就不用存了。但是如果位1比較分散,每個(gè)塊里面都有1,雖然單個(gè)塊里的1很少,這樣只進(jìn)行分塊還是不夠的,那該怎么辦呢?我們?cè)傧胂耄瑢?duì)于單個(gè)塊,是不是可以繼續(xù)優(yōu)化?如果單個(gè)塊內(nèi)部位 1 個(gè)數(shù)量很少,我們可以只存儲(chǔ)所有位1的塊內(nèi)偏移量(整數(shù)),也就是存一個(gè)整數(shù)列表,那么塊內(nèi)的存儲(chǔ)也可以降下來(lái)。這就是單個(gè)塊位圖的稀疏存儲(chǔ)形式 —— 存儲(chǔ)偏移量整數(shù)列表。只有單塊內(nèi)的位1超過(guò)了一個(gè)閾值,才會(huì)一次性將稀疏存儲(chǔ)轉(zhuǎn)換為密集存儲(chǔ)。

咆哮位圖除了可以大幅節(jié)約空間之外,還會(huì)降低 AND、OR 等位運(yùn)算的計(jì)算效率。以前需要計(jì)算整個(gè)位圖,現(xiàn)在只需要計(jì)算部分塊。如果塊內(nèi)非常稀疏,那么只需要對(duì)這些小整數(shù)列表進(jìn)行集合的 AND、OR 運(yùn)算,如是計(jì)算量還能繼續(xù)減輕。

這里既不是用空間換時(shí)間,也沒(méi)有用時(shí)間換空間,而是用邏輯的復(fù)雜度同時(shí)換取了空間和時(shí)間。

咆哮位圖的位長(zhǎng)最大為 2^32,對(duì)應(yīng)的空間為 512M(普通位圖),位偏移被分割成高 16 位和低 16 位,高 16 位表示塊偏移,低16位表示塊內(nèi)位置,單個(gè)塊可以表達(dá) 64k 的位長(zhǎng),也就是 8K 字節(jié)。最多會(huì)有64k個(gè)塊?,F(xiàn)代處理器的 L1 緩存普遍要大于 8K,這樣可以保證單個(gè)塊都可以全部放入 L1 Cache,可以顯著提升性能。

如果單個(gè)塊所有的位全是零,那么它就不需要存儲(chǔ)。具體某個(gè)塊是否存在也可以是用位圖來(lái)表達(dá),當(dāng)塊很少時(shí),用整數(shù)列表表示,當(dāng)塊多了就可以轉(zhuǎn)換成普通位圖。整數(shù)列表占用的空間少,它還有類似于 ArrayList 的動(dòng)態(tài)擴(kuò)容機(jī)制避免反復(fù)擴(kuò)容復(fù)制數(shù)組內(nèi)容。當(dāng)列表中的數(shù)字超出4096個(gè)時(shí),會(huì)立即轉(zhuǎn)變成普通位圖。

用來(lái)表達(dá)塊是否存在的數(shù)據(jù)結(jié)構(gòu)和表達(dá)單個(gè)塊數(shù)據(jù)的結(jié)構(gòu)可以是同一個(gè),因?yàn)閴K是否存在本質(zhì)上也是 0 和 1,就是普通的位標(biāo)志。

但是 Redis 并沒(méi)有原生支持咆哮位圖這個(gè)數(shù)據(jù)結(jié)構(gòu)???我們?cè)撊绾问褂媚兀?br />

Redis 確實(shí)沒(méi)有原生的,但是咆哮位圖的 Redis Module 有。

github.com/aviggiano/r…

這個(gè)項(xiàng)目的 star 數(shù)量并不是很多,我們來(lái)看看它的官方性能對(duì)比

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

很明顯這里對(duì)比的是稀疏位圖,只有稀疏位圖才可以呈現(xiàn)出這樣好看的數(shù)字。如果是密集位圖,咆哮位圖的性能肯定要稍弱于普通位圖,但是通常也不會(huì)弱太多。

下面我們來(lái)觀察一下源代碼看看它的內(nèi)部結(jié)構(gòu)是怎樣的

// 單個(gè)塊
typedef struct roaring_array_s {
 int32_t size;
 int32_t allocation_size;
 void **containers; // 指向整數(shù)數(shù)組或者普通位圖
 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;

很明顯可以看到塊存在與否和塊內(nèi)數(shù)據(jù)都是使用同樣的數(shù)據(jù)結(jié)構(gòu)表達(dá)的,它們都是 roaring_bitmap_t。這個(gè)結(jié)構(gòu)里面有多種編碼形式,類型使用 typecodes 字段來(lái)表示。

#define BITSET_CONTAINER_TYPE_CODE 1
#define ARRAY_CONTAINER_TYPE_CODE 2
#define RUN_CONTAINER_TYPE_CODE 3
#define SHARED_CONTAINER_TYPE_CODE 4

看到這里的類型定義,我們發(fā)現(xiàn)它不止前面提到的普通位圖和數(shù)組列表兩種形式,還有 RUN 和 SHARED 這兩種類型。RUN 形式是位圖的壓縮形式,比如連續(xù)的幾個(gè)位 101,102,103,104,105,106,107,108,109 表示成 RUN 后就是 101,8(1 后面是 8 個(gè)自增的整數(shù)),這樣在空間上就可以明顯壓縮不少。在正常情況下咆哮位圖內(nèi)部沒(méi)有 RUN 類型的塊。只有顯示調(diào)用了咆哮位圖的優(yōu)化 API 才會(huì)轉(zhuǎn)換成 RUN 格式,這個(gè) API 是 roaring_bitmap_run_optimize。

而 SHARED 類型用于在多個(gè)咆哮位圖之間共享塊,它還提供了寫復(fù)制功能。當(dāng)這個(gè)塊被修改時(shí)將會(huì)復(fù)制出新的一份。
咆哮位圖的計(jì)算邏輯還有更多的細(xì)節(jié),我們后面有空再繼續(xù)介紹。

總結(jié)

以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。

您可能感興趣的文章:
  • 基于Redis位圖實(shí)現(xiàn)系統(tǒng)用戶登錄統(tǒng)計(jì)
  • PHP使用redis位圖bitMap 實(shí)現(xiàn)簽到功能
  • redis通過(guò)位圖法記錄在線用戶的狀態(tài)詳解
  • java redis 實(shí)現(xiàn)簡(jiǎn)單的用戶簽到功能
  • 基于Redis位圖實(shí)現(xiàn)用戶簽到功能

標(biāo)簽:泰州 甘南 畢節(jié) 定州 伊春 南寧 拉薩 河源

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Redis精確去重計(jì)數(shù)方法(咆哮位圖)》,本文關(guān)鍵詞  Redis,精確,去重,計(jì)數(shù),方法,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《Redis精確去重計(jì)數(shù)方法(咆哮位圖)》相關(guān)的同類信息!
  • 本頁(yè)收集關(guān)于Redis精確去重計(jì)數(shù)方法(咆哮位圖)的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章

    上一篇:Redis字符串原理的深入理解

    下一篇:Redis實(shí)戰(zhàn)記錄之限制操作頻率

    崇明县| 尉犁县| 迁安市| 启东市| 抚顺市| 河津市| 红河县| 保靖县| 宁阳县| 曲阜市| 虹口区| 舟曲县| 武夷山市| 扎赉特旗| 耒阳市| 新绛县| 蓬莱市| 奎屯市| 浙江省| 兴业县| 邛崃市| 庄浪县| 门头沟区| 杭州市| 榆树市| 旌德县| 无锡市| 鹿泉市| 合阳县| 贺兰县| 白玉县| 溆浦县| 寿阳县| 紫阳县| 济阳县| 藁城市| 湛江市| 兴城市| 石城县| 屯昌县| 中西区|