Redis GEO 功能使用場景

本文來源:https://www.dazhuanlan.com/2020/02/05/5e3a0a3110649/

背景

前段時間自己在做附近直播相關業務,其中有一個核心的點就是檢索用戶附近的主播,也是主要召回池。針對業務場景的特殊性,最後決定使用RedisGEO技術來完成這個功能。主要考慮點在於每天在線直播的主播數量是固定的差不多一萬這個量級,使用配置好一點的單機Rediskey存儲是沒問題的。數據操作主要有兩個:一是主播開播的時候寫入主播Id的經緯度,二是主播關播的時候刪除主播Id元素。這樣就維護了一個具有位置信息的在線主播集合提供給線上檢索。下面詳細介紹一下。

Redis GEO 命令

Redis3.2 版本提供了GEO(地理信息定位)功能,支持存儲地理位置信息用來實現諸如附近位置、搖一搖這類依賴於地理位置信息的功能,對於需要實現這些功能的開發者來說是一大福音。GEO功能是Redis的另一位作者Matt Stancliff 借鑑NoSQL數據庫 Ardb 實現的,Ardb的作者來自中國,它提供了優秀的GEO功能。

Redis GEO 相關的命令如下:

# 添加一個空間元素,longitude、latitude、member分別是該地理位置的經度、緯度、成員
# 這裏的成員就是指代具體的業務數據,比如說用戶的ID等
# 需要注意的是Redis的緯度有效範圍不是[-90,90]而是[-85,85]
# 如果在添加一個空間元素時,這個元素中的menber已經存在key中,那麼GEOADD命令會返回0,相當於更新了這個menber的位置信息
GEOADD key longitude latitude member [longitude latitude member]
# 用於添加城市的座標信息
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding

# 獲取地理位置信息
geopos key member [member ...]
# 獲取天津的座標
geopos cities:locations tianjin

# 獲取兩個座標之間的距離
# unit代表單位,有4個單位值
  - m (meter) 代表米
  - km (kilometer)代表千米
  - mi (miles)代表英里
  - ft (ft)代表尺
geodist key member1 member2 [unit]
# 獲取天津和保定之間的距離
GEODIST cities:locations tianjin baoding km

# 獲取指定位置範圍內的地理信息位置集合,此命令可以用於實現附近的人的功能
# georadius和georadiusbymember兩個命令的作用是一樣的,都是以一個地理位置爲中心算出指定半徑內的其他地理信息位置,不同的是georadius命令的中心位置給出了具體的經緯度,georadiusbymember只需給出成員即可。其中radiusm|km|ft|mi是必需參數,指定了半徑(帶單位),這兩個命令有很多可選參數,參數含義如下:
# - withcoord:返回結果中包含經緯度。 
# - withdist:返回結果中包含離中心節點位置的距離。
# - withhash:返回結果中包含geohash,有關geohash後面介紹。
# - COUNT count:指定返回結果的數量。
# - asc|desc:返回結果按照離中心節點的距離做升序或者降序。
# - store key:將返回結果的地理位置信息保存到指定鍵。
# - storedist key:將返回結果離中心節點的距離保存到指定鍵。
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

# 獲取geo hash
# Redis使用geohash將二維經緯度轉換爲一維字符串,geohash有如下特點:
# - GEO的數據類型爲zset,Redis將所有地理位置信息的geohash存放在zset中。
# - 字符串越長,表示的位置更精確,表3-8給出了字符串長度對應的精度,例如geohash長度爲9時,精度在2米左右。長度和精度的對應關係,請參考:https://easyreadfs.nosdn.127.net/9F42_CKRFsfc8SUALbHKog==/8796093023252281390
# - 兩個字符串越相似,它們之間的距離越近,Redis利用字符串前綴匹配算法實現相關的命令。
# - geohash編碼和經緯度是可以相互轉換的。
# - Redis正是使用有序集合並結合geohash的特性實現了GEO的若干命令。
geohash key member [member ...]

# 刪除操作,GEO沒有提供刪除成員的命令,但是因爲GEO的底層實現是zset,所以可以借用zrem命令實現對地理位置信息的刪除。
zrem key member

Redis GEO 原理

Redis GEO實現之前需要先明白一些關於空間索引的算法GEOHASH的知識。針對索引我們日常所見都是一維的字符,那麼如何對三維空間裏面的座標點建立索引呢,直接點就是三維變二維,二維變一維。

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

img

通過上面的方法將地球的表面轉換成二維空間的平面,那接下來就是如何將二維換行成一維了。我們先將平面切割成四個正方形,然後用簡單的 01 編碼來標識這個四個正方形,最後按照編碼的大小將四個正方形連接起來,這樣整個平面就轉換成了一條Z曲線,變成了一維。我們遞歸對每個正方形做同樣的操作,遞歸的層次越深,整個平面就逐漸被Z曲線填充。我們的點也會落在每個小正方形裏面,小正方形越小,精度就越高。如下圖所示:

img

轉成一維以後接下來就如何建立索引了。當我們拿到一個經緯度之後按照如下方式進行編碼。

img

從上面的圖可以發現二分的次數越多就越接近經緯度的實際值,和前面提到的不斷遞歸正方形是一個意思。按照上面的方式我們選定一個二分的深度(也就是精度)分別對經緯度進行編碼。然後按照以奇數爲緯度,偶數爲經度組合成一個二進制序列,再將獲取到的經緯度組合二進制序列以每5個數爲一組,將每一組都進行轉換成十進制數字,最後採用Base32對應編碼規則進行轉換可得到編碼,也就是最後的索引。

img

通過上面幾個步驟介紹了一下GeoHash具體的流程、有了上面這個知識點,理解Redis GEO原理就很簡單了,Redis使用ZSet的方式存儲Geo類型的數據,有序集合裏面的member是具體的業務對象,score就是該業務對象的經緯度進行GeoHash編碼之後將二級制序列轉成52位整數值數據。當我們想要獲取某個經緯度附近的元素時候,先根據當前經緯度計算出對應的GeoHash塊(52位整數值),在根據半徑計算出當前hash塊周圍的8個hash塊,然後在根據score值獲取這8個hash塊範圍內的元素返回。

GEO HASH 延伸

對於一個經緯度,如果我們編碼的時候選擇對經度二分3次(3位二進制),對維度二分2次(2位二進制),最後組合成一個5位的二級進序列,經過Base32編碼得到一個字符。那麼這個字符的一共有2^5=32個,這樣就將地圖劃分爲32個塊。如下圖所示

img

GeoHash將每一個區域畫成一塊塊矩形塊,每個矩形塊使用一個字符串表示,當我們需要查詢附近的點時,通過自己的座標計算出一個字符串,根據這個字符串定位到我們所在的矩形塊,然後返回這個矩形塊中的點。然後根據編碼的深度來確定精度,或者根據Base32編碼之後字符的長度來確定塊的所表示的區域大小。

img

length width height
1 5000km 5000km
2 1250km 625km
3 156km 156km
4 39.1km 19.5km
5 4.89km 4.89km
6 1.22km 0.61km
7 153m 153m
8 38.2m 19.1m
9 4.77m 4.77m
10 1.19m 0.596m
11 149mm 149mm
12 37.2mm 18.6mm

對於這樣的編碼方式有一定的侷限性:在擁有局部保序性的同時,具有突變性。導致一些鄰近點真實並不是距離較近的點。

參考

http://geohash.gofreerange.com/
https://halfrost.com/go_spatial_search/
https://www.cnblogs.com/LBSer/p/3310455.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章