【位置排序相關的需求】
其實這種需求是比較多的,這里舉一個簡單的場景。比如我們在全國各地有很多充電站,每個充電站在數據庫里都有對應的省市縣以及經度緯度,在對充電站維護的時候經常會依據電工的即時位置查看周邊有哪些充電站,並對這些充電站的位置參照電工的即時位置計算距離,並根據距離大小進行排序。
首先,我們需要確定一個范圍半徑r,而后根據經緯度計算這個范圍內的所有充電站,sql語句如下:
select * from charge_station where x0-r<longitude<x0+r and y0-r<latitude<y0+r
而后對這一批數據與電工的位置點進行距離計算並排序,但這樣的查詢語句無論是性能還是距離直觀性以及分頁按數據查找都不理想。
【GeoHash算法思路】
GeoHash的思路是將經緯度這種二維坐標映射到一維,也就是變成了一條線,二維平面上距離近的元素在一維線上距離也更近,此時只需要在這個一維的線上獲取附近的站點就行了。
在Redis中,經緯度會被編碼放入zset中,value是元素的key,score是GeoHash的52位編碼。此時計算附近的站點只需要根據zset的score進行排序,而后還可以將score還原為坐標值。
【Geo指令】
添加:
geoadd station 116.48105 39.996794 station_id_one
geoadd station 116.48105 39.996794 station_id_two
距離:geodist指令可以用來計算兩個已知元素的距離,如:
geodist station station_id_one station_id_two km #攜帶key,2個元素的value以及距離單位
還原坐標:
geopos station station_id_one
獲取元素的hash值
geohash station station_id_one
【georadius指令】
單獨看看最關鍵的附近的站點查詢指令:
georadiusbymember station station_id_one 20 km count 3 asc
#表示站點station_id_one附近20km的距離正序排序,只取3個,它不會排除自身,當然也可換為desc倒序排列
可以添加的參數:
WITHDIST: 在返回位置元素的同時, 將位置元素與中心之間的距離也一並返回。
WITHCOORD: 將位置元素的經度和維度也一並返回。
WITHHASH: 以 52 位有符號整數的形式, 返回位置元素經過原始 geohash 編碼的有序集合分值。 這個選項主要用於底層應用或者調試, 實際中的作用並不大。
georadisbymember station station_id_one 20 km withcoord withdist withhash count 3 asc
另外,如果是電工的坐標,我們是不方便直接放入這個集合中了,redis也提供了根據坐標來查詢附近的站點:
georadisbymember station 116.514202 39.905409 20 km withdist count 3 asc
【注意事項】
對於數據量很大的集合,最好不要全部投放在一個zset中,不利用集群的遷移。單個key中的數據不宜超過1MB。建議Geo數據使用單獨的Redis實例部署,不使用集群環境。數據量過大時,我們可以按照行政單位等維度進行划分,可以顯著降低單個zset集合大小。
【代碼實現】
package com.redis.geohash; import redis.clients.jedis.GeoCoordinate; import redis.clients.jedis.GeoRadiusResponse; import redis.clients.jedis.GeoUnit; import redis.clients.jedis.Jedis; import redis.clients.jedis.params.GeoRadiusParam; import java.util.List; public class GeoHashTest { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); jedis.auth("123456"); jedis.geoadd("station", 116.48105, 39.996794, "園藝山站點"); jedis.geoadd("station", 116.514203, 39.905409, "游仙站點"); jedis.geoadd("station", 116.514203, 39.905409, "石橋鋪站點"); jedis.geoadd("station", 116.562108, 39.787602, "花荄站點"); jedis.geoadd("station", 116.334255, 40.027400, "三台縣站點"); Double geodist = jedis.geodist("station", "園藝山站點", "石橋鋪站點"); System.out.println("geodist = " + geodist); System.out.println("--------------------------------------"); List<GeoCoordinate> geopos = jedis.geopos("station", "花荄站點", "三台縣站點"); System.out.println("geopos.toString() = " + geopos.toString()); System.out.println("--------------------------------------"); //計算100KM以內的所有站點 List<GeoRadiusResponse> georadius = jedis.georadius("station", 116.334212, 39.992813, 100, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().withCoord().sortAscending()); for (int i = 0; i < georadius.size(); i++) { GeoRadiusResponse geoRadiusResponse = georadius.get(i); System.out.println("geoRadiusResponse.getMember() = " + geoRadiusResponse.getMemberByString()); System.out.println("geoRadiusResponse.getDistance() = " + geoRadiusResponse.getDistance()); } } }
【參考】
《Redis深度歷險 核心原理與應用實踐》