老闆忽然要上線一個需求,獲取當前位置方圓一千米的業務代理點。明天上線!當接到這個需求的時候我差點吐血,這時間也太緊張了。趕忙去查相關的技術選型。通過一番折騰,終於在晚上十點完成了這個需求。如今把大體實現的思路總結一下。html
遇到需求,首先要想到現有的東西能不能知足,成本如何。
MySQL是我首先可以想到的,畢竟大部分數據要持久化到MySQL。可是使用MySQL須要自行計算Geohash。須要使用大量數學幾何計算,而且須要學習地理相關知識,門檻較高,短期內不可能完成需求,並且長期來看這也不是MySQL擅長的領域,因此沒有考慮它。java
Geohash 參考 https://www.cnblogs.com/LBSer...
Redis是咱們最爲熟悉的K-V數據庫,它常被拿來做爲高性能的緩存數據庫來使用,大部分項目都會用到它。從3.2版本開始它開始提供了GEO能力,用來實現諸如附近位置、計算距離等這類依賴於地理位置信息的功能。GEO相關的命令以下:git
Redis命令 | 描述 |
---|---|
GEOHASH | 返回一個或多個位置元素的 Geohash 表示 |
GEOPOS | 從key裏返回全部給定位置元素的位置(經度和緯度) |
GEODIST | 返回兩個給定位置之間的距離 |
GEORADIUS | 以給定的經緯度爲中心, 找出某一半徑內的元素 |
GEOADD | 將指定的地理空間位置(緯度、經度、名稱)添加到指定的key中 |
GEORADIUSBYMEMBER | 找出位於指定範圍內的元素,中心點是由給定的位置元素決定 |
Redis會假設地球爲完美的球形, 因此可能有一些位置計算誤差,聽說<=0.5%,對於有嚴格地理位置要求的需求來講要通過一些場景測試來檢驗是否可以知足需求。
那麼如何實現目標單位半徑內的全部元素呢?咱們能夠將全部的位置的經緯度經過上表中的GEOADD
將這些地理信息轉換爲52位的Geohash寫入Redis。 redis
該命令格式:spring
geoadd key longitude latitude member [longitude latitude member ...]
對應例子:sql
redis> geoadd cities:locs 117.12 39.08 tianjin 114.29 38.02 shijiazhuang (integer) 2
意思是將經度爲117.12
緯度爲39.08
的地點tianjin
和經度爲114.29
緯度爲38.02
的地點shijiazhuang
加入key爲cities:locs
的 sorted set集合中。能夠添加一到多個位置。而後咱們就能夠藉助於其餘命令來進行地理位置的計算了。數據庫
有效的經度從-180度到180度。有效的緯度從-85.05112878度到85.05112878度。當座標位置超出上述指定範圍時,該命令將會返回一個錯誤。
咱們能夠藉助於GEORADIUS
來找出以給定經緯度,某一半徑內的全部元素。緩存
該命令格式:bash
georadius key longtitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]
這個命令比GEOADD
要複雜一些:spring-boot
m
、km
、ft
、mi
、是長度單位選項,四選一。例如,咱們在 cities:locs
中查找以(115.03,38.44)爲中心,方圓200km
的城市,結果包含城市名稱、對應的座標和距離中心點的距離(km),並按照從近到遠排列。命令以下:
redis> georadius cities:locs 115.03 38.44 200 km WITHCOORD WITHDIST ASC 1) 1) "shijiazhuang" 2) "79.7653" 3) 1) "114.29000169038772583" 2) "38.01999994251037407" 2) 1) "tianjin" 2) "186.6937" 3) 1) "117.02000230550765991" 2) "39.0800000535766543"
你能夠加上
COUNT 1
來查找最近的一個位置。
大體的原理思路說完了,接下來就是實操了。結合Spring Boot應用咱們應該如何作?
須要具備GEO特性的Redis版本,這裏我使用的是Redis 4 。另外咱們客戶端使用 spring-boot-starter-data-redis
。這裏咱們會使用到 RedisTemplate
對象。
第一步,咱們須要將位置數據初始化到Redis中。在Spring Data Redis中一個位置座標(lng,lat)
能夠封裝到org.springframework.data.geo.Point
對象中。而後指定一個名稱,就組成了一個位置Geo信息。RedisTemplate
提供了批量添加位置信息的方法。咱們能夠將章節2.1中的添加命令轉換爲下面的代碼:
Map<String, Point> points = new HashMap<>(); points.put("tianjin", new Point(117.12, 39.08)); points.put("shijiazhuang", new Point(114.29, 38.02)); // RedisTemplate 批量添加 Geo redisTemplate.boundGeoOps("cities:locs").add(points);
能夠結合Spring Boot 提供的ApplicationRunner接口來實現初始化。
@Bean public ApplicationRunner cacheActiveAppRunner(RedisTemplate<String, String> redisTemplate) { return args -> { final String GEO_KEY = "cities:locs"; // 清理緩存 redisTemplate.delete(GEO_KEY); Map<String, Point> points = new HashMap<>(); points.put("tianjin", new Point(117.12, 39.08)); points.put("shijiazhuang", new Point(114.29, 38.02)); // RedisTemplate 批量添加 GeoLocation BoundGeoOperations<String, String> geoOps = redisTemplate.boundGeoOps(GEO_KEY); geoOps.add(points); }; }
地理數據持久化到MySQL,而後同步到Redis中。
RedisTemplate
針對GEORADIUS
命令也有封裝:
GeoResults<GeoLocation<M>> radius(K key, Circle within, GeoRadiusCommandArgs args)
Circle
對象是封裝覆蓋的面積(圖1),須要的要素爲中心點座標Point
對象、半徑(radius)、計量單位(metric), 例如:
Point point = new Point(115.03, 38.44); Metric metric = RedisGeoCommands.DistanceUnit.KILOMETERS; Distance distance = new Distance(200, metric); Circle circle = new Circle(point, distance);
GeoRadiusCommandArgs
用來封裝GEORADIUS
的一些可選命令參數,參見章節2.2中的WITHCOORD
、COUNT
、ASC
等,例如咱們須要在返回結果中包含座標、中心距離、由近到遠排序的前5條數據:
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands .GeoRadiusCommandArgs .newGeoRadiusArgs() .includeDistance() .includeCoordinates() .sortAscending() .limit(limit);
而後執行 radius
方法就會拿到GeoResults<RedisGeoCommands.GeoLocation<String>>
封裝的結果,咱們對這個可迭代對象進行解析就能夠拿到咱們想要的數據:
GeoResults<RedisGeoCommands.GeoLocation<String>> radius = redisTemplate.opsForGeo() .radius(GEO_STAGE, circle, args); if (radius != null) { List<StageDTO> stageDTOS = new ArrayList<>(); radius.forEach(geoLocationGeoResult -> { RedisGeoCommands.GeoLocation<String> content = geoLocationGeoResult.getContent(); //member 名稱 如 tianjin String name = content.getName(); // 對應的經緯度座標 Point pos = content.getPoint(); // 距離中心點的距離 Distance dis = geoLocationGeoResult.getDistance(); }); }
有時候咱們可能須要刪除某個位置元素,可是Redis的Geo並無刪除成員的命令。不過因爲它的底層是zset
,咱們能夠藉助zrem
命令進行刪除,對應的Java代碼爲:
redisTemplate.boundZSetOps(GEO_STAGE).remove("tianjin");
今天咱們使用Redis的Geo特性實現了常見的附近的地理信息查詢需求,簡單易上手。其實使用另外一個Nosql數據庫MongoDB也能夠實現。在數據量比較小的狀況下Redis已經能很好的知足須要。若是數據量大可以使用MongoDB來實現。 文中涉及的DEMO可關注:碼農小胖哥 ,公衆號回覆 redisgeo獲取。
關注公衆號:Felordcn 獲取更多資訊