使用场景

通常地理位置距离排序算法 GeoHash 算法,GeoHash是一种优化的网格算法,能够高效的对经纬度进行比较,也是Redis是所使用的算法。比如,我们的线下教育课拼课的时候需要定位哪几个小朋友的位置距离上课的地点的比较接近。比如我们现场面试的时候,需要查一下面试的公司的位置,以及距离有多远。以及我们在查询附近的共享单车🚴♀️的停车点距离我们有多远等等。

内容概括

方法

概述

Long add(K key, Point point, M member);

添加元素

List hash(K key, M… members);

获取地理位置的哈希值

List position(K key, M… members);

根据key和元素获取地理位置数据

Distance distance(K key, M member1, M member1, Metric metric);

计算两个元素之间的距离

GeoResults<GeoLocation> radius(K key, M member, Distance distance, GeoRadiusCommandArgs args);

查询指定元素附近的其它元素

GeoResults<GeoLocation> radius(K key, Circle within, GeoRadiusCommandArgs args);

查询指定元素附近的其它元素-重载方法

Redis中 的 Geo指令基本使用

添加经纬度元素命令

127.0.0.1:6379> geoadd location 116.307629 40.058359 baidu
(integer) 1

获取哈希值命令

127.0.0.1:6379> geohash location baidu
1) "wx4eysksu50"

获取地理位置命令

127.0.0.1:6379> geopos location baidu
1) 1) "116.3076290488243103"
   2) "40.05835933082178002"

计算两个元素之间的距离命令

127.0.0.1:6379> GEODIST location baidu meituan km
"16.4455"

查询指定元素附近的其它元素命令

# withdist 它可以用来显示距离
# asc 排序
# count 3 打印几条数据
127.0.0.1:6379> GEORADIUSBYMEMBER location baidu 20 km withcoord withdist withhash count 3 asc
1) 1) "baidu"
   2) "0.0000"
   3) (integer) 4069883733203026
   4) 1) "116.3076290488243103"
      2) "40.05835933082178002"
2) 1) "xiaomi"
   2) "4.1227"
   3) (integer) 4069880904286516
   4) 1) "116.33425265550613403"
      2) "40.02740024658161389"
3) 1) "juejin"
   2) "16.2804"
   3) (integer) 4069887154388167
   4) 1) "116.48104995489120483"
      2) "39.99679348858259686"

代码中使用Redis中 的 Geo

在Reids中的加入几个经纬度数据

源码解读

// 根据指定key将指定元素添加到redis中。key:redis的key,point:封装经纬度的对象,member:元素
@Nullable
Long add(K key, Point point, M member);

案例演示

/**
 * 地理位置的初始化数据
 */
@Test
public void test_user_simple_geohash_init(){
    String key = "location";
    stringRedisTemplate.opsForGeo().add(key, new Point(116.48105,39.996794), "juejin");
    stringRedisTemplate.opsForGeo().add(key, new Point(116.514203, 39.905409), "ireader");
    stringRedisTemplate.opsForGeo().add(key, new Point(116.489033, 40.007669), "meituan");
    stringRedisTemplate.opsForGeo().add(key, new Point(116.562108, 39.787602), "jd");
    stringRedisTemplate.opsForGeo().add(key, new Point(116.334255, 40.027400), "xiaomi");
}

获取地理位置的哈希值

源码解读

// 获取指定key中的元素的哈希值。key:指定的key,members:要获取哈希值的元素
@Nullable
List<String> hash(K key, M... members);

案例演示

/**
 * 获取地理位置的哈希值
 */
@Test
public void test_user_simple_geohash_hash(){
    String key = "location";
    List<String> position = stringRedisTemplate.opsForGeo().hash(key, "juejin", "ireader", "meituan", "jd", "xiaomi");
    assert position != null;
    position.forEach(System.out::println);
}

打印结果

wx4gd94yjn0
wx4g52e1ce0
wx4gdg0tx40
wx4fk7jgtf0
wx4exqb0880

根据key和元素获取地理位置数据

源码解读

// 根据key和元素获取地理位置数据。key:指定的key,members:指定的元素
@Nullable
List<Point> position(K key, M... members);

案例演示

/**
 * 获取地理位置数据
 */
@Test
public void test_user_simple_geohash_get(){
    String key = "location";
    // 一次性获取多个,也可以获取一个
    List<Point> position = stringRedisTemplate.opsForGeo().position(key, "juejin", "ireader", "meituan", "jd", "xiaomi");
    assert position != null;
    position.forEach(System.out::println);
}

打印结果

Point [x=116.481050, y=39.996793]
Point [x=116.514202, y=39.905409]
Point [x=116.489032, y=40.007670]
Point [x=116.562106, y=39.787603]
Point [x=116.334253, y=40.027400]

计算两个元素之间的距离

源码解读

// 计算两个元素之间的距离。key:指定的key,member1|member1:两个元素,Metric:计量单位,米、千米……
@Nullable
Distance distance(K key, M member1, M member1, Metric metric);

案例演示

/**
 * 计算两个元素之间的距离
 */
@Test
public void test_user_simple_geohash_distance(){
    String key = "location";
    Distance distance = stringRedisTemplate.opsForGeo().distance(key, "juejin", "ireader", RedisGeoCommands.DistanceUnit.KILOMETERS);
    assert distance != null;
    System.out.println(distance.getValue() + " KM");
}

打印结果

10.5501 KM

查询指定元素附近的其它元素

源码解读

// 查询指定元素附近的其它元素,指定范围查询,这个查询会返回自己
@Nullable
GeoResults<GeoLocation<M>> radius(K key, M member, Distance distance, GeoRadiusCommandArgs args);

案例演示

/**
 * 查询指定元素附近的其它元素
 */
@Test
public void test_user_simple_geohash_radius(){
    String key = "location";
    // 获取距离掘金3km以内公司,范围查找,正序排列
    RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
            .sortAscending()// 排序
            .limit(2)// 输出元素的个数
            .includeCoordinates()// 输出经纬度
            .includeDistance();// 距离
    // 封装距离参数
    Distance distance = new Distance(3d, RedisGeoCommands.DistanceUnit.KILOMETERS);
    // 获取值
    GeoResults<RedisGeoCommands.GeoLocation<String>> radius = stringRedisTemplate.opsForGeo().radius(key, "juejin", distance, geoRadiusCommandArgs);
    assert radius != null;
    radius.getContent().forEach(s -> System.out.println("名称:" + s.getContent().getName() + "\n      经纬度:" + s.getContent().getPoint() + "\n      距离:" + s.getDistance()));
}

打印结果

名称:juejin
      经纬度:Point [x=116.481050, y=39.996793]
      距离:0.0 KILOMETERS
名称:meituan
      经纬度:Point [x=116.489032, y=40.007670]
      距离:1.3878 KILOMETERS

查询指定元素附近的其它元素-重载方法

源码解读

// 上面那个方法的一个重载方法
@Nullable
GeoResults<GeoLocation<M>> radius(K key, Circle within, GeoRadiusCommandArgs args);

案例演示

/**
 * todo 重载方法
 * 查询指定元素附近的其它元素
 */
@Test
public void test_user_simple_geohash_radius_circle() {
    String key = "location";
    // 获取距离掘金3km以内公司,范围查找,正序排列
    RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
            .sortAscending()// 排序
            .limit(2)// 输出元素的个数
            .includeCoordinates()// 输出经纬度
            .includeDistance();// 距离
    // 封装距离参数
    Distance distance = new Distance(3d, RedisGeoCommands.DistanceUnit.KILOMETERS);
    // 封装经纬度参数
    Point point = new Point(116.48105,39.996794);
    // 封装范围参数
    Circle circle = new Circle(point, distance);
    GeoResults<RedisGeoCommands.GeoLocation<String>> radius = stringRedisTemplate.opsForGeo().radius(key, circle, geoRadiusCommandArgs);
    assert radius != null;
    // 获取到的距离可能回是科学计数法得到的
    radius.getContent().forEach(s -> System.out.println("名称:" + s.getContent().getName() + "\n      经纬度:" + s.getContent().getPoint() + "\n      距离:" + s.getDistance()));
}

打印结果

名称:juejin
      经纬度:Point [x=116.481050, y=39.996793]
      距离:1.0E-4 KILOMETERS
名称:meituan
      经纬度:Point [x=116.489032, y=40.007670]
      距离:1.3878 KILOMETERS

注意

Redis 的 Geo 数据结构,会将全部数据放在一个 zset 集合中。在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成 较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行,建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。

数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按 市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。