spring-data-redis存储经纬度数据(Geohash)并进行地理操作

Redis 从3.2.0版本开始提供了对地理位置的支持,reids 的地理位置坐标使用 sorted set存储,它使用Geohash技术,将经度和维度的二进制位交叉存储为一个 52 位的整数。在距离计算上,使用的是 Haversine 算法。在覆盖范围上,不能覆盖靠近南、北极的区域:

  • 有效的经度范围为 -180 到 180
  • 有效的维度范围为 -85.05112878 到05112878

相关的命令有:

  • GEOADD,添加成员的经纬度信息
  • GEODIST,计算成员间距离
  • GEORADIUS 基于经纬度坐标的范围查询
  • GEORADIUSBYMEMBER 基于成员位置范围查询
  • GEOPOS,获取成员经纬度
  • GEOHASH 计算经纬度Hash

Redis地理位置相关命令

GEOADD,添加成员的经纬度信息

语法:

GEOADD key longitude latitude member [longitude latitude member ...

以吉林省主要城市的经纬度为例:

>  geoadd citys 125.19 43.54 changchun
>  geoadd citys 122.50 45.38 baicheng
>  geoadd citys 126.26 41.56 baishan
>  geoadd citys 124.18 45.30 daan
>  geoadd citys 125.42 44.32 dehui 
>  geoadd citys 128.13 43.22 dunhua
>  geoadd citys 124.49 43.31 gongzhuling 
>  geoadd citys 129.00 42.32 helong
>  geoadd citys 126.44 42.58 huadian
>  geoadd citys 130.22 42.52 hunchun
>  geoadd citys 126.11 41.08 jian
>  geoadd citys 127.21 43.42 jiaohe 
>  geoadd citys 126.33 43.52 jilin
>  geoadd citys 125.51 44.09 jiutai
>  geoadd citys 125.09 42.54 liaoyuan
>  geoadd citys 126.53 41.49 linjiang
>  geoadd citys 129.26 42.46 longjing
>  geoadd citys 125.40 42.32 meihekou
>  geoadd citys 126.57 44.24 shulan
>  geoadd citys 124.22 43.10 siping
>  geoadd citys 124.49 45.11 songyuan
>  geoadd citys 122.47 45.20 taoyan
>  geoadd citys 125.56 41.43 tonghua
>  geoadd citys 129.51 42.57 tumen
>  geoadd citys 129.30 42.54 yanjin
>  geoadd citys 126.32 44.49 yushu

GEODIST,计算成员间距离

语法:

GEODIST key member1 member2 [unit]

unit 为结果单位,可选,支持:m,km,mi,ft,分别表示米(默认),千米,英里,英尺。

计算演示,计算长春到敦化的距离:

 > GEODIST citys changchun dunhua
"240309.2820"

 > GEODIST citys changchun dunhua km
"240.3093"

GEORADIUS,基于经纬度坐标的范围查询

检索以某个经纬度为圆心,在特定半径的圆形范围内的成员。

语法:

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

查询演示,以经纬度125,42为圆心,100公里范围内的成员:

>  GEORADIUS citys 125 42 100 km
1) "tonghua"
2) "meihekou"
3) "liaoyuan"

支持的可选项的意义:

  • WITHCOORD,同时获取成员经纬度
  • WITHDIST,同时获取距离参考点(圆心)的距离
  • WITHHASH,同时获取成员经纬度HASH值
  • COUNT count,限制获取成员的数量
  • ASC|DESC,结果升降序排序
  • STORE key,在命令表,READONLY模式下使用
  • STOREDIST key,在命令表,READONLY模式下使用

GEORADIUSBYMEMBER 基于成员位置范围查询

语法:

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

检索以某个成员为圆心,在特定半径的圆形范围内的成员。功能与 GEORADIUS 类似,只不过圆心为某个成员位置。

查询演示,以经纬度 changchun 为圆心,100公里范围内的成员:

> GEORADIUSBYMEMBER citys changchun 100 km
1) "siping"
2) "gongzhuling"
3) "changchun"
4) "jilin"
5) "jiutai"
6) "dehui"

GEOPOS,获取成员经纬度

语法:

GEOPOS key member [member ...]

获取某个成员经纬度:

> GEOPOS citys changchun
1) "125.19000023603439"
2) "43.539999086145414"

GEOHASH 计算经纬度Hash

语法:

GEOHASH key member [member ...]

获取将经纬度坐标生成的HASH字符串:

Python
1
2
> GEOHASH citys changchun
1) "wz9p8y0wfk0"

参考连接:Commands | Redis

SpringBoot 使用 Redis Geo

说明:以城市信息为目标,使用 StringRedisTemplate 操作 Redis 提供的关于 Geo 的6个命令。

版本信息

忽略了其他的pom配置信息

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.2.RELEASE</version>
		<relativePath/>
</parent>
...
<properties>
		<java.version>1.8</java.version>
</properties>
....
<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

vo 对象定义

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * <h1>城市信息</h1>
 * Created by Qinyi.
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CityInfo {

    /** 城市 */
    private String city;

    /** 经度 */
    private Double longitude;

    /** 纬度 */
    private Double latitude;
}

服务接口定义

import com.imooc.ad.vo.CityInfo;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;

import java.util.Collection;
import java.util.List;

/**
 * <h1>Geo 服务接口定义</h1>
 * Created by Qinyi.
 */
public interface IGeoService {

    /**
     * <h2>把城市信息保存到 Redis 中</h2>
     * @param cityInfos {@link CityInfo}
     * @return 成功保存的个数
     * */
    Long saveCityInfoToRedis(Collection<CityInfo> cityInfos);

    /**
     * <h2>获取给定城市的坐标</h2>
     * @param cities 给定城市 key
     * @return {@link Point}s
     * */
    List<Point> getCityPos(String[] cities);

    /**
     * <h2>获取两个城市之间的距离</h2>
     * @param city1 第一个城市
     * @param city2 第二个城市
     * @param metric {@link Metric} 单位信息, 可以是 null
     * @return {@link Distance}
     * */
    Distance getTwoCityDistance(String city1, String city2, Metric metric);

    /**
     * <h2>根据给定地理位置坐标获取指定范围内的地理位置集合</h2>
     * @param within {@link Circle} 中心点和距离
     * @param args {@link RedisGeoCommands.GeoRadiusCommandArgs} 限制返回的个数和排序方式, 可以是 null
     * @return {@link RedisGeoCommands.GeoLocation}
     * */
    GeoResults<RedisGeoCommands.GeoLocation<String>> getPointRadius(
            Circle within, RedisGeoCommands.GeoRadiusCommandArgs args);

    /**
     * <h2>根据给定地理位置获取指定范围内的地理位置集合</h2>
     * */
    GeoResults<RedisGeoCommands.GeoLocation<String>> getMemberRadius(
            String member, Distance distance, RedisGeoCommands.GeoRadiusCommandArgs args);

    /**
     * <h2>获取某个地理位置的 geohash 值</h2>
     * @param cities 给定城市 key
     * @return city geohashs
     * */
    List<String> getCityGeoHash(String[] cities);
}

服务接口实现

import com.alibaba.fastjson.JSON;
import com.imooc.ad.service.IGeoService;
import com.imooc.ad.vo.CityInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.GeoOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * <h1>Geo 服务接口实现</h1>
 * Created by Qinyi.
 */
@Slf4j
@Service
public class GeoServiceImpl implements IGeoService {

    private final String GEO_KEY = "ah-cities";

    /** redis 客户端 */
    private final StringRedisTemplate redisTemplate;

    @Autowired
    public GeoServiceImpl(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Long saveCityInfoToRedis(Collection<CityInfo> cityInfos) {

        log.info("start to save city info: {}.", JSON.toJSONString(cityInfos));

        GeoOperations<String, String> ops = redisTemplate.opsForGeo();

        Set<RedisGeoCommands.GeoLocation<String>> locations = new HashSet<>();
        cityInfos.forEach(ci -> locations.add(new RedisGeoCommands.GeoLocation<String>(
                ci.getCity(), new Point(ci.getLongitude(), ci.getLatitude())
        )));

        log.info("done to save city info.");

        return ops.add(GEO_KEY, locations);
    }

    @Override
    public List<Point> getCityPos(String[] cities) {

        GeoOperations<String, String> ops = redisTemplate.opsForGeo();

        return ops.position(GEO_KEY, cities);
    }

    @Override
    public Distance getTwoCityDistance(String city1, String city2, Metric metric) {

        GeoOperations<String, String> ops = redisTemplate.opsForGeo();

        return metric == null ?
            ops.distance(GEO_KEY, city1, city2) : ops.distance(GEO_KEY, city1, city2, metric);
    }

    @Override
    public GeoResults<RedisGeoCommands.GeoLocation<String>> getPointRadius(
            Circle within, RedisGeoCommands.GeoRadiusCommandArgs args
    ) {

        GeoOperations<String, String> ops = redisTemplate.opsForGeo();

        return args == null ?
                ops.radius(GEO_KEY, within) : ops.radius(GEO_KEY, within, args);
    }

    @Override
    public GeoResults<RedisGeoCommands.GeoLocation<String>> getMemberRadius(
            String member, Distance distance, RedisGeoCommands.GeoRadiusCommandArgs args
    ) {

        GeoOperations<String, String> ops = redisTemplate.opsForGeo();

        return args == null ?
                ops.radius(GEO_KEY, member, distance) : ops.radius(GEO_KEY, member, distance, args);
    }

    @Override
    public List<String> getCityGeoHash(String[] cities) {

        GeoOperations<String, String> ops = redisTemplate.opsForGeo();

        return ops.hash(GEO_KEY, cities);
    }
}

测试用例

import com.alibaba.fastjson.JSON;
import com.imooc.ad.Application;
import com.imooc.ad.vo.CityInfo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * <h1>GeoService 测试用例</h1>
 * Created by Qinyi.
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class GeoServiceTest {

    /** fake some cityInfos */
    private List<CityInfo> cityInfos;

    @Autowired
    private IGeoService geoService;

    @Before
    public void init() {

        cityInfos = new ArrayList<>();

        cityInfos.add(new CityInfo("hefei", 117.17, 31.52));
        cityInfos.add(new CityInfo("anqing", 117.02, 30.31));
        cityInfos.add(new CityInfo("huaibei", 116.47, 33.57));
        cityInfos.add(new CityInfo("suzhou", 116.58, 33.38));
        cityInfos.add(new CityInfo("fuyang", 115.48, 32.54));
        cityInfos.add(new CityInfo("bengbu", 117.21, 32.56));
        cityInfos.add(new CityInfo("huangshan", 118.18, 29.43));
    }

    /**
     * <h2>测试 saveCityInfoToRedis 方法</h2>
     * */
    @Test
    public void testSaveCityInfoToRedis() {

        System.out.println(geoService.saveCityInfoToRedis(cityInfos));
    }

    /**
     * <h2>测试 getCityPos 方法</h2>
     * 如果传递的 city 在 Redis 中没有记录, 会返回什么呢 ? 例如, 这里传递的 xxx
     * */
    @Test
    public void testGetCityPos() {

        System.out.println(JSON.toJSONString(geoService.getCityPos(
                Arrays.asList("anqing", "suzhou", "xxx").toArray(new String[3])
        )));
    }

    /**
     * <h2>测试 getTwoCityDistance 方法</h2>
     * */
    @Test
    public void testGetTwoCityDistance() {

        System.out.println(geoService.getTwoCityDistance("anqing", "suzhou", null).getValue());
        System.out.println(geoService.getTwoCityDistance("anqing", "suzhou", Metrics.KILOMETERS).getValue());
    }

    /**
     * <h2>测试 getPointRadius 方法</h2>
     * */
    @Test
    public void testGetPointRadius() {

        Point center = new Point(cityInfos.get(0).getLongitude(), cityInfos.get(0).getLatitude());
        Distance radius = new Distance(200, Metrics.KILOMETERS);
        Circle within = new Circle(center, radius);

        System.out.println(JSON.toJSONString(geoService.getPointRadius(within, null)));

        // order by 距离 limit 2, 同时返回距离中心点的距离
        RedisGeoCommands.GeoRadiusCommandArgs args =
                RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
        System.out.println(JSON.toJSONString(geoService.getPointRadius(within, args)));
    }

    /**
     * <h2>测试 getMemberRadius 方法</h2>
     * */
    @Test
    public void testGetMemberRadius() {

        Distance radius = new Distance(200, Metrics.KILOMETERS);

        System.out.println(JSON.toJSONString(geoService.getMemberRadius("suzhou", radius, null)));

        // order by 距离 limit 2, 同时返回距离中心点的距离
        RedisGeoCommands.GeoRadiusCommandArgs args =
                RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
        System.out.println(JSON.toJSONString(geoService.getMemberRadius("suzhou", radius, args)));
    }

    /**
     * <h2>测试 getCityGeoHash 方法</h2>
     * */
    @Test
    public void testGetCityGeoHash() {

        System.out.println(JSON.toJSONString(geoService.getCityGeoHash(
                Arrays.asList("anqing", "suzhou", "xxx").toArray(new String[3])
        )));
    }
}

原文:https://www.biaodianfu.com/redis-gis.html
作者:标点符

原文:SpringBoot 使用 Redis Geo_慕课手记
作者: 勤一