1. Introduction

The boss suddenly want to line up a demand, obtain the current location of a kilometer radius of the business agent. Launch tomorrow! I almost vomited blood when I received the request. It was so tight. Check out the relevant technology selection. After a lot of struggle, I finally fulfilled this requirement at 10 PM. Now summarize the idea of general implementation.

2. MySQL is inappropriate

When you meet a need, you should first think about whether the existing things can be met and what the cost is.

MySQL is the first thing that comes to mind because most of the data is persisted to MySQL. But using MySQL requires you to compute the Geohash yourself. It requires a lot of mathematical and geometric calculations, and requires the learning of geographical knowledge, which has a high threshold and is impossible to complete in a short period of time. And in the long run, this is not MySQL’s area of expertise, so it was not considered.

Geohash reference www.cnblogs.com/LBSer/p/331…

2. GEO in Redis

Redis is the most familiar K-V database, it is often used as a high-performance cache database, most projects will use it. Starting with version 3.2, it began to provide GEO capabilities for location-dependent functions such as nearby locations, calculating distances, and so on. The commands related to GEO are as follows:

Redis command describe
GEOHASH Of one or more positional elementsGeohashsaid
GEOPOS Returns the position (latitude and longitude) of all elements in a given position from key
GEODIST Returns the distance between two given positions
GEORADIUS Find elements within a radius centered on a given latitude and longitude
GEOADD Adds the specified geospatial location (latitude, longitude, name) to the specified key
GEORADIUSBYMEMBER Finds the element in the specified range, the center point is determined by the given location element

Redis will assume that the earth is perfectly spherical, so there may be some deviation in position calculation, said to be <=0.5%, for requirements with strict geographical location requirements, there will be some scenario testing to verify whether the requirements can be met.

2.1 Write geographic information

So how do you achieve all the elements within the target unit radius? We can convert the latitude and longitude of all locations into a 52-bit Geohash written to Redis using the GEOADD in the table above.

The command format is as follows:

geoadd key longitude latitude member [longitude latitude member ...]
Copy the code

Corresponding examples:

Redis > Geoadd Cities: LOCs 117.12 39.08 tianjin 114.29 38.02 Shijiazhuang (Shijiazhuang)integer2)Copy the code

Add tianjin with 117.12 latitude 39.08 and Tianjin with 114.29 longitude 38.02 latitude Shijiazhuang to the sorted set whose key is Cities :locs. You can add one to multiple locations. Then we can use other commands to calculate the location.

Effective longitude ranges from -180 degrees to 180 degrees. Valid latitudes range from -85.05112878 degrees to 85.05112878 degrees. This command returns an error when the coordinates are outside the range specified above.

2.2 Statistics of areas within the unit radius

With the help of GEORADIUS, we can find all elements within a given radius of latitude and longitude.

The command format is as follows:

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

This command is a little more complicated than GEOADD:

  • radiusThis parameter is mandatory. At the back of them,km,ft,mi, is the length unit option, choose one of four.
  • WITHCOORD returns the longitude and dimension of the positional element, optional.
  • WITHDIST returns both the location element and the distance between the location element and the center point. The distance unit is the same as the query unit.
  • WITHHASH Returns a 52-bit Geohash value for the position. Optional. I rarely use this anyway, so maybe some other low-level LBS application services need this.
  • COUNT returns the number of positional elements that match the condition. This is not mandatory. For example, return the top 10 to avoid performance problems with too many results that match.
  • ASC | DESC sorting way, not a choice. By default it returns unsorted, but most of the time we need to sort it. Referring to the center location, use ASC from near to far and DESC from far to near.

For example, if we look for cities:locs centered around (115.03, 38.44) with a radius of 200km, the result contains the city name, corresponding coordinates, and distance (km) from the center, arranged in order from near to far. The command is as follows:

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"
Copy the code

You can add COUNT 1 to find the nearest location.

3. Redis GEO combat

The general principle of thought said, the next is the actual operation. What should we do with Spring Boot applications?

3.1 Development Environment

You need a Redis version with GEO features, and HERE I’m using Redis 4. In addition, our client uses spring-boot-starter-data-redis. Here we’re going to use the RedisTemplate object.

3.2 Adding Location Information in Batches

As a first step, we need to initialize the location data into Redis. A position coordinate of the Spring Data Redis (LNG, lat) can be encapsulated to org. Springframework. Data. Geo. Point object. Then specify a name that constitutes a location Geo information. RedisTemplate provides a way to add location information in batches. We can convert the add command in Section 2.1 to the following code:

   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 add Geo in batch
   redisTemplate.boundGeoOps("cities:locs").add(points);
Copy the code

This can be done in combination with the ApplicationRunner interface provided by Spring Boot.

@Bean
public ApplicationRunner cacheActiveAppRunner(RedisTemplate<String, String> redisTemplate) {

    return args -> {
        final String GEO_KEY = "cities:locs";

        // Clear the cache
        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 adds GeoLocation in bulk
        BoundGeoOperations<String, String> geoOps = redisTemplate.boundGeoOps(GEO_KEY);
        geoOps.add(points);
    };
}
Copy the code

Geographic data is persisted to MySQL and then synchronized to Redis.

3.3 Querying a specific nearby location

The RedisTemplate also has a wrapper for the GEORADIUS command:

GeoResults<GeoLocation<M>> radius(K key, Circle within, GeoRadiusCommandArgs args)
Copy the code

The Circle object is the area covered by encapsulation (Figure 1). The required elements are the central Point coordinate Point object, radius, and metric, for example:

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);
Copy the code

GeoRadiusCommandArgs is used to encapsulate some of the optional command parameters of GEORADIUS, see WITHCOORD, COUNT, ASC, etc., in Section 2.2. For example, we need to include coordinates, center distance, and the first five data sorted from near to far in the return result:

RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands
        .GeoRadiusCommandArgs
        .newGeoRadiusArgs()
        .includeDistance()
        .includeCoordinates()
        .sortAscending()
        .limit(limit);
Copy the code

Radius, then execute method will get GeoResults < RedisGeoCommands. GeoLocation < String > > encapsulated as a result, we will be paid to this object iterative parsing data we want:

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 name, such as tianjin
        String name = content.getName();
        // Corresponding latitude and longitude coordinates
        Point pos = content.getPoint();
        // The distance from the center
        Distance dis = geoLocationGeoResult.getDistance();
    });
}
Copy the code

3.4 Deleting Elements

Sometimes we need to delete a location element, but Redis Geo does not have a command to delete members. However, since its underlying layer is Zset, we can use zrem command to delete it, and the corresponding Java code is as follows:

redisTemplate.boundZSetOps(GEO_STAGE).remove("tianjin");
Copy the code

4. To summarize

Today we use Redis Geo feature to achieve common nearby geographical information query requirements, simple and easy to use. You can also use MongoDB, another Nosql database. In the case of a relatively small amount of data, Redis has been able to meet the needs well. If the amount of data is large, MongoDB can be used to achieve it. The DEMO involved in the article can be concerned about: code farmer xiao Pangge, the public number to reply to Redisgeo to obtain.

Follow our public id: Felordcn for more information

Personal blog: https://felord.cn