背景
前段時間自己在做附近直播相關業務,其中有一個核心的點就是檢索用戶附近的主播,也是主要召回池。針對業務場景的特殊性,最后決定使用Redis
的GEO
技術來完成這個功能。主要考慮點在於每天在線直播的主播數量是固定的差不多一萬這個量級,使用配置好一點的單機Redis
單key
存儲是沒問題的。數據操作主要有兩個:一是主播開播的時候寫入主播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]
。 將它展開想象成一個矩形。
通過上面的方法將地球的表面轉換成二維空間的平面,那接下來就是如何將二維換行成一維了。我們先將平面切割成四個正方形,然后用簡單的 01 編碼來標識這個四個正方形,最后按照編碼的大小將四個正方形連接起來,這樣整個平面就轉換成了一條Z曲線,變成了一維。我們遞歸對每個正方形做同樣的操作,遞歸的層次越深,整個平面就逐漸被Z曲線填充。我們的點也會落在每個小正方形里面,小正方形越小,精度就越高。如下圖所示:
轉成一維以后接下來就如何建立索引了。當我們拿到一個經緯度之后按照如下方式進行編碼。
從上面的圖可以發現二分的次數越多就越接近經緯度的實際值,和前面提到的不斷遞歸正方形是一個意思。按照上面的方式我們選定一個二分的深度(也就是精度)分別對經緯度進行編碼。然后按照以奇數為緯度,偶數為經度
組合成一個二進制序列,再將獲取到的經緯度組合二進制序列以每5個數為一組,將每一組都進行轉換成十進制數字,最后采用Base32
對應編碼規則進行轉換可得到編碼,也就是最后的索引。
通過上面幾個步驟介紹了一下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個塊。如下圖所示
GeoHash
將每一個區域畫成一塊塊矩形塊,每個矩形塊使用一個字符串表示,當我們需要查詢附近的點時,通過自己的坐標計算出一個字符串,根據這個字符串定位到我們所在的矩形塊,然后返回這個矩形塊中的點。然后根據編碼的深度來確定精度,或者根據Base32編碼之后字符的長度來確定塊的所表示的區域大小。
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