如何“搖一搖”——Redis實現(xiàn)“附近的人”的方法及其他

1、GEOHash——如何“搖一搖”

1-1、為什么需要GEOHash

現(xiàn)在很多APP都有“搖一搖”、“附近的人”、網(wǎng)約車離我有多遠等類似的功能。

那就不可避免需要進行一系列的地理坐標轉換為距離的計算。

獲取地理坐標不難,只需要用戶授權就能輕松獲取到很精確的經(jīng)緯度;

計算距離也不難:兩點之間坐標差的平方相加,再開方即可(簡書的markdown太差了,公式也不支持)。

問題在于,用戶數(shù)量劇增以后(百萬、千萬、億……),每次進行全量距離計算本身就變成了一個巨大的負擔,大量乘方、開方這樣的浮點運算,不管多么強壯的算力都會被壓垮。

1-2、什么是GEOHash

GEOHash是解決這一問題的一種算法。

GEOHash的核心思想是將二維的經(jīng)緯度轉換成一維的字符串,這樣無論使用何種方式存儲用戶的位置數(shù)據(jù),都可以很方便地建立索引,大大簡化計算。

GEOHash有三個主要特點:

  1. 字符串越長,表示的范圍越精確。編碼長度為8時,精度在19米左右,而當編碼長度為9時,精度在2米左右。

  2. 字符串相似的表示距離相近,利用字符串的前綴匹配,可以查詢附近的地理位置。

  3. GEOHash編碼后的字符串,可以反向解碼出原來的經(jīng)緯度。

1-3、GEOHash編碼的簡易原理

GEOHash的編碼過程并不復雜,首先使用分形的思想對經(jīng)緯度做處理(二維空間轉換為分形維),轉為二進制字符串;然后偶數(shù)位放經(jīng)度,奇數(shù)位放緯度,得到一個新的二進制字符串;最后對該二進制字符串做Base32編碼處理即可(或者使用Base64)。

但是“為什么”要這樣編碼,就涉及到計算機圖形學的一些思想(GEOHash應用了Peano曲線),本篇側重應用,原理的具體說明此處略去。

1-4、GEOHash的邊緣問題

GEOHash編碼時,實際上就是把所有區(qū)域分割成大小相同的矩形塊(分得越細,字符串就越長,精度越精確)。

但是不管分割得怎么細,都是一個矩形區(qū)域,會出現(xiàn)區(qū)域內(nèi)靠近邊緣的某個點,與另一片區(qū)域中靠近邊緣的點離得更近,但是被誤判的情況。

畢竟同一片區(qū)域內(nèi)的hash碼是相同的,不管離得多遠,都會被判定為離得最近。

解決方法是在查詢時,除了使用定位點的GeoHash碼進行匹配外,還要使用周圍8個區(qū)域的GeoHash編碼一并匹配。

2、Redis中GEOHash的應用

Redis 從3.2版本開始,追加了GEO數(shù)據(jù)類型用于存儲用戶信息,并且提供了各種用于地理計算的方法。

(BTW,對Redis GEO貢獻最大的是其實是國內(nèi)的開發(fā)者。也是因為我們對這個功能的需求最大)

下面是相關的全部命令列表:

GEOADD:增加地理位置的坐標(支持批量操作)

時間復雜度O(log(N))

GEOADD key longitude latitude member [longitude latitude member ...]

  • key標識一個地理位置的集合。
  • longitude是經(jīng)度,latitude是緯度。
  • member是該地理位置的名稱(自定義)。
應用舉例:

geoadd cities 121.47 31.23 shanghai 

geoadd cities 116.40 39.90 beijing 118.78 32.07 nanjing
GEOPOS:獲取地理位置的坐標(支持批量操作)

時間復雜度O(log(N))

GEOPOS key member [member ...]

GEODIST:獲取兩個地理位置的距離

時間復雜度O(log(N))

GEODIST key member1 member2 [m|km|ft|mi]

單位可以指定為以下四種類型:

  • m:米,距離單位默認為米,不傳遞該參數(shù)則單位為米
  • km:公里
  • mi:英里
  • ft:英尺
應用舉例:
geodist cities beijing shanghai
GEORADIUS:根據(jù)給定地理位置坐標獲取指定范圍內(nèi)的地理位置集合

時間復雜度O(N+log(M)),N為指定半徑范圍內(nèi)的元素個數(shù),M為要返回的個數(shù)

GEORADIUS key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]

  • longitude是經(jīng)度,latitude是緯度。
  • radius表示范圍距離。
  • 距離單位可以為m|km|ft|mi
  • WITHCOORD:傳入WITHCOORD參數(shù),則返回結果會帶上匹配位置的經(jīng)緯度。
  • WITHDIST:傳入WITHDIST參數(shù),則返回結果會帶上匹配位置與給定地理位置的距離。
  • ASC|DESC:默認結果是未排序的,傳入ASC為從近到遠排序,傳入DESC為從遠到近排序。
  • WITHHASH:傳入WITHHASH參數(shù),則返回結果會帶上匹配位置的hash值。
  • COUNT count:傳入COUNT參數(shù),可以返回指定數(shù)量的結果。
GEORADIUSBYMEMBER:根據(jù)給定地理位置獲取指定范圍內(nèi)的地理位置集合

時間復雜度O(N+log(M)),N為指定半徑范圍內(nèi)的元素個數(shù),M為要返回的個數(shù)

GEORADIUSBYMEMBER key member radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]

GEORADIUS 命令的參數(shù)是坐標,而本命令的參數(shù)是地理位置名稱,其他無區(qū)別。

應用舉例:
georadiusbymember cities shanghai 50 km
GEOHASH:獲取地理位置的GEOHash值(支持批量操作)

時間復雜度O(log(N))

GEOHASH key member [member ...]

3、關于Redis中GEO的一些說明

redis使用的GEOHash編碼長度為26位,可以精確到0.59m的精度。

Redis中的GEO核心主要是兩點:

  1. 使用GEOHash保存地理位置的坐標
  2. 使用有序集合(ZSET)保存地理位置的集合

內(nèi)部的實際存儲使用的是ZSET,進一步說明如下:

  • GEO命令中的key就是ZSET的名字

  • Redis沒有提供地理位置的刪除命令??梢岳肸SET的ZREM命令進行刪除: zrem cities shanghai

  • 執(zhí)行GEOADD命令時,會先按照標準的GEOHash計算方法計算hash值,然后將地理位置名稱(member)作為集合的member,將GEOHash值作為score,執(zhí)行ZADD命令插入到ZSET中

  • GEORADIUS和GEORADIUSBYMEMBER。內(nèi)部實現(xiàn)實際上就是先執(zhí)行ZRANGE命令,然后判斷結果是否符合命令中指定的距離即可。另外就是這兩個命令已經(jīng)自行解決了GEOHash的“邊緣問題”,會自動關聯(lián)周圍的八片區(qū)域,無需額外處理。

4、Redis以外的GEOHash實現(xiàn)

4-1、MySQL

MySQL從 5.7.4 labs 版本開始增加了對于空間索引的支持,通過R樹實現(xiàn)。

如果不具備升級MySQL的條件,就需要增加類似 geo_hash 的字段,追加地理位置時手動計算hash碼,搜索附近的人時手動解決GEOHash的邊緣問題,從算力和復雜度上來說都不能令人滿意。

4-2、MongoDB + 2d索引

MongoDB主要是通過它的兩種地理空間索引 2dsphere 和 2d來實現(xiàn)地理位置的操作。

兩種索引的底層依然是基于Geohash,但與通用的Geohash還有一些不同。

2dsphere 索引僅支持球形表面的幾何形狀查詢。

2d 索引支持平面幾何形狀和一些球形查詢。雖然2d 索引支持某些球形查詢,但 2d 索引對這些球形查詢時,可能會出錯。所以球形查詢盡量選擇 2dsphere索引。

盡管兩種索引的方式不同,但只要坐標跨度不太大,計算出的距離誤差幾乎可以忽略不計。

對經(jīng)緯度數(shù)據(jù)創(chuàng)建2d索引(db.coll.createIndex)以后,只要使用geoNear命令就可以實現(xiàn)附近的人的功能了。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容