學透 Redis HyperLogLog,看這篇就夠了

資料庫工作筆記發表於2023-09-25

來源:碼哥位元組

在移動網際網路的業務場景中,資料量很大,系統需要儲存這樣的資訊:一個 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

比如說,拋一次硬幣就出現正面了,此時 k1; 第一次拋硬幣是反面,則繼續拋,直到第三次才出現正面,此時 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[];
};
  1. magic[4]:這個欄位是一個 4 位元組的字元陣列,用來表示資料結構的識別符號。在 HyperLogLog 中,它的值始終為"HYLL",用來標識這是一個 HyperLogLog 資料結構。
  2. encoding:這是一個 1 位元組的欄位,用來表示 HyperLogLog 的編碼方式。它可以取兩個值之一:
  • HLL_DENSE:表示使用稠密表示方式。
  • HLL_SPARSE:表示使用稀疏表示方式。
  • notused[3]:這是一個 3 位元組的欄位,目前保留用於未來的擴充套件,要求這些位元組的值必須為零。
  • card[8]:這是一個 8 位元組的欄位,用來儲存快取的基數(基數估計的值)。
  • egisters[]:這個欄位是一個可變長度的位元組陣列,用來儲存 HyperLogLog 的資料。
  • 學透 Redis HyperLogLog,看這篇就夠了

    圖 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();
        }
    }

    1. Redis Stream 資料結構實現原理真的很強
    2. Redis Sorted Set 底層實現原理深度解讀與排行榜實戰
    3. 深度圖解 Redis Hash(雜湊表)實現原理

    來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027826/viewspace-2985648/,如需轉載,請註明出處,否則將追究法律責任。

    相關文章