位圖、HyperLogLog、布隆過濾器、Geohash


1. 節衣縮食-位圖

  在平時的開發中,會有一些bool 型數據需要存取,比如用戶的簽到記錄,簽了是1,沒簽是0,要記錄365天。如果使用普通的key/value,每個用戶需要記錄365個,當用戶數上億的時候,需要的存儲空間非常大。

  為了解決這個問題,Redis 提供了位圖數據結構,每天的簽到記錄只占一個位,365天就是365位(46個字節)。位圖的最小單位是比特,每個比特的取值只能是0或1。

  位圖不是特殊的數據結構,它的內容其實就是普通的字符串,也就是byte數組。我們可以使用get\set 直接獲取和設置整個位圖的內容,也可以使用位圖操作getbit\setbit 等將byte數組看成"位數組"來處理。

  Redis 的位數組是自動擴展的,如果設置了某個偏移位置超出了現有的內容范圍, 就會自動將位數組進行零擴充。

例如:  用位操作將字符串設置為hello

用python 查看hello的ASCII碼的二進制值;

>>> bin(ord('h'))
'0b1101000'
>>> bin(ord('e'))
'0b1100101'
>>> bin(ord('l'))
'0b1101100'
>>> bin(ord('l'))
'0b1101100'
>>> bin(ord('o'))
'0b1101111'

h 字符只需要設置 1/2/4 位設置為1, 其他自動為0
e 字符需要9、10、13、15 位需要設置
l 是17、18、20、21

 設置如下:( 只設置h 字符)

127.0.0.1:6379> setbit s 1 1
(integer) 0
127.0.0.1:6379> setbit s 2 1
(integer) 0
127.0.0.1:6379> setbit s 4 1
(integer) 0
127.0.0.1:6379> get s
"h"

零存零取:使用單個位設置位值,使用單個位操作獲取具體位值。

127.0.0.1:6379> getbit s 4
(integer) 1
127.0.0.1:6379> getbit s 3
(integer) 0

整存零取:使用字符串操作批量設置位值,使用單個位操作獲取具體位置。

127.0.0.1:6379> set w h
OK
127.0.0.1:6379> getbit w 4
(integer) 1
127.0.0.1:6379> getbit w 3
(integer) 0

統計和查找:bitcount 用來查找某個位置范圍內1的個數; bitpos 用來查找指定范圍第一個出現0或者1的位置

127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> get w
"hello"
127.0.0.1:6379> bitcount w
(integer) 21
127.0.0.1:6379> bitcount w 0 0 # 第一個字符中1的位數
(integer) 3
127.0.0.1:6379> bitcount w 0 1 # 前兩個字符中1的位數
(integer) 7
127.0.0.1:6379> bitpos w 0 #第一個0位
(integer) 0
127.0.0.1:6379> bitpos w 1 #第一個1位
(integer) 1
127.0.0.1:6379> bitpos w 1 1 1  #從第二個字符算起,第一個1位
(integer) 9
127.0.0.1:6379> bitpos w 1 2 2 # 從第三個字符算起,第一個1位
(integer) 17

魔術指令bitfield: 這個是一次操作多個位的操作。bitfield 有三個子指令,分別是get、set、incrby, 最多能操作64個連續的位;超過64位,就得使用多個子指令。

127.0.0.1:6379> bitfield w get u4 0        #從第一個位開始取4個位,結果是無符號數(u)
1) (integer) 6
127.0.0.1:6379> bitfield w get u3 2        #從第三個位開始取3個位,結果是無符號數
1) (integer) 5
127.0.0.1:6379> bitfield w get i4 0        #從第一個位開始取4個位,結果是有符號數(i)
1) (integer) 6
127.0.0.1:6379> bitfield w get i3 2        #從第三個位開始取3個位,結果是有符號數(i)
1) (integer) -3

所謂有符號數是指獲取的位數組中第一個位是符號位,剩下的才是值。如果第一位是1,那就是負數。無符號數表示非負數,沒有符號位,獲取的位數組全部都是值。

所以有了這個數據結構解決上面問題就比較簡單,一年的話統計365天中簽到的次數(位數為1的個數即可)。

2. HyperLogLog

   超日志記錄。提供不精確的去重計數方案,雖然不精確,但是也不是非常離譜,標准誤差是0.81%。

  比如一個場景:統計網頁每天的UV數據(UV統計用戶訪問次數,一個用戶一天的多次請求只能算一次)。最簡單的解決方案是每個頁面設置一個set,當一個請求進來時使用sadd 將當前用戶ID塞進去。 最后通過scard 取出這個集合的大小,這個大小就是頁面的UV。在數據量很多的情況下,比如有幾千個頁面,每個頁面有幾千萬個UV,那么需要的存儲空間也是驚人的。

1. 使用方法

HyperLogLog 提供了兩個指令。 pfadd 和 pfcount, 一個用於增加,一個用於計數。例如:

127.0.0.1:6379> pfadd codehole user1
(integer) 1
127.0.0.1:6379> pfadd codehole user2 user3
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 3

2. pfadd 中的pf 是什么意思

HyperLogLog數據結構 的發明人是Philippe Flajolet, pf 是他的名字的縮寫。

3. pfmerge 合並

  用於合並兩個或多個HyperLogLog

127.0.0.1:6379> pfadd codehole2 user4    # 在創建一個
(integer) 1
127.0.0.1:6379> pfmerge codehole codehole2    #將codehole2合並到codehole
OK
127.0.0.1:6379> pfcount codehole
(integer) 4
127.0.0.1:6379> pfcount codehole2
(integer) 1
127.0.0.1:6379> pfadd codehole user4    #重復添加不會計數
(integer) 0
127.0.0.1:6379> pfcount codehole
(integer) 4

 

HyperLogLog 是一種概率數據結構,它使用概率算法來統計集合的近似基數。其沒有提供pfcontains的功能,也就是如果先判斷某個元素是否在HyperLogLog 內部,是無能為力的。

3. 布隆過濾器

  上面的HyperLogLog 沒有提供pfcontains 的功能,也就是無法判斷某個元素是否在集合中。

  布隆過濾器可以理解為一個不精確的set結構。當布隆過濾器說某個值存在,可能不存在; 說某個值不存在就一定不存在。

  比如說,做一個用戶新聞推薦的系統,可以用布隆過濾器保存已經給用戶推送過的新聞。但是有可能出現誤差,也就是說有可能過濾掉那些用戶已經看過的內容; 而且布隆過濾器無法刪除元素。

1. redis 中的布隆過濾器

  Redis4.0 以后官方提供了布隆過濾器。布隆過濾器作為一個插件加載到RedisServer 中, 給Redis 提供了強大的布隆去重功能。

基於docker 安裝:

docker pull redislabs/rebloom

然后啟動容器

$ docker run -p6379:6379 redislabs/rebloom
1:C 27 Jun 2021 04:12:06.863 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 27 Jun 2021 04:12:06.863 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 27 Jun 2021 04:12:06.863 # Configuration loaded
1:M 27 Jun 2021 04:12:06.865 * Running mode=standalone, port=6379.
1:M 27 Jun 2021 04:12:06.865 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
1:M 27 Jun 2021 04:12:06.866 # Server initialized
1:M 27 Jun 2021 04:12:06.866 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
1:M 27 Jun 2021 04:12:06.866 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').
1:M 27 Jun 2021 04:12:06.867 * Module 'bf' loaded from /usr/lib/redis/modules/redisbloom.so
1:M 27 Jun 2021 04:12:06.867 * Ready to accept connections

在開啟一個窗口進入容器測試布隆過濾器

docker exec -it 57 bash

測試其基本用法:bf.add和bf.exists操作單個;bf.madd、bf.mexists 操作多個

root@5734e47e2acb:/data# redis-cli
127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehele user1
(integer) 0
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0

其他用法:

127.0.0.1:6379> bf.info codehole    # 查看布隆過濾器信息
 1) Capacity
 2) (integer) 100
 3) Size
 4) (integer) 296
 5) Number of filters
 6) (integer) 1
 7) Number of items inserted
 8) (integer) 6
 9) Expansion rate
10) (integer) 2
127.0.0.1:6379> bf.debug codehole    # 查看布隆過濾器的內部詳細信息
1) "size:6"
2) "bytes:144 bits:1152 hashes:8 hashwidth:64 capacity:100 size:6 ratio:0.005"

  上面使用的布隆過濾器是默認參數的布隆過濾器(默認error_rate 是0.01, initial_size 是100),它在第一次add的時候會自動創建。Redis 其實還提供了自定義參數的布隆過濾器,需要我們在add 之前使用bf.reserve 指令顯示的創建。如果對應的key 已經存在,bf.reserve 會報錯。

  bf.reserve 有三個參數, 分別是key、error_rate(錯誤率)、initial_size。 error_rate 越低,需要的空間越大。initial_size 表示預計放入的元素的數量,當實際數量超過這個數值時,誤判率會上升,所以需要提前設置一個比較大的數字避免超出導致誤判率升高。

例如:

127.0.0.1:6379> bf.reserve codehole2 0.08 500
OK
127.0.0.1:6379> bf.add codehole2 user0
(integer) 1
127.0.0.1:6379> bf.info codehole2
 1) Capacity
 2) (integer) 500
 3) Size
 4) (integer) 576
 5) Number of filters
 6) (integer) 1
 7) Number of items inserted
 8) (integer) 1
 9) Expansion rate
10) (integer) 2

注意:initiali_size 設置的過大,會浪費存儲空間,設置的過小會影響准確率。    error_rate 越小,需要的存儲空間就越大。

2. 布隆過濾器基本原理

每個布隆過濾器到redis 數據結構里面就是一個大型的位數組和幾個不一樣的無偏hash 函數(比如f\g\h就是這樣的hash函數)。所謂無偏函數就是能夠把元素的hash 值算得比較均勻,讓元素被hash 映射到位數組中的位置比較隨機。

 

 add過程:使用多個hash函數對key 進行函數,得到一個整數索引值,然后對位數組長度進行取模運算得到一個位置,每個hash 函數都會得到一個不同的位置。 再把數組的這幾個位置都設為1, 就完成了add 操作

exists 過程:跟add 一樣經過hash,然后取模運算得到位置判斷是否全部為1, 只要有一個為0 就是不存在;全部為1 就是存在(所以可能出現誤判的情況,不同元素的hash 算法的值可能一樣)。

3. 使用場景

1. 爬蟲系統中,對URL進行去重,已經爬過的網頁就可以不用再爬了

2. 解決緩存穿透的問題(過濾器過濾掉大量不存在的row 請求)

3. 郵箱系統的垃圾郵件過濾功能

4. GeoHash

  Redis3.2 以后增加了地理位置Geo 模塊, 意味着我們可以使用redis 來實現類似摩拜單車的"附近的Mobike"、以及"附近的餐館"這樣的功能。

1. 基本用法

1. 增加

127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin
(integer) 1
127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader
(integer) 1
127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan
(integer) 1
127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi
(integer) 2

  Geo 沒有提供刪除元素的命令,因為其內部采用的是zset 結構,所以刪除元素的時候可以直接使用zrem 指令。

127.0.0.1:6379> zrange company 0 -1 WITHSCORES
 1) "jd"
 2) "4069154033428715"
 3) "xiaomi"
 4) "4069880904286516"
 5) "ireader"
 6) "4069886008361398"
 7) "juejin"
 8) "4069887154388167"
 9) "meituan"
10) "4069887179083478"

2. 距離

geodist 指令可以用來計算兩個元素之間的距離。

127.0.0.1:6379> geodist company juejin meituan km
"1.3878"
127.0.0.1:6379> geodist company juejin meituan m
"1387.8166"
127.0.0.1:6379> geodist company juejin jd km
"24.2739"
127.0.0.1:6379> geodist company juejin jd m
"24273.9390"

3. 獲取元素位置

geopos 可以獲取元素的位置

127.0.0.1:6379> geopos company jd meituan
1) 1) "116.56210631132125854"
   2) "39.78760295130235392"
2) 1) "116.48903220891952515"
   2) "40.00766997707732031"
127.0.0.1:6379> geopos company jd
1) 1) "116.56210631132125854"
   2) "39.78760295130235392"

4. 獲取元素的hash 值

geohash 可以獲取元素的經緯度編碼字符串

127.0.0.1:6379> geohash company jd meituan
1) "wx4fk7jgtf0"
2) "wx4gdg0tx40"
127.0.0.1:6379> geohash company jd
1) "wx4fk7jgtf0"

5. 附近的公司

georadiusbymember 可以查詢指定元素附件的其他元素

# 20 公里以內最多3個元素按距離正排, 它不會排除自身
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc
1) "ireader"
2) "juejin"
3) "meituan"
# 20 公里以內最多3個元素按距離倒排
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc
1) "jd"
2) "meituan"
3) "juejin"
# 三個可選參數,withcoord\withdist\withhash 用來攜帶附加參數
127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc
1) 1) "ireader"
   2) "0.0000"
   3) (integer) 4069886008361398
   4) 1) "116.5142020583152771"
      2) "39.90540918662494363"
2) 1) "juejin"
   2) "10.5501"
   3) (integer) 4069887154388167
   4) 1) "116.48104995489120483"
      2) "39.99679348858259686"
3) 1) "meituan"
   2) "11.5748"
   3) (integer) 4069887179083478
   4) 1) "116.48903220891952515"
      2) "40.00766997707732031"
# 根據坐標值查詢附件的元素
127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc
1) 1) "ireader"
   2) "0.0000"
2) 1) "juejin"
   2) "10.5501"
3) 1) "meituan"
   2) "11.5748"

 

  注意:Redis如果使用Geo 數據,它們會將全部放在一個zset 中。如果在集群環境中,會對集群的遷移工作造成很大影響,而且可能導致先生服務的正常運行。所以建議是Geo 的數據使用單獨的redis 實例部署,不使用集群環境。

 

2. 原理

GeoHash 算法。 業界比較通用的地理位置排序算法是GeoHash 算法。 這個算法將二維的經緯度數據映射到一維的整數,這樣所有的元素都將掛載到一條線上。

在redis 里面,經緯度使用52 位的整數進行編碼,然后放進zset 里面。 value 是元素的key, score 是GeoHash 的52 位整數值。

在使用redis 進行Geo 查詢時,可以把它的內部結構理解為一個zset(skiplist)。 通過zset的score 排序就可以得到坐標附件的其他元素, 通過將score 還原成坐標值就可以得到元素的原始坐標。  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM