學透 Redis HyperLogLog,看這篇就夠了
來源:碼哥位元組
在移動網際網路的業務場景中,資料量很大,系統需要儲存這樣的資訊:一個 key 關聯了一個資料集合,同時對這個資料集合做統計做一個報表給運營人員看。
比如。
統計一個 APP
的日活、月活數。統計一個頁面的每天被多少個不同賬戶訪問量(Unique Visitor,UV)。 統計使用者每天搜尋不同詞條的個數。 統計註冊 IP 數。
通常情況下,系統面臨的使用者數量以及訪問量都是巨大的,比如百萬、千萬級別的使用者數量,或者千萬級別、甚至億級別的訪問資訊,咋辦呢?
❝Redis:“這些就是典型的基數統計應用場景,基數統計:統計一個集合中不重複元素,這被稱為基數。”
1. 是什麼
HyperLogLog 是一種機率資料結構,用於估計集合的基數。每個 HyperLogLog
最多隻需要花費 12KB 記憶體,在標準誤差 0.81%
的前提下,就可以計算 2 的 64 次方個元素的基數。
HyperLogLog
的優點在於它所需的記憶體並不會因為集合的大小而改變,無論集合包含的元素有多少個,HyperLogLog 進行計算所需的記憶體總是固定的,並且是非常少的。
主要特點如下。
高效的記憶體使用:HyperLogLog 的記憶體消耗是固定的,與集合中的元素數量無關。這使得它特別適用於處理大規模資料集,因為它不需要儲存每個不同的元素,只需要儲存估計基數所需的資訊。 機率估計:HyperLogLog 提供的結果是機率性的,而不是精確的基數計數。它透過雜湊函式將輸入元素對映到點陣圖中的某些位置,並基於點陣圖的統計資訊來估計基數。由於這是一種機率性方法,因此可能存在一定的誤差,但通常在實際應用中,這個誤差是可接受的。 高速計算:HyperLogLog 可以在常量時間內計算估計的基數,無論集合的大小如何。這意味著它的效能非常好,不會受到集合大小的影響。
2. 修煉心法
基本原理
HyperLogLog 是一種機率資料結構,它使用機率演算法來統計集合的近似基數。而它演算法的最本源則是伯努利過程。
伯努利過程就是一個拋硬幣實驗的過程。拋一枚正常硬幣,落地可能是正面,也可能是反面,二者的機率都是 1/2
。
伯努利過程就是一直拋硬幣,直到落地時出現正面位置,並記錄下拋擲次數k
。
比如說,拋一次硬幣就出現正面了,此時 k
為 1
; 第一次拋硬幣是反面,則繼續拋,直到第三次才出現正面,此時 k
為 3。
對於 n
次伯努利過程,我們會得到 n 個出現正面的投擲次數值 k1, k2 ... kn
, 其中這裡的最大值是 k_max
。
根據一頓數學推導,我們可以得出一個結論:2^{k_ max} 來作為 n 的估計值。
也就是說你可以根據最大投擲次數近似的推算出進行了幾次伯努利過程。
所以 HyperLogLog 的基本思想是利用集合中數字的位元串第一個 1 出現位置的最大值來預估整體基數,但是這種預估方法存在較大誤差,為了改善誤差情況,HyperLogLog 中引入分桶平均的概念,計算 m 個桶的調和平均值。
Redis 內部使用字串點陣圖來儲存 HyperLogLog 所有桶的計數值,一共分了 2^14 個桶,也就是 16384 個桶。每個桶中是一個 6 bit 的陣列。
這段程式碼描述了 Redis HyperLogLog 資料結構的頭部定義(hyperLogLog.c 中的 hllhdr 結構體)。以下是關於這個資料結構的各個欄位的解釋。
struct hllhdr {
char magic[4];
uint8_t encoding;
uint8_t notused[3];
uint8_t card[8];
uint8_t registers[];
};
magic[4]:這個欄位是一個 4 位元組的字元陣列,用來表示資料結構的識別符號。在 HyperLogLog 中,它的值始終為"HYLL",用來標識這是一個 HyperLogLog 資料結構。 encoding:這是一個 1 位元組的欄位,用來表示 HyperLogLog 的編碼方式。它可以取兩個值之一:
HLL_DENSE
:表示使用稠密表示方式。HLL_SPARSE
:表示使用稀疏表示方式。
圖 2-45
Redis 對 HyperLogLog
的儲存進行了最佳化,在計數比較小的時候,儲存空間採用係數矩陣,佔用空間很小。
只有在計數很大,稀疏矩陣佔用的空間超過了閾值才會轉變成稠密矩陣,佔用 12KB 空間。
3. 出招實戰:網頁訪問量統計
在移動網際網路的業務場景中,資料量很大,系統需要儲存這樣的資訊:一個 key 關聯了一個資料集合,同時對這個資料集合做統計做一個報表給運營人員看。
比如。
統計一個 APP
的日活、月活數。統計一個頁面的每天被多少個不同賬戶訪問量(Unique Visitor,UV)。 統計使用者每天搜尋不同詞條的個數。 統計註冊 IP 數。
通常情況下,系統面臨的使用者數量以及訪問量都是巨大的,比如百萬、千萬級別的使用者數量,或者千萬級別、甚至億級別的訪問資訊,咋辦呢?
使用 Set 實現
一個使用者一天內多次訪問一個網站只能算作一次,所以很容易就想到透過 Redis 的 Set 集合來實現。
比如微信暱稱叫 “Chaya” 的小姐姐訪問【愛一個人總是要掉眼淚的風險】這篇文章時,我把這個微信暱稱 “Chaya” 存到 Set 集合中。
SADD 愛一個人總是要掉眼淚的風險:uv 碼哥 Chaya 趙小因 Chaya
(integer) 3
“Chaya” 多次訪問這篇文章, Set 的去重特性保證集合中只有一個記錄。接著,透過 SCARD
命令,統計頁面 UV。指令返回這個集合的元素個數(也就是微信暱稱個數)。
SCARD 愛一個人總是要掉眼淚的風險:uv
(integer) 3
使用 HyperLogLog 實現
❝Chaya:“Set 集合雖好,如果文章非常火爆達到千萬級別,一個 Set 集合就儲存了千萬個使用者的 ID,頁面多了消耗的記憶體也太大了。”
不要怕,只要思想不滑坡,辦法總比困難多。這些就是典型的基數統計應用場景,基數統計:統計一個集合中不重複元素的個數。
HyperLogLog
的優點在於它所需的記憶體並不會因為集合的大小而改變,無論集合包含的元素有多少個,HyperLogLog 進行計算所需的記憶體總是固定的,並且是非常少的。
每個 HyperLogLog
最多隻需要花費 12KB 記憶體,在標準誤差 0.81%
的前提下,就可以計算 2 的 64 次方個元素的基數。
HyperLogLog 使用太簡單了。PFADD、PFCOUNT、PFMERGE
三個指令打天下。
PFADD
每訪問一次頁面,呼叫 PFADD
指令 將這個使用者 ID 新增到 HyperLogLog 中。如下 一共有三個使用者訪問了這頁面,其中 Chaya 訪問了兩次,但只算一次。
PFADD 愛一個人總是要掉眼淚的風險:uv 碼哥 Chaya 趙小因 Chaya
如果執行命令後 HyperLogLog 估計的近似基數發生變化,PFADD
則返回 1,否則返回 0。如果指定的鍵不存在,該命令會自動建立一個空的 HyperLogLog 結構。
pfadd
命令並不會一次性分配 12k 記憶體,而是隨著基數的增加而逐漸增加記憶體分配;
PFCOUNT
接下來,透過 PFCOUNT
指令獲取文章【愛一個人總是要掉眼淚的風險】的 UV 值,可以看到返回值是 3 ,符合預期。
> PFCOUNT 愛一個人總是要掉眼淚的風險:uv
3
PFMERGE 合併統計
❝Chaya:“還有一個變態需求,對文章進行標籤分類,運營說要把都是情感文章標籤的幾個頁面資料合併統計。”
其中頁面的 UV 訪問量也需要合併,那這個時候 PFMERGE
就可以派上用場了,也就是同樣的使用者訪問這兩個頁面則只算做一次。
如下指令,把愛一個人總是要掉眼淚的風險:uv
和愛情是幸福和不委屈:uv
兩個 HyperLogLog 集合資料合併到情感分類文章:uv
這個集合中。
PFADD 愛情是幸福和不委屈:uv Chaya 趙小因 幸運草
# 合併兩個頁面 UV
PFMERGE 情感分類文章:uv 愛一個人總是要掉眼淚的風險:uv 愛情是幸福和不委屈:uv
接著,執行 PFCOUNT 情感分類文章:uv
統計合併後的資料。
> PFCOUNT 情感分類文章:uv
4
將多個 HyperLogLog 合併(merge)為一個 HyperLogLog , 合併後的 HyperLogLog 的基數接近於所有輸入 HyperLogLog 的可見集合(observed set)的並集。
4. Redisson 實戰
開門見山,Spring Boot 與 Redisson 整合詳見前面篇章,主要有四個方法。
add、addAll
,閱讀文章呼叫該方法將資料存入 HyperLogLog 中。count
,統計基數。merge
,合併多個 HyperLogLog 為一個。
@Service
public class HyperLogLogService {
@Autowired
private RedissonClient redissonClient;
/**
* 將資料新增到 HyperLogLog
*
* @param logName
* @param item
* @param <T>
*/
public <T> void add(String logName, T item) {
RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);
hyperLogLog.add(item);
}
/**
* 將集合資料新增到 HyperLogLog
* @param logName
* @param items
* @param <T>
*/
public <T> void addAll(String logName, List<T> items) {
RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);
hyperLogLog.addAll(items);
}
/**
* 將 otherLogNames 的 log 合併到 logName
*
* @param logName 當前 log
* @param otherLogNames 需要合併到當前 log 的其他 logs
* @param <T>
*/
public <T> void merge(String logName, String... otherLogNames) {
RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);
hyperLogLog.mergeWith(otherLogNames);
}
/**
* 統計基數
*
* @param logName 需要統計的 logName
* @param <T>
* @return
*/
public <T> long count(String logName) {
RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);
return hyperLogLog.count();
}
}
Redis Stream 資料結構實現原理真的很強 Redis Sorted Set 底層實現原理深度解讀與排行榜實戰 深度圖解 Redis Hash(雜湊表)實現原理
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027826/viewspace-2985648/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 學Redis這篇就夠了Redis
- Redis主從複製看這篇就夠了Redis
- mongoDB看這篇就夠了MongoDB
- 搞透 IOC,Spring IOC 看這篇就夠了!Spring
- 學習Less-看這篇就夠了
- [收藏]學習sed,看這篇就夠了
- Redis基礎命令彙總,看這篇就夠了Redis
- Oracle索引,看這篇就夠了Oracle索引
- 入門Webpack,看這篇就夠了Web
- Android Fragment看這篇就夠了AndroidFragment
- OAuth授權|看這篇就夠了OAuth
- Zookeeper入門看這篇就夠了
- Git 看這一篇就夠了Git
- React入門看這篇就夠了React
- JavaScript正則,看這篇就夠了JavaScript
- 小程式分享,看這篇就夠了
- 面試中關於Redis的問題看這篇就夠了面試Redis
- Flutter DataTable 看這一篇就夠了Flutter
- 小程式入門看這篇就夠了
- Java 動態代理,看這篇就夠了Java
- vue 元件通訊看這篇就夠了Vue元件
- java序列化,看這篇就夠了Java
- 代理模式看這一篇就夠了模式
- EFCore 6.0入門看這篇就夠了
- ZooKeeper分散式配置——看這篇就夠了分散式
- Java 集合看這一篇就夠了Java
- 小白如何學習六西格瑪,看這篇就夠了!
- 學Mybatis,入門看這一篇就夠你學的了!MyBatis
- 瞭解 MongoDB 看這一篇就夠了MongoDB
- 關於SwiftUI,看這一篇就夠了SwiftUI
- 前端er瞭解GraphQL,看這篇就夠了前端
- 入門Hbase,看這一篇就夠了
- macOS 安裝 Nebula Graph 看這篇就夠了Mac
- HashMap的實現原理(看這篇就夠了)HashMap
- jQuery入門看這一篇就夠了jQuery
- Elasticsearch入門,看這一篇就夠了Elasticsearch
- ActiveMq 之JMS 看這一篇就夠了MQ
- MySQL入門看這一篇就夠了MySql