1 前言
移動網際網路已融入到我們生活中的方方面面。
我們平時找商家、找房子、找車都可以通過各種App來完成。作為??的筆者職業習慣性地思考這些功能是如何實現的呢?
例如尋找附近3公里範圍內的計程車的需求,最直觀的想法就是去資料庫裡面查表篩選出距離使用者小於3公里的車輛,將資料返回給客戶端。
這種方法有一個很嚴重的問題,需要對整張表裡面的每一項都計算一次相對距離太耗時了。既然整張表資料量比較大那麼我們能不能縮小掃描的範圍呢?那麼就會想到是否可以按業務特點縮小掃描範圍比如只掃描使用者當前位置所在城市的車輛,按照這個思路擴充套件開來發現資料量還是很大而且不能解決當使用者處於兩個城市的邊界時的問題。
如何快速地索引資料是解決這個問題的關鍵,在瀏覽Redis API的時候發現其可以直接實現附近的XXX功能,下文中將介紹如何以Redis 實現此類功能並深入分析其背後的實現原理。
2 Redis GEO API
2.1 增加地理位置資訊
geo add key longitude latitude member [longitude latitude member ...]
複製程式碼
eg:
向cars:locations中增加車輛編號為1以及車輛編號為2的位置資訊。
127.0.0.1:6379> geoadd cars:locations 120.346111 31.556381 1 120.375821 31.560368 2
複製程式碼
2.2 獲取地理位置資訊
eg:
獲取車輛編號為1的車輛位置資訊
127.0.0.1:6379> geopos cars:locations 1
1) 1) "120.34611314535140991"
2) "31.55637987511895659"
複製程式碼
2.3 獲取兩個地理位置的距離
eg:
獲取編號為1的車輛與編號為2的車輛之間的距離
127.0.0.1:6379> geodist cars:locations 1 2 km
"2.8504"
複製程式碼
2.4 獲取指定位置範圍的地理資訊位置集合
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
複製程式碼
以給定的經緯度為中心, 返回鍵包含的位置元素當中, 與中心的距離不超過給定最大距離的所有位置元素。
eg:
以經度120.375821緯度31.556381為中心查詢5公里範圍內的車輛
127.0.0.1:6379> GEORADIUS cars:locations 120.375821 31.556381 5 km WITHCOORD WITHDIST WITHHASH ASC COUNT 100
1) 1) "2"
2) "0.4433"
3) (integer) 4054421167795118
4) 1) "120.37582129240036011"
2) "31.5603669915025975"
2) 1) "1"
2) "2.8157"
3) (integer) 4054421060663027
4) 1) "120.34611314535140991"
2) "31.55637987511895659"
複製程式碼
以給定的經緯度為中心, 返回鍵包含的位置元素當中, 與中心的距離不超過給定最大距離的所有位置元素。
範圍可以使用以下其中一個單位:
- m 表示單位為米。
- km 表示單位為千米。
- mi 表示單位為英里。
- ft 表示單位為英尺。
在給定以下可選項時, 命令會返回額外的資訊:
-
WITHDIST : 在返回位置元素的同時, 將位置元素與中心之間的距離也一併返回。 距離的單位和使用者給定的範圍單位保持一致。
-
WITHCOORD : 將位置元素的經度和維度也一併返回。
-
WITHHASH : 以 52 位有符號整數的形式, 返回位置元素經過原始 geohash 編碼的有序集合分值。 這個選項主要用於底層應用或者除錯, 實際中的作用並不大。 命令預設返回未排序的位置元素。 通過以下兩個引數, 使用者可以指定被返回位置元素的排序方式:
-
ASC : 根據中心的位置, 按照從近到遠的方式返回位置元素。DESC : 根據中心的位置, 按照從遠到近的方式返回位置元素。
-
在預設情況下, GEORADIUS 命令會返回所有匹配的位置元素。 雖然使用者可以使用 COUNT 選項去獲取前 N 個匹配元素, 但是因為命令在內部可能會需要對所有被匹配的元素進行處理, 所以在對一個非常大的區域進行搜尋時, 即使只使用 COUNT 選項去獲取少量元素, 命令的執行速度也可能會非常慢。 但是從另一方面來說, 使用 COUNT 選項去減少需要返回的元素數量, 對於減少頻寬來說仍然是非常有用的。
2.5 獲取指定元素範圍的地理資訊位置集合
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
複製程式碼
eg:
以編號為1的車輛為中心查詢5公里範圍內的車輛 GEORA
127.0.0.1:6379> GEORADIUSBYMEMBER cars:locations 2 5 km WITHCOORD WITHDIST WITHHASH ASC COUNT 100
1) 1) "2"
2) "0.0000"
3) (integer) 4054421167795118
4) 1) "120.37582129240036011"
2) "31.5603669915025975"
2) 1) "1"
2) "2.8157"
3) (integer) 4054421060663027
4) 1) "120.34611314535140991"
2) "31.55637987511895659"
複製程式碼
相關可選引數同2.4中一致。
3 Redis GEO實現附近XXX功能
研究完Redis GEO API後可以發現只要在Redis客戶端呼叫
2.4 獲取指定位置範圍的地理資訊位置集合
API 即可實現相關需求。so easy!!!
4 Redis GEO背後的原理
4.1 儲存結構
Redis 在儲存資料不同資料型別的資料時都有對應的編碼方式。 Redis GEO是採用哪種編碼方式進行儲存的呢?
在翻閱Redis GEO API時發現其並沒有刪除指令,因為其底層是使用zset進行實現的。 我們可以使用zrem 進行資料的刪除。
再嘗試用zset的查詢指令,查詢上文中新增的GEO資訊
127.0.0.1:6379> ZRANGE cars:locations 0 -1 WITHSCORES
1) "1"
2) "4054421060663027"
3) "2"
4) "4054421167795118"
複製程式碼
發現車輛編號為1的位置資訊為4054421060663027;車輛編號為2的位置資訊為4054421167795118。 再回顧一下zset增加成員的指令
ZADD key score member [[score member] [score member] ...]
複製程式碼
至此可以推斷出Redis GEO 新增經、緯度位置資訊的指令的過程是
ZADD cars:locations 4054421060663027 1
複製程式碼
4054421060663027為對經緯度進行編碼後的值。使用4054421060663027作為score 可以快速實現對經緯度的索引。
檢視相關文件發現Redis使用了geohash對經緯度資訊進行的編碼。
4.2 geohash原理分析
關於geohash的核心原理,這篇文章分析的很好 GeoHash核心原理解析
總結下來就是
- 如何唯一表示地球上的一塊空間?
- 如何將地球切分成大小近似的區塊,並支援不同粒度的表示?
為了解決上述兩個問題,我們需要三個步驟。
- 第一步,將三維地球變成二維;
- 第二步,將二維再轉成一維;
- 最後一步,將一維表示成二進位制碼儲存。
4.2.1 如何將三維變二維?
地球緯度區間是[-90,90],經度區間是[-180,180]。 將它展開想象長一個矩形為
4.2.2 如何將二維變一維?
通過剛才的方法,我們能夠將地球的表面轉換成二維空間的平面。那接下來要將二維轉變成一維。如果切割二維空間,可以切割出很多正方形。如何表示這個正方形呢?最簡單的方法是在平面上進行遍歷。每遍歷到一個點,就給它標註一個值,比如00、01、10、11,隨著二進位制數字增加,相當於遍歷面上不同的位置。
當將空間劃分為四塊時候,編碼的順序分別是左下角00,左上角01,右下腳10,右上角11,也就是類似於Z的曲線。
如何表示不同的粒度?
當我們遞迴的將各個塊分解成更小的子塊時可以標識更小的空間範圍(如上圖二中所示),如從0000開始到1111結束編碼的順序是自相似的(分形),每一個子快也形成Z曲線,這種型別的曲線被稱為Peano空間填充曲線。
4.2.3 如何將一維表示成二進位制碼儲存
Geohash 也有幾種編碼形式,常見的有2種,base 32 和 base 36。 會將落到網格中的二進位制資料編碼成字串
尾巴
分析完Redis GEO的實現原理後不然發現其背後核心是geohash,使用geohash將二維的經緯度資料編碼成一維資料,再使用B樹索引快速查詢出需要的資料。
上述使用Redis GEO 實現附近的人,附近的車輛,附近的商家此類功能時只能通過半徑進行查詢。
Q:如果需求是我要查詢附近5公里內所有商家中有賣咖啡的呢?
A:當然我們可以在應用層對Reids 查詢出的所有資料進行過濾。
Q:當Redis返回資料量、使用者請求量比較大時是非常吃記憶體資源的,是否有更優解?業內的資料庫實現中是否已經有了更好的解決方案?
A:帶著這樣的疑問我查詢了相關資料發現geohash其實是空間索引的一種實現,我們經常使用的MySql、MongoDB都有空間索引的實現。
- MongoDB
mongo中的GeoJSON物件有點、線、多邊形、多條線段、多點、多個多邊形。支援 包含、相交、臨近的查詢,同時支援多條件查詢。(感覺非常強大的樣子真是換一個解決方案可能會有質的收益)
- MySql
MySql InnoDB 在5.7.4 labs版本中才新增對空間索引的支援,它們都是通過 R 樹來實現空間索引。
MySql的升級成本是很高的,理解了geohash原理後我們可以在MySql表中新增geohash欄位,通過B數的二分查詢法快速定位資料。
下一篇blog將進行手動計算geohash + MySql B樹索引實現的相關實踐總結,並對比MySql自帶的空間索引在儲存空間和查詢效率上的區別。