1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Redis 實戰(zhàn)篇:巧用 Bitmap 實現(xiàn)億級海量數(shù)據(jù)統(tǒng)計

        共 4523字,需瀏覽 10分鐘

         ·

        2021-06-07 15:49


        在移動應(yīng)用的業(yè)務(wù)場景中,我們需要保存這樣的信息:一個 key 關(guān)聯(lián)了一個數(shù)據(jù)集合。

        常見的場景如下:

        • 給一個 userId ,判斷用戶登陸狀態(tài);
        • 顯示用戶某個月的簽到次數(shù)和首次簽到時間;
        • 兩億用戶最近 7 天的簽到情況,統(tǒng)計 7 天內(nèi)連續(xù)簽到的用戶總數(shù);

        通常情況下,我們面臨的用戶數(shù)量以及訪問量都是巨大的,比如百萬、千萬級別的用戶數(shù)量,或者千萬級別、甚至億級別的訪問信息。

        所以,我們必須要選擇能夠非常高效地統(tǒng)計大量數(shù)據(jù)(例如億級)的集合類型。

        如何選擇合適的數(shù)據(jù)集合,我們首先要了解常用的統(tǒng)計模式,并運用合理的數(shù)據(jù)類型來解決實際問題。

        四種統(tǒng)計類型:

        1. 二值狀態(tài)統(tǒng)計;
        2. 聚合統(tǒng)計;
        3. 排序統(tǒng)計;
        4. 基數(shù)統(tǒng)計。

        本文將由二值狀態(tài)統(tǒng)計類型作為實戰(zhàn)篇系列的開篇,文中將用到 String、Set、Zset、List、hash 以外的拓展數(shù)據(jù)類型 Bitmap 來實現(xiàn)。

        文章涉及到的指令可以通過在線 Redis 客戶端運行調(diào)試,地址:https://try.redis.io/,超方便的說。

        二值狀態(tài)統(tǒng)計

        ?

        碼哥,什么是二值狀態(tài)統(tǒng)計呀?

        也就是集合中的元素的值只有 0 和 1 兩種,在簽到打卡和用戶是否登陸的場景中,只需記錄簽到(1)未簽到(0),已登錄(1)未登陸(0)。

        假如我們在判斷用戶是否登陸的場景中使用 Redis 的 String 類型實現(xiàn)(key -> userId,value -> 0 表示下線,1 - 登陸),假如存儲 100 萬個用戶的登陸狀態(tài),如果以字符串的形式存儲,就需要存儲 100 萬個字符串了,內(nèi)存開銷太大。

        ?

        碼哥,為什么 String 類型內(nèi)存開銷大?

        String 類型除了記錄實際數(shù)據(jù)以外,還需要額外的內(nèi)存記錄數(shù)據(jù)長度、空間使用等信息。

        當(dāng)保存的數(shù)據(jù)包含字符串,String 類型就使用簡單動態(tài)字符串(SDS)結(jié)構(gòu)體來保存,如下圖所示:

        SDS
        • len:占 4 個字節(jié),表示 buf 的已用長度。
        • alloc:占 4 個字節(jié),表示 buf 實際分配的長度,通常 > len。
        • buf:字節(jié)數(shù)組,保存實際的數(shù)據(jù),Redis 自動在數(shù)組最后加上一個 “\0”,額外占用一個字節(jié)的開銷。

        所以,在 SDS 中除了 buf 保存實際的數(shù)據(jù), len 與 alloc 就是額外的開銷。

        另外,還有一個 RedisObject 結(jié)構(gòu)的開銷,因為 Redis 的數(shù)據(jù)類型有很多,而且,不同數(shù)據(jù)類型都有些相同的元數(shù)據(jù)要記錄(比如最后一次訪問的時間、被引用的次數(shù)等)。

        所以,Redis 會用一個 RedisObject 結(jié)構(gòu)體來統(tǒng)一記錄這些元數(shù)據(jù),同時指向?qū)嶋H數(shù)據(jù)。

        對于二值狀態(tài)場景,我們就可以利用 Bitmap 來實現(xiàn)。比如登陸狀態(tài)我們用一個 bit 位表示,一億個用戶也只占用 一億 個 bit 位內(nèi)存 ≈ (100000000 / 8/ 1024/1024)12 MB。

        大概的空間占用計算公式是:($offset/8/1024/1024) MB
        ?

        什么是 Bitmap 呢?

        Bitmap 的底層數(shù)據(jù)結(jié)構(gòu)用的是 String 類型的 SDS 數(shù)據(jù)結(jié)構(gòu)來保存位數(shù)組,Redis 把每個字節(jié)數(shù)組的 8 個 bit 位利用起來,每個 bit 位 表示一個元素的二值狀態(tài)(不是 0 就是 1)。

        可以將 Bitmap 看成是一個 bit 為單位的數(shù)組,數(shù)組的每個單元只能存儲 0 或者 1,數(shù)組的下標在 Bitmap 中叫做 offset 偏移量。

        為了直觀展示,我們可以理解成 buf 數(shù)組的每個字節(jié)用一行表示,每一行有 8 個 bit 位,8 個格子分別表示這個字節(jié)中的 8 個 bit 位,如下圖所示:

        Bitmap

        8 個 bit 組成一個 Byte,所以 Bitmap 會極大地節(jié)省存儲空間。 這就是 Bitmap 的優(yōu)勢。

        判斷用戶登陸態(tài)

        ?

        怎么用 Bitmap 來判斷海量用戶中某個用戶是否在線呢?

        Bitmap 提供了 GETBIT、SETBIT 操作,通過一個偏移值 offset 對 bit 數(shù)組的 offset 位置的 bit 位進行讀寫操作,需要注意的是 offset 從 0 開始。

        只需要一個 key = login_status 表示存儲用戶登陸狀態(tài)集合數(shù)據(jù), 將用戶 ID 作為 offset,在線就設(shè)置為 1,下線設(shè)置 0。通過 GETBIT判斷對應(yīng)的用戶是否在線。50000 萬 用戶只需要 6 MB 的空間。

        SETBIT 命令

        SETBIT <key> <offset> <value>

        設(shè)置或者清空 key 的 value 在 offset 處的 bit 值(只能是 0 或者 1)。

        GETBIT 命令

        GETBIT <key> <offset>

        獲取 key 的 value 在 offset 處的 bit 位的值,當(dāng) key 不存在時,返回 0。

        假如我們要判斷 ID = 10086 的用戶的登陸情況:

        第一步,執(zhí)行以下指令,表示用戶已登錄。

        SETBIT login_status 10086 1

        第二步,檢查該用戶是否登陸,返回值 1 表示已登錄。

        GETBIT login_status 10086

        第三步,登出,將 offset 對應(yīng)的 value 設(shè)置成 0。

        SETBIT login_status 10086 0

        用戶每個月的簽到情況

        在簽到統(tǒng)計中,每個用戶每天的簽到用 1 個 bit 位表示,一年的簽到只需要 365 個 bit 位。一個月最多只有 31 天,只需要 31 個 bit 位即可。

        ?

        比如統(tǒng)計編號 89757 的用戶在 2021 年 5 月份的打卡情況要如何進行?

        key 可以設(shè)計成 uid:sign:{userId}:{yyyyMM},月份的每一天的值 - 1 可以作為 offset(因為 offset 從 0 開始,所以 offset = 日期 - 1)。

        第一步,執(zhí)行下面指令表示記錄用戶在 2021 年 5 月 16 號打卡。

        SETBIT uid:sign:89757:202105 15 1

        第二步,判斷編號 89757 用戶在 2021 年 5 月 16 號是否打卡。

        GETBIT uid:sign:89757:202105 15

        第三步,統(tǒng)計該用戶在 5 月份的打卡次數(shù),使用 BITCOUNT 指令。該指令用于統(tǒng)計給定的 bit 數(shù)組中,值 = 1 的 bit 位的數(shù)量。

        BITCOUNT uid:sign:89757:202105

        這樣我們就可以實現(xiàn)用戶每個月的打卡情況了,是不是很贊。

        ?

        如何統(tǒng)計這個月首次打卡時間呢?

        Redis 提供了 BITPOS key bitValue [start] [end]指令,返回數(shù)據(jù)表示 Bitmap 中第一個值為 bitValue 的 offset 位置。

        在默認情況下, 命令將檢測整個位圖, 用戶可以通過可選的 start 參數(shù)和 end 參數(shù)指定要檢測的范圍。

        所以我們可以通過執(zhí)行以下指令來獲取 userID = 89757 在 2021 年 5 月份首次打卡日期:

        BITPOS uid:sign:89757:202105 1

        需要注意的是,我們需要將返回的 value + 1 ,因為 offset 從 0 開始。

        連續(xù)簽到用戶總數(shù)

        ?

        在記錄了一個億的用戶連續(xù) 7 天的打卡數(shù)據(jù),如何統(tǒng)計出這連續(xù) 7 天連續(xù)打卡用戶總數(shù)呢?

        我們把每天的日期作為 Bitmap 的 key,userId 作為 offset,若是打卡則將 offset 位置的 bit 設(shè)置成 1。

        key 對應(yīng)的集合的每個 bit 位的數(shù)據(jù)則是一個用戶在該日期的打卡記錄。

        一共有 7 個這樣的 Bitmap,如果我們能對這 7 個 Bitmap 的對應(yīng)的 bit 位做 『與』運算。

        同樣的 UserID  offset 都是一樣的,當(dāng)一個 userID 在 7 個 Bitmap 對應(yīng)對應(yīng)的 offset 位置的 bit = 1 就說明該用戶 7 天連續(xù)打卡。

        結(jié)果保存到一個新 Bitmap 中,我們再通過 BITCOUNT 統(tǒng)計 bit = 1 的個數(shù)便得到了連續(xù)打卡 7 天的用戶總數(shù)了。

        Redis 提供了 BITOP operation destkey key [key ...]這個指令用于對一個或者多個 鍵 = key 的 Bitmap 進行位元操作。

        opration 可以是 and、OR、NOTXOR。當(dāng) BITOP 處理不同長度的字符串時,較短的那個字符串所缺少的部分會被看作 0 。空的 key 也被看作是包含 0 的字符串序列。

        便于理解,如下圖所示:

        BITOP

        3 個 Bitmap,對應(yīng)的 bit 位做「與」操作,結(jié)果保存到新的 Bitmap 中。

        操作指令表示將 三個 bitmap 進行 AND 操作,并將結(jié)果保存到 destmap 中。接著對 destmap 執(zhí)行 BITCOUNT 統(tǒng)計。

        // 與操作
        BITOP AND destmap bitmap:01 bitmap:02 bitmap:03
        // 統(tǒng)計 bit 位 =  1 的個數(shù)
        BITCOUNT destmap

        簡單計算下 一個一億個位的 Bitmap占用的內(nèi)存開銷,大約占 12 MB 的內(nèi)存(10^8/8/1024/1024),7 天的 Bitmap 的內(nèi)存開銷約為 84 MB。同時我們最好給 Bitmap 設(shè)置過期時間,讓 Redis 刪除過期的打卡數(shù)據(jù),節(jié)省內(nèi)存。

        小結(jié)

        思路才是最重要,當(dāng)我們遇到的統(tǒng)計場景只需要統(tǒng)計數(shù)據(jù)的二值狀態(tài),比如用戶是否存在、 ip 是否是黑名單、以及簽到打卡統(tǒng)計等場景就可以考慮使用 Bitmap。

        只需要一個 bit 位就能表示 0 和 1。在統(tǒng)計海量數(shù)據(jù)的時候?qū)⒋蟠鬁p少內(nèi)存占用。


        1、Intellij IDEA這樣 配置注釋模板,讓你瞬間高出一個逼格!
        2、基于SpringBoot的迷你商城系統(tǒng),附源碼!
        3、最牛逼的 Java 日志框架,性能無敵,橫掃所有對手!
        4、把Redis當(dāng)作隊列來用,真的合適嗎?
        5、驚呆了,Spring Boot居然這么耗內(nèi)存!你知道嗎?
        6、全網(wǎng)最全 Java 日志框架適配方案!還有誰不會?
        7、Spring中毒太深,離開Spring我居然連最基本的接口都不會寫了

        點分享

        點收藏

        點點贊

        點在看

        瀏覽 43
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            日韩黄色视频网站 | 娇妻乳峰乱颤娇喘连连 | a视频在线免费观看 | 久久久久成人电影 | 亚洲操逼视频网站 | 性爱无码视频 | 色哟哟 精品一区 | 成人免费高清无码视频 | 国产精品激情在线 | 特级西西444www大胆高清图片 |