Redis GEO & 實現原理深度分析

1 前言

找吃的、找住的、找車

移動互聯網已融入到咱們生活中的方方面面。html

咱們平時找商家、找房子、找車均可以經過各類App來完成。做爲👨‍💻‍的筆者職業習慣性地思考這些功能是如何實現的呢?git

例如尋找附近3千米範圍內的出租車的需求,最直觀的想法就是去數據庫裏面查表篩選出距離用戶小於3千米的車輛,將數據返回給客戶端。數據庫

這種方法有一個很嚴重的問題,須要對整張表裏面的每一項都計算一次相對距離太耗時了。既然整張表數據量比較大那麼咱們能不能縮小掃描的範圍呢?那麼就會想到是否能夠按業務特色縮小掃描範圍好比只掃描用戶當前位置所在城市的車輛,按照這個思路擴展開來發現數據量仍是很大並且不能解決當用戶處於兩個城市的邊界時的問題。bash

如何快速地索引數據是解決這個問題的關鍵,在瀏覽Redis API的時候發現其能夠直接實現附近的XXX功能,下文中將介紹如何以Redis 實現此類功能並深刻分析其背後的實現原理。ui

2 Redis GEO API

2.1 增長地理位置信息

geo add key longitude latitude member [longitude latitude member ...]
複製代碼

eg:編碼

向cars:locations中增長車輛編號爲1以及車輛編號爲2的位置信息。spa

127.0.0.1:6379> geoadd cars:locations 120.346111 31.556381 1 120.375821 31.560368 2 
複製代碼

2.2 獲取地理位置信息

eg:3d

獲取車輛編號爲1的車輛位置信息調試

127.0.0.1:6379> geopos cars:locations 1
1) 1) "120.34611314535140991"
   2) "31.55637987511895659"
複製代碼

2.3 獲取兩個地理位置的距離

eg:code

獲取編號爲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核心原理解析

總結下來就是

  • 如何惟一表示地球上的一塊空間?
  • 如何將地球切分紅大小近似的區塊,並支持不一樣粒度的表示?

爲了解決上述兩個問題,咱們須要三個步驟。

  1. 第一步,將三維地球變成二維;
  2. 第二步,將二維再轉成一維;
  3. 最後一步,將一維表示成二進制碼存儲。

4.2.1 如何將三維變二維?

地球緯度區間是[-90,90],經度區間是[-180,180]。 將它展開想象長一個矩形爲

三維變二維

4.2.2 如何將二維變一維?

經過剛纔的方法,咱們可以將地球的表面轉換成二維空間的平面。那接下來要將二維轉變成一維。若是切割二維空間,能夠切割出不少正方形。如何表示這個正方形呢?最簡單的方法是在平面上進行遍歷。每遍歷到一個點,就給它標註一個值,好比00、0一、十、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都有空間索引的實現。

  1. MongoDB

mongo中的GeoJSON對象有點、線、多邊形、多條線段、多點、多個多邊形。支持 包含、相交、臨近的查詢,同時支持多條件查詢。(感受很是強大的樣子真是換一個解決方案可能會有質的收益)

  1. MySql

MySql InnoDB 在5.7.4 labs版本中才添加對空間索引的支持,它們都是經過 R 樹來實現空間索引。

MySql的升級成本是很高的,理解了geohash原理後咱們能夠在MySql表中新增geohash字段,經過B數的二分查找法快速定位數據。

下一篇blog將進行手動計算geohash + MySql B樹索引實現的相關實踐總結,並對比MySql自帶的空間索引在存儲空間和查詢效率上的區別。

相關文章
相關標籤/搜索