Redis系列9:Geo 類型賦能億級地圖位置計算

Redis系列1:深刻理解高性能Redis的本質
Redis系列2:數據持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 集群模式
追求性能極致:Redis6.0的多線程模型
追求性能極致:客戶端緩存帶來的革命
Redis系列8:Bitmap實現億萬級數據計算

1 前言

我們在第一篇 深刻理解高性能Redis的本質 的時候就介紹過Redis的幾種基本數據結構,它是基於不同業務場景而設計的:

  • 動態字符串(REDIS_STRING):整數(REDIS_ENCODING_INT)、字符串(REDIS_ENCODING_RAW)
  • 雙端列表(REDIS_ENCODING_LINKEDLIST)
  • 壓縮列表(REDIS_ENCODING_ZIPLIST)
  • 跳躍表(REDIS_ENCODING_SKIPLIST)
  • 哈希表(REDIS_HASH)
  • 整數集合(REDIS_ENCODING_INTSET)
    除了這些常見數據類型,還有一些不常用的數據類型,如 BitMap、Geo、HyperLogLog 等等,他們在各自的方向為不同的類型的數據統計給出解決方案。
    上一篇我們說了位圖(BitMap)計算,可以應用於任何大數據場景下的二值計算,比如 是否登錄、是否在線、是否簽到、用戶性別狀態、IP黑名單、是否VIP用戶統計 等等場景。
    這一篇我們來介紹下Geo,分析它在 坐標記錄、位置計算、距離計算上的能力,以及在地圖業務中的應用場景。

2 了解一下 Location Based Services

Location Based Services,記作 LBS,基於用戶的地理位置數據定位展開的服務,廣泛應用與地圖類(百度地圖、高德地圖)、電商團購類(美團、餓了么)軟件。它常見的使用場景有:

  • 計算用戶的精準的地理坐標位置
  • 統計用戶定點坐標一定範圍內的其他地理位置,並計算出距離
  • 對一定範圍內的地理位置進行排序,並由近到遠篩選

有沒有感覺很熟悉,當然了,在我們的身邊到處都是這樣的應用場景。

3 Geo所支持的能力

Redis 的 GEO 特性在 Redis 3.2 版本就有了, 這個功能主要是用於存儲用戶地理位置信息,並對這些信息進行操作。
GEO 的數據結構總共有六個命令,我們一個個來介紹 :

  • geoadd
  • geopos
  • geodist
  • georadius
  • georadiusbymember
  • gethash

3.1 GEOADD 添加經緯信息

Redis 提供了 GEOADD key longitude latitude member 命令,將一組經緯度信息和對應的所屬對象的信息 記錄到 GEO 類型的集合中,指令如下

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

longitude latitude member 分別指給定的空間元素:維度、精度、名稱 ,這些數據會以有序集合的形式存儲在給定的鍵裏面。
我們舉個例子,如果你在地圖上查找美食,那應該會出現一堆餐飲店鋪和坐標位置,那他們的空間信息存儲可能是這樣的。

redis> GEOADD food:location 115.775632 39.483256 "東北餃子館" 114.081569 39.692756 "蘭州拉麵"

(integer) 2

image

3.2 GEOPOS 獲取給定位置的經緯

提供對應的鍵和位置名稱,返回相應的經緯度信息。

GEOPOS key member [member ...]

按照上面的例子,我要獲取對應的美食店位置坐標信息如下:

redis> GEOPOS food:location 東北餃子館 蘭州拉麵 NonExisting
 
 "115.775632 39.483256"
 
 "114.081569 39.692756"

3.3 GEODIST 返回給定兩個位置距離

很多時候,我們要導航去一個地方就會用到這類需求。打開百度或者高德地圖,起始位置就定位用戶當前位置,目的地定位為搜索到的地址,比如上面的 東北餃子館。
這時候地圖軟件需要計算出兩個坐標之間的舉例,來推薦用戶是飛機高鐵、開車、還是步行。那麼獲取給定兩個位置之間的距離就變得非常重要,GEODIST就是用來解決這個問題的。

GEODIST key member1 member2 [unit]

上述指令可以返回兩個給定位置之間的距離,unit是距離單位,可選項,默認為m,枚舉如下:

  • m:表示單位為米
  • km:表示單位為千米
  • mi:表示單位為英里
  • ft:表示單位為英尺

需要注意的是如果兩個位置之間的其中一個不存在, 那麼會返回空值。下面代碼計算出 東北餃子館 和 蘭州拉麵 店鋪之間的距離,大概是6.1公里。

image

redis> GEODIST food:location 東北餃子館 蘭州拉麵
 
"6184.15156"

3.4 GEORADIUS 獲取給定經緯度的固定距離內的位置信息

很多種應用場景是我登錄了外賣APP,也確定了我自己所在的位置(即已確知經緯),需要獲取一定距離範圍內(比如10公里),所有的餐飲店。
這時候就使用到了 GEO 提供的 GEORADIUS指令了:根據輸入的經緯度,查找以這個經緯度為中心的一定距離內的其他位置信息。

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
  • key longitude latitude: 是前置條件,給定的經緯度信息,以及我要搜索的key
  • radius :距離半徑,指的搜索的範圍
  • m|km|ft|mi: 為給定的距離單位,有 米、千米、英尺、英里 4種
  • [WITHCOORD] [WITHDIST] [WITHHASH]: 為返回的信息類型
    • WITHDIST: 在返回位置元素的同時, 將位置元素與中心之間的距離也一併返回。距離的單位和用戶給定的範圍單位保持一致。
    • WITHCOORD: 將位置元素的經度和維度也一併返回。
    • WITHHASH: 以 52 位有符號整數的形式, 返回位置元素經過原始 geohash 編碼的有序集合分值。這個選項主要用於底層應用或者調試, 實際中的作用並不大。
  • ASC|DESC :可選參數,按照距離升序或者降序排列,即 由近到遠(asc) 還是 由遠到近(desc)
  • COUNT count:取數數量,避免獲取到太多的信息,返回太多信息

所以如果需要獲取 距離本人位置10公里半徑內由近到遠的美食店排序,按km單位計算,返回值帶上距離信息,並只取前100個的信息,代碼如下:

redis> GEORADIUS food:location 115.791331 39.5120003  10 km WITHDIST  ASC COUNT 100

"東北餃子館"   3.3421
"蘭州拉麵"    9.4571

下圖的綠色區域在固定半徑(紅圈)中搜索到了特定的幾個目標位置:1、2、5、9、10。
image

3.5 GEORADIUSBYMEMBER 按照位置名稱獲取

GEORADIUS 的區別是 GEORADIUSBYMEMBER 的中心點是由給定的位置元素決定的, 而不是像 GEORADIUS 那樣,通過傳入經度和緯度來決定中心點。
所以如下,已知蘭州拉麵和東北餃子館的距離是6.1公里,根據蘭州拉麵獲取10公里範圍內的距離的美食店,可以獲取到東北餃子館和自己的位置:

redis> GEORADIUSBYMEMBER food:location "蘭州拉麵" 100 km WITHDIST

"東北餃子館"   6.09127
"蘭州拉麵"    0

3.6 ZREM 刪除關閉的店鋪

redis>  ZREM food:location "蘭州拉麵"

(integer) 1

4 總結

  • GEO 使用了 Sorted Set 集合類型,並通過 GeoHash 編碼方法實現了經緯度到 Sorted Set 中元素權重分數的轉換,涵蓋兩個關鍵能力就是就是對二維地圖做區間劃分,以及對區間進行編碼。
  • 具體可應用的場景如下:
    • 計算用戶的精準的地理坐標位置
    • 統計用戶定點坐標一定範圍內的其他地理位置,並計算出距離
    • 對一定範圍內的地理位置進行排序,並由近到遠篩選
  • 真實的地圖數據存儲,場景比這複雜的多,數據量會達到驚人的巨量,所以還是會使用分治原理進行拆分,細化到一個市甚至一個區,來提高存儲和檢索的效率。