Redis 高階數據類型重溫

今天這個專題接着上一篇 Redis 的基本數據類型 繼續講解剩下的高階數據類型:BitMap、HyperLogLog 和 GEO hash。這些數據結構的底層也都是基於我們前面說的 5 種 基本類型,但是實現上有很多 Redis 自己的創意。下面我們一起進入高階數據結構的世界。

BitMap

BitMap 從字面意義上能看出是一個字節組成的集合。由於一個比特位只有 0 和 1 兩種狀態,所以 BitMap 能應用的場景是有限的,只能對這種二值的場景進行使用。BitMap 底層是怎麼實現的呢?

Redis 底層只有我們上文說的 5 種基本數據類型,BitMap 並不是一種新型的數據結構,而是基於 Redis 的 string 類型的 SDS 結構來實現的。

SDS 的數據存儲在 buf 字節數組中 ,1 byte = 8 bit。那麼將這個字節數組連起來就是很多個 8 bit 組成的比特位數組。我們可以將數組的下標當做要存儲的 key,數組的值 0 或者 1 就是 value。

爲了驗證這個存儲邏輯,我們可以使用 Redis 命令行來手動調試一下:

# 首先將偏移量是0的位置設爲1
127.0.0.1:6379> setbit csx:key:1 0 1
(integer) 0
# 通過STRLEN命令,我們可以看到字符串的長度是1
127.0.0.1:6379> STRLEN csx:key:1
(integer) 1
# 將偏移量是1的位置設置爲1
127.0.0.1:6379> setbit csx:key:1 1 1
(integer) 0
# 此時字符串的長度還是爲1,以爲一個字符串有8個比特位,不需要再開闢新的內存空間
127.0.0.1:6379> STRLEN csx:key:1
(integer) 1
# 將偏移量是8的位置設置成1
127.0.0.1:6379> setbit csx:key:1 8 1
(integer) 0
# 此時字符串的長度編程2,因爲一個字節存不下9個比特位,需要再開闢一個字節的空間
127.0.0.1:6379> STRLEN csx:key:1
(integer) 2

通過上面的實驗我們可以看出,BitMap 佔用的空間,就是底層字符串佔用的空間。假如 BitMap 偏移量的最大值是 OFFSET_MAX,那麼它底層佔用的空間就是:

(OFFSET_MAX/8)+1 = 佔用字節數

因爲字符串內存只能以字節分配,所以上面的單位是字節。

但是需要注意,Redis 中字符串的最大長度是 512M,所以 BitMap 的 offset 值也是有上限的,其最大值是:

8 * 1024 * 1024 * 512  =  2^32

由於 C語言中字符串的末尾都要存儲一位分隔符,所以實際上 BitMap 的 offset 值上限是:

(8 * 1024 * 1024 * 512) -1  =  2^32 - 1

瞭解了 BitMap 的實現邏輯以及在 Redis 中的存儲邏輯之後我們對 Redis 提供的命令應該就有了更深刻的認識。

HyperLogLog

基數估計(Cardinality Estimation) 一直是大數據領域的重要問題之一。基數估計就是爲了估計在一批數據中它的不重複元素有多少個。

這個問題的應用場景非常之多,比如對於百度,谷歌這樣的大搜索引擎, 要統計每天每個地區有多少人訪問網站,在這種統計中十億和十億零三千的區別並不是很大,只需要一個近似的值即可。

如果去考慮實現,我們首先想到的就是字典。對於新來的元素,查一下是否已經屬於這個字典,不屬於則添加進去,這個字典的整體長度就是我們要的結果。方法雖然好並且還精確,但是帶來的後果就是空間複雜度會很高,數據量大的情況下,使用的空間可能超過我們的想象。

那麼是否存在一些近似的方法,可以估算出大數據的基數呢?伯努利過程可以用來做基數統計。

伯努利過程

伯努利過程就是一個拋硬幣實驗的過程。拋一枚硬幣只會有兩種結果:正面,反面,概率都是 1/2。伯努利過程就是一直拋硬幣直到出現正面爲止,並記錄下此時拋擲的次數 K。比如說拋一次硬幣就出現了正面, k = 1;或者直到第三次纔出現正面,那麼 k=3。伯努利試驗是在同樣的條件下重複地、相互獨立地進行的一種隨機試驗,其中“在相同條件下”意在說明:每一次試驗的結果不會受其它實驗結果的影響,事件之間相互獨立。

對於 n 次伯努利實驗,我們會得到 n 個出現正面的投擲次數,k1, k2,k3...kn。這其中最大的值是 kmax,接下來我們省略一大堆數學推導過程,直接來到結論的部分:

​ n = 2 kmax

即可以根據最大投擲次數來近似估計進行了幾次伯努利過程。比如:

第一次實驗:拋 3 次出現正面,k = 3,n = 23;

第二次實驗:拋 2 次出現正面,k = 2,n = 22

第三次實驗:拋 5 次出現正面,k = 5,n = 25

......

第 n 次實驗:拋 7 次出現正面,k = 7,我們根據公式可以預估 n = 27

根據上面的示例大家基本瞭解伯努利過程是什麼。在實驗次數較少的條件下(即進行伯努利過程次數少的情況)我們預估出來的 n 值肯定是有較大誤差的。比如上面的拋硬幣實驗一共就進行了 3 次就出現了正面,此時你預估 n=23 這個值可能跟再拋多次出現的值有一定的誤差。

Redis 中的 HyperLogLog 就是基於伯努利過程的原理來統計集合基數的,當然對於消除誤差, Redis 也做了一些處理。

HyperLogLog 如何模擬伯努利過程

HyperLogLog 基於 LogLog Counting 算法改進而來。 在添加元素的時候,HyperLogLog 通過 MurmurHash64A 算法給元素計算出一個 64bit 的 hash 值來模擬伯努利過程。

例如 17, hash Value = 10001,hash value 總共是64 bit 所以前面的位置都是 0。這些 byte 位就類似於一次拋硬幣的伯努利過程,0 代表拋硬幣落地式反面,1 代表拋硬幣落地式正面。LogLog Counting 的基本思想是:

  1. 對每個待測集合中的元素 x,計算它的哈希值 hash = H(x);
  2. 將哈希值 hash 通過它們的前 k=logm 個比特分組(即分桶),作爲桶的編號,即一共有 m 個桶;
  3. 將 hash 的後 L - k 個比特作爲真正參與基數估計的串 s,計算並記錄下所有桶內的 ρ(s);
  4. 令 M[k] 表示第 k 個桶內所有元素中最大的那個 ρ 值,那麼該集合基數的估計量爲:

​ Ñ = αm · m · 2ΣM[i] / m

其中 αm 是修正參數。

LogLog Counting 在誤差精簡上並沒有比它的前輩 F-M 算法小,反而還大了。但是它真正有意義的是降低了空間複雜度,F-M 算法中用 logN 個比特位來存儲 hash 值,而 LogLog Counting 算法只存儲下標(即 ρ 值)。可以降低爲 log(logN) 個比特位,再加上分桶數爲 m,所以空間複雜度爲:

​ O[m* log(logN)]

這也是 LogLog Counting 算法名字的由來。

Redis 基於節省內存空間的原因在 LogLog Counting 算法的基礎上做了一些優化,優化的目的是減少誤差和變態地更加節省內存!Hyper 是“超越”的含義,那麼HyperLogLog 到底比前輩超越在哪裏呢?

第一是分桶幾何平均數改爲計算 m 個桶的 調和平均數

調和平均數

調和平均數區別於幾何平均數概念。幾何平均數=總數/個數,但是會有一個問題:馬雲的平均年收入是100 億,我的平均年收入是 0.00000001 億,我和馬雲的平均年收入是100億,沒毛病。

使用調和平均數就能解決這個問題,調和平均數的結果會趨向於集合中比較小的數,x1 到 xn 的調和平均數符合如下公式:

根據這個公式計算我和馬雲的平均工資:

​ 平均工資 = 2 / (1/1億 + 1/0.00000001億) = 20元

這樣算起來,我還能接受!

幾何平均值受離羣值(偏離均值很大的值)的影響非常大,分桶的空桶越多,LLC 的估計值就越不準。使用調和平均值就不會受這種影響。

第二是根據基數估值的大小採用不同的估計方法進行修正。論文中給出了在常見情況:即被估集合的基數在億級別以下,m 取值在 2[4, 16] 區間下的修正的算法。

對應到 Redis 代碼實現,Redis 使用了 214 = 16384個桶,按照標準差誤差爲 0.81%,精度相當高。HLL 將 64 bit 的低 14 位拿出來作爲對應桶的序號,14 bit 可以表示 214 = 16384 個桶。剩下的 50 個比特用來做基數估計。而 26=64,所以只需要用 6 個比特表示下標值。

假設一個字符串的前 14 位是:00 0000 0000 00110,十進制 = 6,那麼該基數將會被放到編號爲 6 的桶中。在一般情況下,一個 HLL 數據結構佔用內存的大小爲16384 * 6 / 8 = 12kB,Redis將這種情況稱爲密集(dense)存儲。

爲了更高效的說清楚 Redis 的實現,我們先看 Redis 對 HLL 的定義,Redis 使用 hllhdr 來持有一個 HLL:

struct hllhdr {
    char magic[4];      /* "HYLL" */
    // 編碼格式:sparse或者dense
    uint8_t encoding;   /* HLL_DENSE or HLL_SPARSE. */
    uint8_t notused[3]; /* Reserved for future use, must be zero. */
    // 緩存的基數統計值
    uint8_t card[8];    /* Cached cardinality, little endian. */
    // 實際的寄存器們,dense有16384個6bit寄存器
    uint8_t registers[]; /* Data bytes. */
};

從上述結構體可以大致看出,Redis 實現的 HLL 結構大致分爲兩部分:

  • 頭部信息
  • 存儲桶數據的 registers

爲了減少計算的成本,Redis 的 HLL 實現使用 card 來保存基數估計的最新計算結果,card 字段採用小端存儲,用最高有效位來標識 card 內存儲的結果是否還有效。由於 Redis 的 HLL 實現使用了 16384 個分組,基於前文所述的 HLL 的標準差計算方法,Redis 的 HLL 實現的誤差是,也就是 Redis 官方文檔中公佈的誤差率。

encoding 標識當前 HLL 結構所使用的編碼方式,一共有兩種:

  • sparse(稀疏存儲)
  • dense(密集存儲)

稀疏存儲

在 HLL 初始化的時候數據量很少,如果還是使用固定位數來存儲 16384 個桶的話大量的空間就被浪費。所以 Redis 使用稀疏編碼(HLL_SPARSE)初始化一個新的 HLL 結構,它是具有壓縮性質的編碼,將重複的分組計數壓縮成操作碼(opcode),以此降低內存使用。爲了節省空間 Redis 會將連續的全 0 桶壓縮成 0 桶計數值。該計數值可以用單字節或雙字節表示,高 2bit 爲標誌位,因此可以分別表示連續 64 個全 0 桶和連續 16384 個全 0 桶。

稀疏編碼使用 ZERO、XZERO、VAL 三種操作碼對registers存儲的數據進行編碼,其中 ZERO、VAL 佔一個字節,XZERO 佔兩個字節:

多個連續桶的計數值都是零 時,Redis 提供了幾種不同的表達形式:

  • ZERO:操作碼爲00xxxxxx,前綴兩個零表示接下來的 6bit 整數值加 1 就是零值計數器的數量,注意這裏要加 1 是因爲數量如果爲零是沒有意義的。6位 xxxxxx 表示有 1 到 64(111111+1,0 沒有意義)個連續分組爲空。比如 00010101 表示連續 22 個零值計數器。
  • XZERO:操作碼錶示爲01xxxxxx yyyyyyyy,6bit 最多隻能表示連續 64 個零值計數器,這樣擴展出的 14bit 可以表示最多連續 16384 個零值計數器。這意味着 HyperLogLog 數據結構中 16384 個桶的初始狀態,所有的計數器都是零值,可以直接使用 2 個字節來表示。
  • VAL:操作碼錶示爲1vvvvvxx,中間 5bit 表示計數值,尾部 2bit 表示連續幾個桶。它的意思是連續 (xx +1) 個計數值都是 (vvvvv + 1)。比如 10101011 表示連續 4 個計數值都是 11

注意 上面第三種方式 的計數值最大隻能表示到 32,而 HLL 的密集存儲單個計數值用 6bit 表示,最大可以表示到 63當稀疏存儲的某個計數值需要調整到大於 32 時,Redis 就會立即轉換 HLL 的存儲結構,將稀疏存儲轉換成密集存儲。

密集存儲

HLL 的密集編碼(HLL_DENSE)結構是由稀疏編碼轉換而來的,它的結構相對簡單,由連續 16384 個桶拼接而成,每個組佔用 6 位(16384×6/8,12KB的由來)。不過由於存儲採用的是小端模式,所以每個桶的 6bit 計數值是從 LSB(低位) 開始一個接一個地編碼到 MSB(高位)(區別於大端模式即正常的數字是從左到右依次是高位到低位,小端模式是從左到右依次是低位到高位)。

密集存儲涉及到一個定位字節的問題,因爲一個桶是 6bit,而一個字節是 8bit,那麼必然會涉及到一個字節持有兩個桶的情況。這種情況下給定一個桶的編號如何定位到對應的桶呢?

假設桶的編號爲 idx,這個 6bit 計數值的起始字節位置偏移用 offset_bytes 表示,它在這個字節的起始比特位置偏移用 offset_bits 表示。我們有:

offset_bytes = (idx * 6) / 8offset_bits = (idx * 6) % 8

前者是商,後者是餘數。比如 bucket 2 的字節偏移是 1,也就是第 2 個字節。它的位偏移是4,也就是第 2 個字節的第 5 個位開始是 bucket 2 的計數值。需要注意的是字節位序是左邊低位右邊高位。

如果 offset_bits 小於等於 2,那麼這 6bit 在一個字節內部,可以直接使用下面的表達式得到計數值 val

val = buffer[offset_bytes] >> offset_bits  # 向右移位

如果 offset_bits 大於 2,那麼就會跨越字節邊界,這時需要拼接兩個字節的位片段。

低位值 low_val = buffer[offset_bytes] >> offset_bits
低位個數 low_bits = 8 - offset_bits 
拼接,保留低6位 val = (high_val << low_bits | low_val) & 0b111111

總體來說,HLL 存儲結構將內存使用優化到極致,以 12k 的空間來估算最多 264 個不同元素的基數。並且優化了基數結果的誤差範圍爲 0.81 %。如果我們有海量數據查詢的需求,可以考慮使用 HLL。

GEO

GEO 算法是一種地理位置距離排序算法,將二維的經緯度數據映射到一維的整數上。這樣所有的元素都將掛載到一條線上,距離靠近的二維座標映射到一維後的點之間距離會很接近。當我們想要計算附近的人時,首先將目標位置映射到這條線上,然後在這條一維的線上獲取附近的點就行。

對應到我們的地球經緯度信息來說,緯度區間爲[-90, 90],經度區間爲 [-180,180]。如何將二維的座標轉換爲一維的值呢?下面我們一起演示一下當前的處理手段。首先我們把地球經緯度座標展開爲一張平面:

首先我們將整幅地圖分割成四個小塊,然後用簡單的 01 編碼來標識每個小塊:

這樣整個地圖被我們分割爲 4 個空間,每個空間用一維的 01 來標識。如果 地點 A 坐落於 00 空間,那麼它的一維標識就是 00。當然你會說 這麼大一塊空間都用 00 標識這範圍也太大了吧。既然我們可以分割爲 4 塊,你是不是順推就知道我們可以繼續分割爲 8,16,100,甚至更大都可以,這樣我們每一塊空間所標識的範圍更精確,搜索地理位置也更精確。

根據這個原理我們可以通過最小逼近的準則來用一維字符串表達相似準確的最小位置。比如北京市區座標爲:北緯39.9”,東經116. 3”,那麼我們可以按照如下算法對北京的經緯度進行逼近編碼:

  1. 緯度區間爲[-90, 90],一分爲二爲[-90, 0),[0 , 90],稱爲左右區間,北京在北緯 39.9,那麼在右區間,給標記爲 1;
  2. 接着將區間[0, 90] 二分爲[0, 45), [45, 90],可以確定 39.9 在左區間,標記爲 0;
  3. 遞歸上述過程,可以確定 39.9 總是屬於某個細小的區間 [x, y],隨着迭代的越多,那麼這個區間會 越來越逼近 39.9 ;
  4. 通過對緯度這樣劃分,屬於左區間記錄 0 ,屬於右區間記錄 1,那麼最終會得到一個關於緯度的 01 編碼,編碼長度取決於區間劃分的粒度。
  5. 同理經度也是如此劃分。

假設 經過上述計算,緯度產生的編碼爲 1001000101,經度產生的編碼爲 0100100101。我們把經緯度組合成一個串,這個串的偶數位放經度,奇數位放緯度,把兩串編碼組合成一個新串:

​ 01100001100000110011

最後使用用 0-9、b-z(去掉a, i, l, o)這 32 個字母進行 base32 編碼,首先將 01100 00110 00001 10011 轉成十進制,對應着 12、6、1、19 十進制對應的編碼就是:MGBT。同理,將編碼轉換成經緯度的解碼算法與之相反。

​ RFC 4648 Base32 字母表

符號 符號 符號 符號
0 A 8 I 16 Q 24 Y
1 B 9 J 17 R 25 Z
2 C 10 K 18 S 26 2
3 D 11 L 19 T 27 3
4 E 12 M 20 U 28 4
5 F 13 N 21 V 29 5
6 G 14 O 22 W 30 6
7 H 15 P 23 X 31 7
填充 =

通過這種加密之後我們就把經緯度轉換爲很短的幾個字母來表示。

空間填充曲線

上面我們將空間劃分爲很多個小範圍之後,我們是按照範圍從大到小進行逼近,那麼對應到地圖逼近的順序是什麼呢?這裏就提到了空間曲線的概念,即我們從哪裏開始遍歷,遍歷的順序又是如何。

在數學分析中,有這樣一個難題:能否用一條無限長的線,穿過任意維度空間裏面的所有點? 常見的有: Z階曲線(Z-order curve)、皮亞諾曲線(Peano curve)、希爾伯特曲線(Hilbert curve),之後還有很多變種的空間填充曲線,龍曲線(Dragon curve)、 高斯帕曲線(Gosper curve)、Koch曲線(Koch curve)、摩爾定律曲線(Moore curve)、謝爾賓斯基曲線(Sierpiński curve)、奧斯古德曲線(Osgood curve)等。

Z 階曲線(Z-order curve)是 GEO hash 目前再用的空間填充曲線:

這個曲線比較簡單,生成也比較容易,秩序要把每個 Z 首位相連即可。 Geohash 能夠提供任意精度的分段級別。一般分級從 1-12 級。利用 Geohash 的字符串長短來決定要劃分區域的大小,一旦選定 cell 的寬和高,那麼 Geohash 字符串的長度就確定下來了。地圖上雖然把區域劃分好了,但是還有一個問題沒有解決,那就是如何快速的查找一個點附近鄰近的點和區域呢?

Geohash 有一個和 Z 階曲線相關的性質,那就是一個點附近的地方(但不絕對) hash 字符串總是有公共前綴,並且公共前綴的長度越長,這兩個點距離越近。由於這個特性,Geohash 就常常被用來作爲唯一標識符。用在數據庫裏面可用 Geohash 來表示一個點。Geohash 這個公共前綴的特性就可以用來快速的進行鄰近點的搜索。越接近的點通常和目標點的 Geohash 字符串公共前綴越長(特殊情況除外)。

前面說 Geohash 編碼的時候提到 Geohash 碼生成規劃: “偶數位放經度,奇數位放緯度”。這個規則就是 Z 階曲線。看下圖:

x 軸就是緯度,y 軸就是經度。經度放偶數位,緯度放奇數位就是這樣而來的。

但是 Z 階曲線有個問題:它的突變性問題會導致某些編碼位置相鄰但是距離缺很遠。比如上圖中的 001000 和 000111,編碼相鄰,距離卻很大。

除了 Z 階曲線外,工人效果最好的是 Hilbert Curve 希爾伯特曲線, 希爾伯特曲線是一種能填充滿一個平面正方形的分形曲線(空間填充曲線),由大衛·希爾伯特在 1891 年提出。由於它能填滿平面,它的豪斯多夫維是2。取它填充的正方形的邊長爲 1,第 n 步的希爾伯特曲線的長度是 2n - 2(-n)

一階的希爾伯特曲線,生成方法就是把正方形四等分,從其中一個子正方形的中心開始,依次穿線,穿過其餘 3 個正方形的中心。如下圖:

二階的希爾伯特曲線,生成方法就是把之前每個子正方形繼續四等分,每 4 個小的正方形先生成一階希爾伯特曲線。然後把 4 個一階的希爾伯特曲線首尾相連。

n 階的希爾伯特曲線 的生成方法也是遞歸的,先生成 n-1 階的希爾伯特曲線,然後把 4 個 n-1 階的希爾伯特曲線首尾相連。

希爾伯特曲線空間曲線的優點在於對多維空間有效的降維。如下圖就是希爾伯特曲線在填滿一個平面以後,把平面上的點都展開成一維的線了。 另外希爾伯特曲線是連續的,所以能保證一定可以填滿空間。

Redis 沒有選擇效果更好的希爾伯特曲線來實現低位位置劃分而是使用 Z 階曲線這個官方並沒有說明,大概只是 Z 階曲線的實現相對來說要簡單一些。另外關於 Z 階曲線邊界點距離問題,Redis 也有修正,除了使用定位點的 GEOhash 編碼之外,目前比較通行的做法就是我們不僅獲取當前我們所在的矩形區域,還獲取周圍 8 個矩形塊中的點。那麼怎樣定位周圍 8 個點呢?關鍵就是需要獲取周圍 8 個點的經緯度,那我們已經知道自己的經緯度,只需要用自己的經緯度減去最小劃分單位的經緯度就行。因爲我們知道經緯度的範圍,有知道需要劃分的次數,所以很容易就能計算出最小劃分單位的經緯度。

通過上面這張圖,我們就能很容易的計算出周圍 8 個點的經緯度,有了經緯度就能定位到周圍 8 個矩形塊。這樣我們就能獲取包括自己所在矩形塊的 9 個矩形塊中的所有的點。最後分別計算這些點和自己的距離(由於範圍很小,點的數量就也很少,計算量就很少)過濾掉不滿足條件的點就行。

在 Redis 中,查找某個中心地理位置(longitude,latitude)周圍 radius 範圍內的點,先根據 latitude 和 radius 計算出經緯度二進制編碼的長度 step,即劃分的次數,然後根據中心地理位置(longitude,latitude)、radius、step 計算出中心位置的二進制編碼,再計算出周邊 8 個位置塊的二進制編碼,再依次計算 9 個位置塊中的點是否滿足距離要求。

GEOhash 在 Redis 中的實現方式如下:

typedef struct {
    double min;
    double max;
} GeoHashRange;
typedef struct {
    uint64_t bits;
    uint8_t step;
} GeoHashBits;

int geohashEncode(const GeoHashRange *long_range, const GeoHashRange *lat_range,
              double longitude, double latitude, uint8_t step,
              GeoHashBits *hash) {
    /* Check basic arguments sanity. */
    if (hash == NULL || step > 32 || step == 0 ||
        RANGEPISZERO(lat_range) || RANGEPISZERO(long_range)) return 0;

    /* Return an error when trying to index outside the supported
     * constraints. */
    if (longitude > 180 || longitude < -180 ||
        latitude > 85.05112878 || latitude < -85.05112878) return 0;

    hash->bits = 0;
    hash->step = step;

    if (latitude < lat_range->min || latitude > lat_range->max ||
        longitude < long_range->min || longitude > long_range->max) {
        return 0;
    }

    double lat_offset =
        (latitude - lat_range->min) / (lat_range->max - lat_range->min);
    double long_offset =
        (longitude - long_range->min) / (long_range->max - long_range->min);

    /* convert to fixed point based on the step size */
    lat_offset *= (1ULL << step);
    long_offset *= (1ULL << step);
    hash->bits = interleave64(lat_offset, long_offset);
    return 1;
}

long_range 和 lat_range 爲地球經緯度的範圍;hash->bits 用戶保存最終二進制編碼結果,hash->step 是經緯度劃分的次數,在 Redis 中該值爲 26,即經度/緯度的二進制編碼長度爲 26,最終經交叉組合而成的地理位置的二進制編碼爲 52 位。Base32 編碼爲每 5bits 組成一個字符,所以最終的 GeoHash 字符串爲 11 位。

爲什麼是 52 位?因爲在 Redis 中是把地理位置編碼後的二進制值存入 zset 數據結構中,double 類型的尾數部分長度爲 52 位。

上述算法在分別計算經度/緯度的二進制編碼時,是先計算所求地理位置的經度/緯度在整個經度/緯度範圍內的偏移,再乘以 2 的 step 次方。劃分 step 次,會得到 2 的 step 次方個區域,區域值爲 0 -(1<<step-1),所以偏移乘以 2 的 step 次方即可得到劃分 step 次時改經度/緯度的二進制編碼。

Redis 中處理這些地理位置座標點的思想是: 二維平面座標點 --> 一維整數編碼值 --> zset(score爲編碼值) --> zrangebyrank(獲取score相近的元素)、zrangebyscore --> 通過score(整數編碼值)反解座標點 --> 附近點的地理位置座標。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章