javascript
Spring Boot 2 实战:利用Redis的Geo功能实现查找附近的位置
1. 前言
老板突然要上線一個需求,獲取當(dāng)前位置方圓一公里的業(yè)務(wù)代理點。明天上線!當(dāng)接到這個需求的時候我差點吐血,這時間也太緊張了。趕緊去查相關(guān)的技術(shù)選型。經(jīng)過一番折騰,終于在晚上十點完成了這個需求。現(xiàn)在把大致實現(xiàn)的思路總結(jié)一下。
2. MySQL 不合適
遇到需求,首先要想到現(xiàn)有的東西能不能滿足,成本如何。
MySQL是我首先能夠想到的,畢竟大部分數(shù)據(jù)要持久化到MySQL。但是使用MySQL需要自行計算Geohash。需要使用大量數(shù)學(xué)幾何計算,并且需要學(xué)習(xí)地理相關(guān)知識,門檻較高,短時間內(nèi)不可能完成需求,而且長期來看這也不是MySQL擅長的領(lǐng)域,所以沒有考慮它。
Geohash?參考?https://www.cnblogs.com/LBSer/p/3310455.html
2. Redis 中的GEO
Redis是我們最為熟悉的K-V數(shù)據(jù)庫,它常被拿來作為高性能的緩存數(shù)據(jù)庫來使用,大部分項目都會用到它。從3.2版本開始它開始提供了GEO能力,用來實現(xiàn)諸如附近位置、計算距離等這類依賴于地理位置信息的功能。GEO相關(guān)的命令如下:
| GEOHASH | 返回一個或多個位置元素的?Geohash?表示 |
| GEOPOS | 從key里返回所有給定位置元素的位置(經(jīng)度和緯度) |
| GEODIST | 返回兩個給定位置之間的距離 |
| GEORADIUS | 以給定的經(jīng)緯度為中心, 找出某一半徑內(nèi)的元素 |
| GEOADD | 將指定的地理空間位置(緯度、經(jīng)度、名稱)添加到指定的key中 |
| GEORADIUSBYMEMBER | 找出位于指定范圍內(nèi)的元素,中心點是由給定的位置元素決定 |
Redis會假設(shè)地球為完美的球形, 所以可能有一些位置計算偏差,據(jù)說<=0.5%,對于有嚴格地理位置要求的需求來說要經(jīng)過一些場景測試來檢驗是否能夠滿足需求。
2.1 寫入地理信息
那么如何實現(xiàn)目標單位半徑內(nèi)的所有元素呢?我們可以將所有的位置的經(jīng)緯度通過上表中的GEOADD將這些地理信息轉(zhuǎn)換為52位的Geohash寫入Redis。
該命令格式:
geoadd key longitude latitude member [longitude latitude member ...]對應(yīng)例子:
redis> geoadd cities:locs 117.12 39.08 tianjin 114.29 38.02 shijiazhuang (integer) 2意思是將經(jīng)度為117.12緯度為39.08的地點tianjin和經(jīng)度為114.29緯度為38.02的地點shijiazhuang加入key為cities:locs的?sorted set集合中。可以添加一到多個位置。然后我們就可以借助于其他命令來進行地理位置的計算了。
有效的經(jīng)度從-180度到180度。有效的緯度從-85.05112878度到85.05112878度。當(dāng)坐標位置超出上述指定范圍時,該命令將會返回一個錯誤。
2.2 統(tǒng)計單位半徑內(nèi)的地區(qū)
我們可以借助于GEORADIUS來找出以給定經(jīng)緯度,某一半徑內(nèi)的所有元素。
該命令格式:
georadius key longtitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]這個命令比GEOADD要復(fù)雜一些:
- radius?半徑長度,必選項。后面的m、km、ft、mi、是長度單位選項,四選一。
- WITHCOORD?將位置元素的經(jīng)度和維度也一并返回,非必選。
- WITHDIST?在返回位置元素的同時, 將位置元素與中心點的距離也一并返回。 距離的單位和查詢單位一致,非必選。
- WITHHASH?返回位置的52位精度的Geohash值,非必選。這個我反正很少用,可能其它一些偏向底層的LBS應(yīng)用服務(wù)需要這個。
- COUNT?返回符合條件的位置元素的數(shù)量,非必選。比如返回前10個,以避免出現(xiàn)符合的結(jié)果太多而出現(xiàn)性能問題。
- ASC|DESC?排序方式,非必選。默認情況下返回未排序,但是大多數(shù)我們需要進行排序。參照中心位置,從近到遠使用ASC?,從遠到近使用DESC。
例如,我們在?cities:locs?中查找以(115.03,38.44)為中心,方圓200km的城市,結(jié)果包含城市名稱、對應(yīng)的坐標和距離中心點的距離(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來查找最近的一個位置。
3. 基于Redis GEO實戰(zhàn)
大致的原理思路說完了,接下來就是實操了。結(jié)合Spring Boot應(yīng)用我們應(yīng)該如何做?
3.1 開發(fā)環(huán)境
需要具有GEO特性的Redis版本,這里我使用的是Redis 4?。另外我們客戶端使用?spring-boot-starter-data-redis?。這里我們會使用到?RedisTemplate對象。
3.2 批量添加位置信息
第一步,我們需要將位置數(shù)據(jù)初始化到Redis中。在Spring Data Redis中一個位置坐標(lng,lat)?可以封裝到org.springframework.data.geo.Point對象中。然后指定一個名稱,就組成了一個位置Geo信息。RedisTemplate提供了批量添加位置信息的方法。我們可以將章節(jié)2.1中的添加命令轉(zhuǎn)換為下面的代碼:
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 批量添加 GeoredisTemplate.boundGeoOps("cities:locs").add(points);可以結(jié)合Spring Boot 提供的ApplicationRunner接口來實現(xiàn)初始化。
@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 批量添加 GeoLocationBoundGeoOperations<String, String> geoOps = redisTemplate.boundGeoOps(GEO_KEY);geoOps.add(points);}; }地理數(shù)據(jù)持久化到MySQL,然后同步到Redis中。
3.3 查詢附近的特定位置
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的一些可選命令參數(shù),參見章節(jié)2.2中的WITHCOORD、COUNT、ASC等,例如我們需要在返回結(jié)果中包含坐標、中心距離、由近到遠排序的前5條數(shù)據(jù):
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(limit);然后執(zhí)行?radius方法就會拿到GeoResults<RedisGeoCommands.GeoLocation<String>>封裝的結(jié)果,我們對這個可迭代對象進行解析就可以拿到我們想要的數(shù)據(jù):
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();// 對應(yīng)的經(jīng)緯度坐標Point pos = content.getPoint();// 距離中心點的距離Distance dis = geoLocationGeoResult.getDistance();}); }3.4 刪除元素
有時候我們可能需要刪除某個位置元素,但是Redis的Geo并沒有刪除成員的命令。不過由于它的底層是zset,我們可以借助zrem命令進行刪除,對應(yīng)的Java代碼為:
redisTemplate.boundZSetOps(GEO_STAGE).remove("tianjin");4. 總結(jié)
今天我們使用Redis的Geo特性實現(xiàn)了常見的附近的地理信息查詢需求,簡單易上手。其實使用另一個Nosql數(shù)據(jù)庫MongoDB也可以實現(xiàn)。在數(shù)據(jù)量比較小的情況下Redis已經(jīng)能很好的滿足需要。如果數(shù)據(jù)量大可使用MongoDB來實現(xiàn)。 文中涉及的DEMO可通過我個人博客獲取。
總結(jié)
以上是生活随笔為你收集整理的Spring Boot 2 实战:利用Redis的Geo功能实现查找附近的位置的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle 批量插入
- 下一篇: mysql虚拟列(Generated C