1. 程式人生 > 實用技巧 >Redis -基於地理位置(Geohash)

Redis -基於地理位置(Geohash)

Redis Geohash

​ Redis在3.2版本後增加了地理位置GEO模組, 意味著可以使用Redis來實現摩拜但這[附近的Mobike]、美團和餓了麼[附近的餐館]這樣的功能了。

用資料庫來算附件的人

​ 地圖元素的位置資料使用二維的經緯度表示, 經度範圍(-180, 180], 緯度範圍(-90,90],緯度正負以赤道為界, 北正南負, 經度正負以本初子午線(應該格林尼天文臺)為界, 東正西負。

​ 當兩個元素的距離不是很遠時, 可以直接使用勾股定理就能直接算得兩個元素之間的距離。我們平時使用的[附近的人]的功能, 元素距離都不是很大, 勾股定理算距離足夠。 不過需要注意的是, 經緯度座標的密度不一樣(經度總共360度, 緯度總共180度)。勾股定律計算平方差之後在求和時, 需要按一定的係數比加權求和。

​ 現在,如果要計算[附近的人],也就是給定一個元素的座標, 然後計算這個左邊附件的其他元素, 按照距離進行排序, 該楚河處理呢?

​ 如果現在元素的經緯度座標使用關係資料庫(元素id, 經度x, 緯度y) 儲存, 你該如何計算?

​ 首先, 你不能通過遍歷來計算所有的元素和目標元素的距離然後在進行排序, 這個計算量太大了,效能指標肯定無法滿足。 一般的方法都是通過矩形來限定元素的數量, 然後對區域內的元素進行權力距離計算在排序。這樣可以明顯減少計算量。如果劃分矩形區域呢? 可以指定一個半徑r, 使用一條SQL可以圈定出來。 當用戶對篩出來的結果不滿意, 那就擴大半徑繼續篩選。

select id from tables where x0-r < x < x0+r and y0-r < y < y0+r

​ 為了滿足高效能的矩形區域演算法, 資料表需要在經緯度座標加上雙向符合索引(x, y),這樣可以最大優化查詢效能。

​ 但是資料庫查詢效能畢竟優先, 如果[附近的人]查詢請求非常多, 在高併發場合, 這可能並不是一個很好的方案。

GeoHash演算法

​ 業界比較通用的地理位置距離排序演算法時GeoHash演算法, Redis也使用了GeoHash演算法。 GeoHash演算法將二維的經緯度資料對映到一維的整數, 這樣所有的元素都將掛在到一條線上, 距離靠近的二維座標對映到一維後的點之間距離也會很接近。 當我們想要計算[附近的人], 首先將目標位置對映到這條線上, 然後在這個一維的線上獲取附近的點就可以了。

​ 那這個對映演算法具體時怎麼樣的呢? 它將整個地球看出一個二維平面, 然後劃分成了一系列正方形的方格, 就比如圍棋棋盤。 所有的地圖元素座標都放置於唯一的方格中。 方格越小,座標越精確。 然後對這些方格進行整數編碼, 越是靠近的方格編碼越是解決。 那如何編碼呢? 一個最簡單的方案就是切蛋糕。 設想一個正方形蛋糕擺在你面前, 二刀下去均分成4個小正方形, 這四個小正方形可以標記為00, 01, 10, 11四個二進位制整數。 然後對每個小正方形繼續用二刀法切割下, 這個每個正方性就可以用4bit的二進位制整數表示。然後繼續切下去, 正方形就會越來越小, 二進位制整數也會越來越長, 經度就會越來越高。

​ 上面的例子中使用的是二刀法, 真實演算法中還會有其他很多的刀法, 最終編碼出來的整數數字也都不一樣。

​ 編碼之後, 每個地圖元素的座標都將變成一個整數, 通過這個整數可以還原出元素的座標, 整數越長, 還遠出來的座標值的損失程度就越小, 對於[附近的人]這個功能而言, 損失的一點精度可以忽略不計。

​ GeoHash演算法會繼續對這個整數做一次base32編碼(0-9,a-z去掉a,i, l, o四個字母)變成一個字串。在Redis裡面, 經緯度使用52位的整數進行編碼, 放進zset裡面, zset的value是元素的key, score是GeoHash的52位整數值。zset的score雖然是浮點數, 但是對於52位的整數值, 它可以是無損儲存。

​ 在使用Redis進行Geo查詢時, 我們要時刻想到它的內部結構實際上只是一個zset(skiplist)。通過zset的score排序就可以直接得到座標附近的其他元素(實際情況要複雜一些,不過可以這樣理解就足夠了),通過將score還原成座標值就可以得到元素的原始座標。

Redis的Geo指令基本使用

​ Redis提供Geo指令只有6個, 使用時, 務必再次想起, 它時一個普通的zset結構。

增加

​ geoadd指令攜帶集合名稱以及多個經緯度名稱三元組, 注意這裡可以新增多個三元組

127.0.0.1:6379> geoadd company 115.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

距離

geodist指令可以用來計算兩個元素之間的距離, 攜帶幾個名稱、2個名稱和距離單位

127.0.0.1:6379> geodist company juejin ireader km
"88.6762"
127.0.0.1:6379> geodist company juejin meituan km
"85.8899"
127.0.0.1:6379> geodist company juejin jd km
"95.1443"
127.0.0.1:6379> geodist company juejin xiaomi km
"72.7633"
127.0.0.1:6379> geodist company juejin juejin km
"0.0000"

​ 距離單位可以是m、km、ml、ft分別代表米、千米、英里和尺。

獲取元素的位置

​ geopos指令可以獲取集合中任意元素的經緯度座標,可以一次獲取多個。

127.0.0.1:6379> geopos company juejin
1) 1) "115.48104733228683472"
   2) "39.99679348858259686"
127.0.0.1:6379> geopos company ireader
1) 1) "116.5142020583152771"
   2) "39.90540918662494363"
127.0.0.1:6379> geopos company juejin ireader
1) 1) "115.48104733228683472"
   2) "39.99679348858259686"
2) 1) "116.5142020583152771"
   2) "39.90540918662494363"

​ 可以觀察到獲取的經緯度座標和geoadd進去的座標有輕微的誤差, 原因是geohash對二維座標進行的一維對映是有損的, 通過對映在還原回來的值會出現較小的差別。對於[附近的人]這種功能來說, 這點誤差根本不是事。

獲取元素的hash值

​ geohash可以獲取元素的經緯度編碼字串, 上面提到, 它是base32編碼。 可以使用這個編碼值取http://geohash.org/${hash}中進行直接定位, 它是geohash的標準編碼值。

127.0.0.1:6379> geohash company ireader
1) "wx4g52e1ce0"
127.0.0.1:6379> geohash company juejin
1) "wx45ec4wpq0"

​ 開啟地址http://geohash.org/wx4g52e1ce0,觀察地圖指向的位置是否正確。

附件的公司

georadiusbymember指令是最為關鍵的指令, 它可以用來查詢指定元素附近的其他元素, 它的引數非常複製。

# 範圍20公里以內最多2個元素按距離正排, 它不會排除自身
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc
1) "ireader"
2) "meituan"
3) "jd"

# 範圍20公里以內最多3個元素倒排
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc
1) "jd"
2) "meituan"
3) "ireader"

# 三個可選引數withcoord withdist withhash 用來攜帶附加引數
# withdist很有用, 它可以用來顯示距離
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) "meituan"
   2) "11.5748"
   3) (integer) 4069887179083478
   4) 1) "116.48903220891952515"
      2) "40.00766997707732031"
3) 1) "jd"
   2) "13.7269"
   3) (integer) 4069154033428715
   4) 1) "116.56210631132125854"
      2) "39.78760295130235392"

​ 除了georadiusbymember指令根據元素查詢附近的元素, Redis還提供了根據座標值來查詢附近的元素, 這個指令更加有用, 它可以根據使用者的定位來計算[附近的車],[附近的餐館]等。它的引數和georadiusbymember基本一致, 除了將目標元素改成經緯度座標值。

127.0.0.1:6379> georadius company 116.514203 39.905409 20 km withdist count 3 asc
1) 1) "ireader"
   2) "0.0001"
2) 1) "meituan"
   2) "11.5748"
3) 1) "jd"
   2) "13.7268"

注意事項

​ 在一個地圖應用中, 車的資料、餐館的資料、人的資料可能會有百萬千萬條, 如果使用Redis的Geo資料結構, 它們將全部放在一個zset集合中。 在Redis叢集環境中, 集合可能會從一個節點遷移到另一個節點, 如果單個key的資料過大, 會對叢集的遷移工作造成較大影響, 在叢集環境中單個key對應的資料量不宜超過1M, 否則會導致叢集遷移出現卡頓現象, 影響線上服務的正常允許。

​ 所以,這裡建議Geo的資料使用單獨的Redis例項部署,不使用叢集環境。

​ 如果資料量過億甚至更大, 就需要對Geo資料進行拆分, 按國家拆分、按省拆分、按市拆分, 在人口特大城市甚至可以按區拆分。 這個就可以顯著降低單個zset集合的大小。