Redis高级数据结构原理解析-bitmap,hyperloglog
生活随笔
收集整理的這篇文章主要介紹了
Redis高级数据结构原理解析-bitmap,hyperloglog
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
Redis 位圖
- 開發過程中,我們可能遇到這種場景記錄用戶的打卡情況,簽到情況,這些場景只有兩種結果,有或者沒有,加入記錄的數據量比較大,比如用一年的數據,如果用Redis中普通key/value,每個用戶要記錄365個,當用戶上億時候,需要的存儲就比較多了。Redis為解決這種勤快提供了位圖的數據結構,這樣一條數據在位圖中只需要占用1位,365天就是365位,一個字節8位,你們就是46個字節左右,這樣大大節省空間,位圖的最小單位是bit,每個bit只能取1或者0.
- 位圖在Redis中就是普通字符串,也就是byte數組。可以通過Redis命令get/set直接獲取,和設置整個位圖的內容,也可以使用getbit/setbit 等將byte看出位數組操作某一個位。
基本用法
- Redis中提供位圖的統計指令bitcount和位圖查找指令bitpos, bitcount統計指定范圍內1 個數,bitpos查找指定范圍內出現的第一個0或者1。
- 還是簽到的案例,我們通過bitcount統計用戶一共簽到多少天,通過bitpos查找從那一天開始簽到的。還可以指定范圍參數[start, end],可以統計某個時間范圍內用戶簽到多少天。
- 注意此處的start,end是字節的索引,也就是說指定的范圍必須是8的倍數(1字節=8位),不能任意指定。如下案例
特殊的bitfield
- Redis中setbit, getbit指定的值都是單個位的,如果要一次操作多個位,就必須使用管道處理。Redis3.2 之后增加了一個強大的指令bitfield,可以一次性處理多個位操作。
- bitfield 有三個基本指令,get,set, incrby,可以對指定位片段進行讀寫,但最多只能處理64個連續位,如果超過64個,就得使用多個子指令,bitfield可以一次執行多個指令。如下是h,e兩個字符的bit位示意圖:
- 如上結果中,有符號以上是第一個是按符號位算,其他的才是值,符號為0-正 1-負,無符號標識非負數,沒有符合為,獲取到的位全是數值。有符號數最多獲取64位,無符號只能獲取63位,因為Redis協議中的integer是有符號數,最大64位,符號位占用一位,所以只有63位數據位,如果超出限制,會提示參數錯誤。一次性執行多個指令:
- bitfielt提供單個字節替換的功能,因為1字節=8為,我們可以替換其中的一個8 位bit符來達到操作字節的目的,如下面這個案例我們將hello中的第二個字符替換成hallo:
- 同樣的bitmap中也有自增命令incrby,用來對指定范圍的位進行自增操作,因為自增的時候就可能出現數據溢出的風險,Redis中默認是折返處理,比如溢出后將溢出符號為丟棄,如果8位無符號255,加1 溢出變成0 也就是向上近位 1111 1111 變成1 0000 0000但是只有8 位所以第一位1 被舍棄,變成最后的0 。
- bitfield指令提供溢出的操作行為默認是折返(wrap),還可以失敗(fail)就是報錯不執行,飽和截斷(sat)超過范圍停留在最大值比如255 +1 = 255,如下三個案例:
Redis HyperLogLog
-Java web開發過程中經常需要統計網頁的UV,這個我們怎么實現,之前我們統計PV只需要給每個網頁分配一個Redis計數器就可以,將key后綴加上日期,每天一個,每次請求incrby就行。但是UV需要區分不同的用戶,最笨的辦法是每個也沒分配一個set集合,用來存儲訪問的用戶ID,但是這個方式是非常消耗內存的,當有熱點也沒達到百萬兵法時候,當有N個熱點也沒時候,那內存消耗將會是一個非常大的數量,Redis中HyperLogLog引入來一個新的解決方案,提供來不精確的去重計數方案,雖然不精確但是標準誤差也就0.81%而已,對于統計UV的這種業務場景是完全能夠接受的。
HyperLogLog使用方式
127.0.0.1:6379> pfadd code userq (integer) 1 127.0.0.1:6379> pfadd code user1 (integer) 1 127.0.0.1:6379> pfcount code (integer) 2- 如上,Redis中HyperLogLog提供pfadd,pfcount,一個增加,一個統計,我們試了一把,統計沒有誤差,我們執行100000 次add操作來測試他的誤差以及性能
- 如上結果相差285個按照0.285%的誤差,對于UV的統計需求誤差可以忽略,我們可以多次重復執行,其實還是得到近似的結果,說明他有去重的功能。
pfmerge
- HypLogLog除了提供上面的兩個命令還有一個merge功能,用于將多個計數器累加成一個新的pf值
- 比如多個內容相似的網頁,運營需要合并統計,就可以用這個來統計。
- HyperLogLog這個數據結構占用內存異常的小,只需要占用12KB內存,如果統計的計數有億級別,那么對空間的節省是非常令人驚訝的
存儲原理
-
HypLogLog有以下幾個特點:
- 實現比較困難
- 能夠用極少的內存來統計巨量的數據,在Redis中實現的HyperLogLog,只需要12k內存就能統計2^64個數據
- 計數存在一定的誤差0.81%
- 誤差可以被設置輔助計算因子進行降低
-
12K是一個什么概念,1byte=8bit,long類型是8字節64bit,對應2^63-1 個數,那假設有這么多個數,從0 ~ 2 ^ 63-1,按照long以及1k=1026字節內存總數應該是((2 ^ 63-1) * 8.1024)K,這個遠遠超過來12k。
伯努力試驗
- HyperLogLog用極少內存完成巨量數據計算,我們先需要了解他的數學原理:伯努利試驗(概率論)
- 拋硬幣一次得到正反概率都是50%,假設一直拋硬幣,一直到得到一次正面,記這位一次完整試驗,期間記錄拋硬幣的次數,這個試驗就是伯努利試驗;
- 對于多次伯努利試驗,假設多次為n次,每次試驗拋次數為K(拋K次得到正面),因此第一次k1, 依次類推,第n次kn
- 期間必然在n次試驗中有一個最大拋次數k,我們稱為k_max。
- 伯努利試驗容易得出如下結論
- n次伯努利過程拋次數不大于k_max
- n次伯努利過程至少依次k_max
- 結合極大似然估算方法,可以發現n和k_max的估算關聯: n = 2^(k_max),這種估算方法需要用概率論和數理統計方法推導證明,此處略過。
- 如上試驗,共三組,k_max=6,n=3,代如公式3 != 2^6。此處只能說明樣本太少,估算誤差大。
估算優化
- 我們假設上面3次試驗算一輪,當n足夠大,估算誤差才小,例如我們進行100 輪,然后每一輪取出k_max,在取出平均數即:k_max/100,在估算數n,下面是LogLog的估算公式:
- 上面公式中DVLL對于的是n,contant是修正因子,具體值不定,可以根據時間情況分鐘設置,m代表試驗輪次,頭上有一橫的R就是平均數:(k_max_1+…+k_max_n)/m。
- 這種通過增加試驗次數在取k_max平均數的算法優化就是LogLog的做法,而HyperLogLog和LogLoe有一點區別,不是用平均數,而是調和平均數,相對平均數更不容易受到大數的影響。如下案例
- 顯然,調和平均數比平均數更準確表達事實情況,下面是調和平均數的計算方式,∑ 是累加符號:
- 以上其實就是他的數學原理。
帶入實際案例講解
- 上面的內容中通過拋硬幣的案例解釋了伯努利實驗通過k_max來估算n,那么這種估算方法和我們UV統計方式有什么關聯,我們需求是統計APP或者網頁的一個頁面,每天有多少個不同的用戶點擊進入的次數。同一個用戶的反復點擊進入記為1。HyperLogLog是按如下幾個步驟操作:
- 通過Hash函數,將數據轉為比特串,比如,輸入一個用戶id=5,我們轉成101,通過這種方式將訪問用戶數據和上面的拋硬幣對應上,比特串中,0 代表反面,1 代表正面,如果一個數據最終被轉成10010000,那么按照從右到左,從低位到高位看,我們認為首次出現1 的時候代表正面。
- 基于上面的估算結論,我們可以通過多次試驗的最大拋到正面的次數來預估總共進行了多少次實驗,同樣更具存儲的數據中,轉化后出現了1 的最大值k_max來估算出總量n。
- 分桶就是分開進行多輪次。抽象到計算機中存儲,可以看成是一個以單位是bit,長度是L的大的數組S,將S平均分成M組,這里的M其實就是對應多少輪,然后每組鎖占有的比特個數是平均的,設為P,我們可以得出以下公式
- L = S.length
- L = M * P
- 以K為單位,S占用內存= L / 8 / 1024
- 在Redis中,HyperLogLog設置為: m=16834,p=6,L=168346。占用內存為168346/8/1024 = 12K
- 如下示意來標識他存儲的結構:
- 我們回到原始APP頁面統計的具體問題中,設APP主頁的key為:main, 用戶id:idn, n->0,1,2,3 …
- 在這個統計問題中,不同用戶id標識了一個用戶,你們我們可以把用戶id作為被hash的輸入:hash(id) = 比特串
- 不同的用戶id必然有不同比特串,每一個比特串,也必然至少出現一次1 的位置,我們類比每一個比特串就是一次伯努利試驗
- 現在要分輪次,即分桶存儲,設定每個比特串錢w位轉為10進制后,其值就對應于所在的桶的下標(1~16833)。假設比特串的低兩位用來計算桶下標,此時有一個用戶id的比特串是:1001 0110 0001 1,他的下標為 11(二進制) = 12 ^ 0 + 12 ^ 1 = 3,處于第三個桶。即第三輪
- 上面案例中,計算桶號后,剩下的比特串: 1001 0110 000, 從低位到高位看,第一次出現1 的位置是5 ,也就是說,此時第三個桶,第三輪試驗中k_max = 5, 5 對應二進制是 101,又因為每個通有p個比特位,單 p >= 3 時候,便可以將101存進去(三個比特位)。
- 模擬上面流程,多個不同用戶id就被分配到不同桶中區了,并且每個桶有其k_max,然后統計出main頁面有多少用戶點擊量,就是一次估算。最終結合所有桶中的k_max,帶入估算公式,便得出我們的估算值。
- 更具上面調和平均數的公式,得出下面HyperLogLog的估算公式,變量的意義和LogLog的一樣:
- 其中以下表示每個通的估計值,對每個通估計值進行調和平均數求職
Redis中HyperLogLog的原理
- 首先Redis中HyperLogLog是他的意志高級數據結構,他的實現有16384個桶,即 2^14 = 16384,每個桶有6位,每個桶可以表達的最大數字是 :2 ^ 5 + 2 ^ 4 + 2 ^ 3+ 2 ^ 2 + 2 ^ 1 = 63,二進制是111 111。
- 對應pfadd key value命令,存儲時候,value被hash成64 位,即64bit位,前14位用來分桶,前14位的二進制轉成10進制就是通標號。之所以選14位是因為2^14 = 16384 ,剛好最大的時候可以把桶利用完,不造成浪費,假設一個字符串的錢14位是00 0000 0000 0010,其十進制是2 ^ 1 = 2,你們就會被放到編號是2 的桶
- Index的轉化規則:
- 因為完整value是64位,減去低位14位,剩下高位50 位,極端情況,出現1 的位置是第50位,次數index = 50,轉二進制是 110010,不超過6 bit數,所以剛好可以被設置到第2 號桶中去,因為50是最壞的情況,類比第五十次才拋出正面,最壞情況,其他情況必然都能被存儲到桶中,
- 如果不同value有相同的前面 14 位,也就是兩個用戶hash(userid) 得到的64位低位14 位相同,但是后面出現1 的位置不同。那么比較原來的index是否比新的index打,是則替換,否則不變,存儲更大的數字
- 最終一個key對應的16384 個桶都設置了很多的value,每個桶有一個k_max,此時調用pfcount 時候,按前面介紹估算方法,計算key的設置了多少次的value,就得到對應統計值
- value被轉為64 位比特串,最終被按照上做法記錄到每個桶中,64位轉十進制2 ^ 64 ,HyperLogLog僅僅用了16384 * 6 /8/1024= 12K存儲空間就能夠完成2 ^64 數量基數的數據統計。
布隆過濾器
- 上面HyperLogLog中,如果我想知道某個userId是不是已經存儲了,發現找不到對應Redis命令來查找,因為HyperLogLog只提供了add,count的功能,沒有提供contant功能。
- 如果有這樣的業務,比如,我們做用戶推薦,新聞推薦的時候,他會給我們不停的推薦新內容,怎么去掉已經看過的內容。推送去重這是一個問題
- 用set存儲歷史記錄,太費內存,在Redis高級數據結構中布隆過濾器(Bloom Filter),專門用來解決去重問題。并且空間上節省90%以上,但是他會有一點不精確,可能導致微小的誤判。
Redis中的布隆過濾器
- Redis4.0 才提供了bloomfilter,但是他是作為一個插件加載到Redis server中,給Redis提供了強大的布隆過濾功能,
- 布隆過濾器有兩個基本指令
- bf.add添加元素, 一次只能添加一個元素
- bf.madd 添加元素,一次可以添加多個
- bf.exists 判斷是否存在,一次判斷一個
- bf.mexists 判斷多個元素是否存在
布隆過濾器原理
- 每個bloomFilter 對應到Redis的數據結構就是一個大小的bitmap加上杰哥不一樣的無偏hash函數,如下面圖,f,g,h就是這樣的hash函數。所謂的無偏就是能把元素值算的比較均勻,讓元素被hash后映射到位數字中的位置比較隨機。
- 想bloomfilter中添加key時候,會使用多個hash函數對key進行hash,算得一個整數值,然后對維數組長度進行取模運算得到這個整數在位數組中的位置,每個hash函數都會算得一個不同的位置,再把這個位數組的這幾個位置都設置為1,就完成add操作。所以其實一個key是會標記多個bit位,這取決于他hash算法的個數,每個hash都出來一個位置。(減少重復率)
- 向bloomFilter詢問key是否存在是,也和add一樣,吧hash的幾個位置算出,看bitmao里面是不是都是1,有一個是0 就一定不存在 。但是都是1 并不100%保證一定存在,只是極有可能存在,因為這些被設置為1 ,可能是其他key存在的hash位,如果數組比較稀疏,判斷的正確率就高,如果數組擁擠,誤判就高。
- bloomFilter使用之前可以指定其參數,Redis提供了自定義參數的BloomFilter,我們需要在add之前用bf.reserve顯示的創建,如果對于key已經存在,你們bf.reserve會報錯。bf.reserve有三個參數key, error_rate(錯誤率), initial_size(預計數據大小)
- error_rate越低,需要空間越大
- initial_size表示預計放入的元素數量,當實際數量超過這個數,誤判就會上升,所以需要提前設置一個較大的數值避免超過導致誤判率升高
- 如果不使用bf.reserver,默認的error_rate是0.01,默認的initial_size是100.
空間占用估計
- bloomFilter空間占用有一個簡單計算公式,不推導了,直接給吧,有兩個參數,第一個語句元素數量n,第二個錯誤率f, 公式根據兩個輸入得到兩個輸出,第一個輸出是位數組的長度1,也就是需要的存儲空間大小bit,第二個輸出是hash函數的最佳數量k。hash函數的數量也直接影響錯誤率,最佳數量會有最低錯誤率。
- 如上公式得出:
- 位數越長(1/n),錯誤率發越低
- 位數月長(1/n),hash函數需要的最佳數量也越多,影響計算效率
- 當一個元素評價需要1字節8bit的指紋空間(槽空間)時候(1/n = 8),錯誤率大概2%
- 錯誤率0.1% 時候,一個元素平均指紋空間14.377bit,大約15bit
- 即使15bit也比set強,因為這邊是15位,set存儲取決于他存儲的字符串大小,不是一個量級。
元素超出誤判率變化
- 當實際元素超過預計元素,錯誤率會有多大變化,這個也有公司,引入參數t 表示實際元素和預計元素的倍數
-暫時就這些
上一篇Redis基礎數據結構內部實現簡單介紹
下一篇Redis流量控制策略
總結
以上是生活随笔為你收集整理的Redis高级数据结构原理解析-bitmap,hyperloglog的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis基础数据结构内部实现简单介绍
- 下一篇: 艾灸坐灸的好处具体都有哪些呢