針對「附近的人」這一位置服務領域的應用場景,常見的可以使用PG、
MySQL和MongoDB等多種DB的空間索引進行實現。
而Redis另闢蹊徑,結合其有序隊列zset以及geohash編碼,實現了空間搜索功能,且擁有極高的運行效率。
本文將從源碼角度對其算法原理進行解析,並推算查詢時間複雜度。
要提供完整的「附近的人」服務,最基本的是要實現「增」、「刪」、「查」的功能。
如下將分別進行介紹,其中會重點對查詢功能進行解析。
操做命令
自Redis 3.2開始,Redis基於geohash和有序集合提供了地理位置相關功能。Redis Geo模塊包含了如下6個命令:
其中,組合使用GEOADD和GEORADIUS可實現「附近的人」中「增」和「查」的基本功能。
要實現微信中「附近的人」功能,可直接使用GEORADIUSBYMEMBER命令。其中「給定的位置對象」即爲用戶本人,搜索的對象爲其餘用戶。
不過本質上,GEORADIUSBYMEMBER = GEOPOS + GEORADIUS,即先查找用戶位置再經過該位置搜索附近知足位置相互距離條件的其餘用戶對象。
如下會從源碼角度入手對GEOADD和GEORADIUS命令進行分析,剖析其算法原理。
Redis geo操做中只包含了「增」和「查」的操做,並無專門的「刪除」命令。主要是由於Redis內部使用有序集合(zset)保存位置對象,可用zrem進行刪除。
在Redis源碼geo.c的文件註釋中,只說明瞭該文件爲GEOADD、GEORADIUS和GEORADIUSBYMEMBER的實現文件(其實在也實現了另三個命令)。從側面看出其餘三個命令爲輔助命令。
GEOADD
使用方式
將給定的位置對象(緯度、經度、名字)添加到指定的key。
其中,key爲集合名稱,member爲該經緯度所對應的對象。在實際運用中,當所需存儲的對象數量過多時,可經過設置多key(如一個省一個key)的方式對對象集合變相作sharding,避免單集合數量過多。
成功插入後的返回值:
其中N爲成功插入的個數。
源碼分析
經過源碼分析能夠看出Redis內部使用有序集合(zset)保存位置對象,有序集合中每一個元素都是一個帶位置的對象,元素的score值爲其經緯度對應的52位的geohash值。
double類型精度爲52位;
geohash是以base32的方式編碼,52bits最高可存儲10位geohash值,對應地理區域大小爲0.6*0.6米的格子。換句話說經Redis geo轉換過的位置理論上會有約0.3*1.414=0.424米的偏差。
算法小結
簡單總結下GEOADD命令都幹了啥:
一、參數提取和校驗;
二、將入參經緯度轉換爲52位的geohash值(score);
三、調用ZADD命令將member及其對應的score存入集合key中。
GEORADIUS
使用方式
範圍單位:m | km | ft | mi --> 米 | 公里 | 英尺 | 英里
額外參數:
- WITHDIST:在返回位置對象的同時,將位置對象與中心之間的距離也一併返回。距離的單位和用戶給定的範圍單位保持一致。
- WITHCOORD:將位置對象的經度和維度也一併返回。
- WITHHASH:以 52 位有符號整數的形式,返回位置對象通過原始 geohash 編碼的有序集合分值。這個選項主要用於底層應用或者調試,實際中的做用並不大。
- ASC|DESC:從近到遠返回位置對象元素 | 從遠到近返回位置對象元素。- COUNT count:選取前N個匹配位置對象元素。(不設置則返回全部元素)
- STORE key:將返回結果的地理位置信息保存到指定key。- STORedisT key:將返回結果離中心點的距離保存到指定key。
關注微信公衆號:Java技術棧,在後臺回覆:redis,能夠獲取我整理的 N 篇最新 Redis 教程,都是乾貨。
因爲 STORE 和 STORedisT 兩個選項的存在,GEORADIUS 和 GEORADIUSBYMEMBER 命令在技術上會被標記爲寫入命令,從而只會查詢(寫入)主實例,QPS太高時容易形成主實例讀寫壓力過大。
爲解決這個問題,在 Redis 3.2.10 和 Redis 4.0.0 中,分別新增了 GEORADIUS_RO 和 GEORADIUSBYMEMBER_RO兩個只讀命令。
不過,在實際開發中筆者發現 在java package
Redis.clients.jedis.params.geo
的 GeoRadiusParam 參數類中並不包含 STORE 和 STORedisT 兩個參數選項,在調用georadius時是否真的只查詢了主實例,仍是進行了只讀封裝。感興趣的朋友能夠本身研究下。
成功查詢後的返回值:
不帶WITH限定,返回一個member list,如:
帶WITH限定,member list中每一個member也是一個嵌套list,如:
源碼分析
此段源碼較長,看不下去的可直接看中文註釋,或直接跳到小結部分
對應的是
geohashGetAreasByRadiusWGS84
和
membersOfAllNeighbors
兩個函數。
咱們依次來看:
// geohash_helper.c
// geo.c
算法小結
拋開衆多可選參數不談,簡單總結下GEORADIUS命令是怎麼利用geohash獲取目標位置對象的:
一、參數提取和校驗;
二、利用中心點和輸入半徑計算待查區域範圍。這個範圍參數包括知足條件的最高的geohash網格等級(精度) 以及 對應的可以覆蓋目標區域的九宮格位置;(後續會有詳細說明)
三、對九宮格進行遍歷,根據每一個geohash網格的範圍框選出位置對象。進一步找出與中心點距離小於輸入半徑的對象,進行返回。
直接描述不太好理解,咱們經過以下兩張圖在對算法進行簡單的演示:
令左圖的中心爲搜索中心,綠色圓形區域爲目標區域,全部點爲待搜索的位置對象,紅色點則爲知足條件的位置對象。
在實際搜索時,首先會根據搜索半徑計算geohash網格等級(即右圖中網格大小等級),並肯定九宮格位置(即紅色九宮格位置信息);再依次查找計算九宮格中的點(藍點和紅點)與中心點的距離,最終篩選出距離範圍內的點(紅點)。
算法分析
爲何要用這種算法策略進行查詢,或者說這種策略的優點在哪,讓咱們以問答的方式進行分析說明。
這實際上是一個問題,本質上是對全部的元素對象進行了一次初步篩選。 在多層geohash網格中,每一個低等級的geohash網格都是由4個高一級的網格拼接而成(如圖)。
換句話說,geohash網格等級越高,所覆蓋的地理位置範圍就越小。當咱們根據輸入半徑和中心點位置計算出的可以覆蓋目標區域的最高等級的九宮格(網格)時,就已經對九宮格外的元素進行了篩除。
這裏之因此使用九宮格,而不用單個網格,主要緣由仍是爲了不邊界狀況,儘量縮小查詢區域範圍。試想以0經緯度爲中心,就算查1米範圍,單個網格覆蓋的話也得查整個地球區域。而向四周八個方向擴展一圈可有效避免這個問題。
如何經過geohash網格的範圍框選出元素對象?效率如何?
首先在每一個geohash網格中的geohash值都是連續的,有固定範圍。因此只要找出有序集合中,處在該範圍的位置對象便可。如下是有序集合的跳錶數據結構:
其擁有相似二叉查找樹的查詢效率,操做平均時間複雜性爲O(log(N))。且最底層的全部元素都以鏈表的形式按序排列。
因此在查詢時,只要找到集合中處在目標geohash網格中的第一個值,後續依次對比便可,不用屢次查找。
九宮格不能一塊兒查,要一個個遍歷的緣由也在於九宮格各網格對應的geohash值不具備連續性。只有連續了,查詢效率纔會高,否則要多作許多距離運算。
綜上,咱們從源碼角度解析了Redis Geo模塊中 「增(GEOADD)」 和 「查(GEORADIUS)」 的詳細過程。並可推算出Redis中GEORADIUS查找附近的人功能,時間複雜度爲:O(N+log(M))
其中N爲指定半徑範圍內的位置元素數量,而M則是被九宮格圈住計算距離的元素的數量。結合Redis自己基於內存的存儲特性,在實際使用過程當中有很是高的運行效率。
來源:餓了麼物流團隊javascript
https://juejin.im/post/5da40462f265da5baf410a11java
- END -
關注
Java技術棧
公衆號在後臺回覆:
redis
,可獲取一份棧長整理的最新 Redis 教程,都是乾貨。
