Spring Boot 2 實戰:利用Redis的Geo功能實現查找附近的位置

1. 前言

老闆忽然要上線一個需求,獲取當前位置方圓一千米的業務代理點。明天上線!當接到這個需求的時候我差點吐血,這時間也太緊張了。趕忙去查相關的技術選型。通過一番折騰,終於在晚上十點完成了這個需求。如今把大體實現的思路總結一下。html

圖1

2. MySQL 不合適

遇到需求,首先要想到現有的東西能不能知足,成本如何。

MySQL是我首先可以想到的,畢竟大部分數據要持久化到MySQL。可是使用MySQL須要自行計算Geohash。須要使用大量數學幾何計算,而且須要學習地理相關知識,門檻較高,短期內不可能完成需求,並且長期來看這也不是MySQL擅長的領域,因此沒有考慮它。java

Geohash 參考 https://www.cnblogs.com/LBSer...

2. Redis 中的GEO

Redis是咱們最爲熟悉的K-V數據庫,它常被拿來做爲高性能的緩存數據庫來使用,大部分項目都會用到它。從3.2版本開始它開始提供了GEO能力,用來實現諸如附近位置、計算距離等這類依賴於地理位置信息的功能。GEO相關的命令以下:git

Redis命令 描述
GEOHASH 返回一個或多個位置元素的 Geohash 表示
GEOPOS 從key裏返回全部給定位置元素的位置(經度和緯度)
GEODIST 返回兩個給定位置之間的距離
GEORADIUS 以給定的經緯度爲中心, 找出某一半徑內的元素
GEOADD 將指定的地理空間位置(緯度、經度、名稱)添加到指定的key中
GEORADIUSBYMEMBER 找出位於指定範圍內的元素,中心點是由給定的位置元素決定
Redis會假設地球爲完美的球形, 因此可能有一些位置計算誤差,聽說<=0.5%,對於有嚴格地理位置要求的需求來講要通過一些場景測試來檢驗是否可以知足需求。

2.1 寫入地理信息

那麼如何實現目標單位半徑內的全部元素呢?咱們能夠將全部的位置的經緯度經過上表中的GEOADD將這些地理信息轉換爲52位的Geohash寫入Redisredis

該命令格式: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加入keycities:locssorted set集合中。能夠添加一到多個位置。而後咱們就能夠藉助於其餘命令來進行地理位置的計算了。數據庫

有效的經度從-180度到180度。有效的緯度從-85.05112878度到85.05112878度。當座標位置超出上述指定範圍時,該命令將會返回一個錯誤。

2.2 統計單位半徑內的地區

咱們能夠藉助於GEORADIUS來找出以給定經緯度,某一半徑內的全部元素。緩存

該命令格式:bash

georadius key longtitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]

這個命令比GEOADD要複雜一些:spring-boot

  • radius 半徑長度,必選項。後面的mkmftmi、是長度單位選項,四選一。
  • WITHCOORD 將位置元素的經度和維度也一併返回,非必選。
  • WITHDIST 在返回位置元素的同時, 將位置元素與中心點的距離也一併返回。 距離的單位和查詢單位一致,非必選。
  • WITHHASH 返回位置的52位精度的Geohash值,非必選。這個我反正不多用,可能其它一些偏向底層的LBS應用服務須要這個。
  • COUNT 返回符合條件的位置元素的數量,非必選。好比返回前10個,以免出現符合的結果太多而出現性能問題。
  • ASC|DESC 排序方式,非必選。默認狀況下返回未排序,可是大多數咱們須要進行排序。參照中心位置,從近到遠使用ASC ,從遠到近使用DESC

例如,咱們在 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來查找最近的一個位置。

3. 基於Redis GEO實戰

大體的原理思路說完了,接下來就是實操了。結合Spring Boot應用咱們應該如何作?

3.1 開發環境

須要具備GEO特性的Redis版本,這裏我使用的是Redis 4 。另外咱們客戶端使用 spring-boot-starter-data-redis 。這裏咱們會使用到 RedisTemplate對象。

3.2 批量添加位置信息

第一步,咱們須要將位置數據初始化到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中。

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的一些可選命令參數,參見章節2.2中的WITHCOORDCOUNTASC等,例如咱們須要在返回結果中包含座標、中心距離、由近到遠排序的前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();
    });
}

3.4 刪除元素

有時候咱們可能須要刪除某個位置元素,可是RedisGeo並無刪除成員的命令。不過因爲它的底層是zset,咱們能夠藉助zrem命令進行刪除,對應的Java代碼爲:

redisTemplate.boundZSetOps(GEO_STAGE).remove("tianjin");

4. 總結

今天咱們使用RedisGeo特性實現了常見的附近的地理信息查詢需求,簡單易上手。其實使用另外一個Nosql數據庫MongoDB也能夠實現。在數據量比較小的狀況下Redis已經能很好的知足須要。若是數據量大可以使用MongoDB來實現。 文中涉及的DEMO可關注:碼農小胖哥 ,公衆號回覆 redisgeo獲取。

關注公衆號:Felordcn 獲取更多資訊

我的博客:https://felord.cn

相關文章
相關標籤/搜索