Redis的GEO使用

2023/11/23 缓存类型

# GEO语法

在 Redis 3.2 版本中,新增了存储地理位置信息的功能,即 GEO(英文全称 geographic),它的底层通过 Redis 有序集合(zset)实现。不过 Redis GEO 并没有与 zset 共用一套的命令,而是拥有自己的一套命令。

GEO操作是一种基于地理位置信息进行操作的功能。它使用经度和纬度坐标来表示地理位置,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能

# 1、GEOADD、GEOPOS

  • GEOADD 用于存储指定的地理空间位置的经度和纬度

  • GEOPOS 用于从给定的 key 里返回所有指定名称(member)的位置(经度和纬度)

# 语法

GEOADD key [ NX | XX] [CH] longitude latitude member [ longitude latitude member ...]
1

v6.2.0开始增加CH、NX、XX选项

选项说明:

  • XX:不添加新元素,只更新既有的元素

  • NX:不更新既有元素,只添加新元素

  • CH:将返回值由新添加的元素数量改为变更过的元素数量(同时包含新增的元素和变更的数量)

# 命令操作

127.0.0.1:6379> geoadd geo1 113.883078 22.553291 shenzhen
(integer) 1
127.0.0.1:6379> geoadd geo1 113.273241 23.157921 guangzhou
(integer) 1
127.0.0.1:6379> geopos geo1 shenzhen guangzhou
1) 1) "113.88307839632034302"
   2) "22.55329111565713873"
2) 1) "113.27324062585830688"
   2) "23.1579209662846921"
1
2
3
4
5
6
7
8
9

# Java操作

@Test
public void geoAddAndGeoPos() {
        String redisKey = "geo1";
        redisTemplate.delete(redisKey);
        // 设置深圳的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.883078, 22.553291), "shenzhen");
        // 设置广州的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.273241, 23.157921), "guangzhou");
        // 获取深圳的坐标
        List<Point> list = redisTemplate.opsForGeo().position(redisKey, "shenzhen", "guangzhou");
        log.info("获取深圳的坐标:{}", list.get(0));
        log.info("获取广州的坐标:{}", list.get(1));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
获取深圳的坐标:Point [x=113.883078, y=22.553291]
获取广州的坐标:Point [x=113.273241, y=23.157921]
1
2

# 2、GEODIST

  • GEODIST 用于返回两个给定位置之间的直线距离

# 语法

GEODIST key member1 member2 [ M | KM | FT | MI]
1

选项说明:

  • M: 单位:米

  • KM: 单位:公里(千米)

  • FT: 单位:英里

  • MI: 单位:英尺

# 命令操作

127.0.0.1:6379> geoadd geo2 113.883078 22.553291 shenzhen
(integer) 1
127.0.0.1:6379> geoadd geo2 113.273241 23.157921 guangzhou
(integer) 1
127.0.0.1:6379> geodist geo2 shenzhen guangzhou km
"91.8118"
1
2
3
4
5
6

# Java操作

@Test
public void geoDist() {
        String redisKey = "geo2";
        redisTemplate.delete(redisKey);
        // 设置深圳的坐标
        redisTemplate.opsForGeo().add(redisKey,new Point(113.883078,22.553291),"shenzhen");
        // 设置广州的坐标
        redisTemplate.opsForGeo().add(redisKey,new Point(113.273241,23.157921),"guangzhou");
        // 获取两个城市之间的距离
        Distance distance = redisTemplate.opsForGeo().distance(redisKey, "shenzhen", "guangzhou", RedisGeoCommands.DistanceUnit.KILOMETERS);
        log.info("获取两个城市之间的距离:{} 千米", distance.getValue());
}
1
2
3
4
5
6
7
8
9
10
11
12
获取两个城市之间的距离:91.8118 千米
1

# 3、GEORADIUS

  • GEORADIUS 根据给定的经纬度,返回半径不超过指定距离的元素

# 语法

GEORADIUS key longitude latitude radius M | KM | FT | MI [WITHCOORD] [WITHDIST] [WITHHASH] [ COUNT count [ANY]] [ ASC | DESC] [STORE key] [STOREDIST key]
1

v6.2.0开始此命令被视为废弃,可以用GEOSEARCH和GEOSEARCHSTORE代替

v6.2.0开始为COUNT参数添加了ANY选项

选项说明:

  • radius长度单位说明:

    • M:单位:米
    • KM:单位:公里(千米)
    • FT:单位:英里
    • MI:单位:英尺
  • with选项说明:

    • WITHDIST:同时返回匹配项与中心点的距离,距离单位与命令指定的半径单位相同
    • WITHCOORD:同时返回匹配项的经纬度坐标
    • WITHHASH:同时返回匹配项的GEOHASH值
  • 默认返回的匹配项是基于中心距离无序的,匹配项返回顺序说明:

    • ASC:按照距离中心点由近到远的顺序排序
    • DESC:按照距离中心距离由远到近的顺序排序
  • 命令默认返回区域内所有的匹配项,调用方可以通过COUNT参数指定需要返回的匹配项数量,当COUNT参数被提供了ANY参数时,命令将会尽快返回,即只要匹配项个数满足COUNT后立即返回。

  • 默认情况下,命令会将结果返回给客户端,但如果指定了存储选项,命令会将结果存储进对应的配置里

    • STORE:与原有序集合一样,存储地理位置的位置信息
    • STOREDIST:将匹配项距离中心点的距离作为分数存储进有序集合中

# 命令操作

127.0.0.1:6379> geoadd geo3 113.883078 22.553291 shenzhen
(integer) 1
127.0.0.1:6379> geoadd geo3 113.273241 23.157921 guangzhou
(integer) 1
127.0.0.1:6379> geoadd geo3 113.751791 23.020672 dongguan
(integer) 1
127.0.0.1:6379> geoadd geo3 113.392616 22.515951 zhongshan
(integer) 1
127.0.0.1:6379> georadius geo3 113.273241 23.157921 80 km
1) "guangzhou"
2) "dongguan"
3) "zhongshan"
127.0.0.1:6379> georadius geo3 113.273241 23.157921 80 km withdist
1) 1) "guangzhou"
   2) "0.0000"
2) 1) "dongguan"
   2) "51.2880"
3) 1) "zhongshan"
   2) "72.4447"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Java操作

@Test
public void geoRadius() {
        String redisKey = "geo3";
        redisTemplate.delete(redisKey);
        // 设置深圳的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.883078, 22.553291), "shenzhen");
        // 设置广州的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.273241, 23.157921), "guangzhou");
        // 设置东莞的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.751791, 23.020672), "dongguan");
        // 设置中山的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.392616, 22.515951), "zhongshan");
        // 以经纬度为中心,获取半径不超过最大距离的所有元素
        Point point = new Point(113.273241, 23.157921);
        // 半径为80km
        Distance distance = new Distance(80, RedisGeoCommands.DistanceUnit.KILOMETERS);
        Circle circle = new Circle(point, distance);
        GeoResults<RedisGeoCommands.GeoLocation<Object>> radius = redisTemplate.opsForGeo().radius(redisKey, circle);
        List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content= radius.getContent();
        for (GeoResult<RedisGeoCommands.GeoLocation<Object>> geoResult:content){
            RedisGeoCommands.GeoLocation<Object> geoResultContent = geoResult.getContent();
            log.info("获取半径不超过最大距离的所有元素:{}", geoResultContent.getName());
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
获取半径不超过最大距离的所有元素:guangzhou
获取半径不超过最大距离的所有元素:dongguan
获取半径不超过最大距离的所有元素:zhongshan
1
2
3

# 4、GEORADIUSBYMEMBER

  • GEORADIUSBYMEMBER 根据给定元素的经纬度,返回半径不超过指定距离的元素

# 语法

GEORADIUSBYMEMBER key member radius M | KM | FT | MI [WITHCOORD] [WITHDIST] [WITHHASH] [ COUNT count [ANY]] [ ASC | DESC] [STORE key] [STOREDIST key]
1

v3.2.0开始,v6.2.0开始被视为废弃,可以用命令GEOSEARCH替代

# 命令操作

127.0.0.1:6379> geoadd geo4 113.883078 22.553291 shenzhen
(integer) 1
127.0.0.1:6379> geoadd geo4 113.273241 23.157921 guangzhou
(integer) 1
127.0.0.1:6379> geoadd geo4 113.751791 23.020672 dongguan
(integer) 1
127.0.0.1:6379> geoadd geo4 113.392616 22.515951 zhongshan
(integer) 1
127.0.0.1:6379> georadiusbymember geo4 guangzhou 100 km withdist
1) 1) "guangzhou"
   2) "0.0000"
2) 1) "dongguan"
   2) "51.2880"
3) 1) "zhongshan"
   2) "72.4447"
4) 1) "shenzhen"
   2) "91.8118"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Java操作

@Test
public void geoRadiusByMember() {
        String redisKey = "geo4";
        redisTemplate.delete(redisKey);
        // 设置深圳的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.883078, 22.553291), "shenzhen");
        // 设置广州的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.273241, 23.157921), "guangzhou");
        // 设置东莞的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.751791, 23.020672), "dongguan");
        // 设置中山的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.392616, 22.515951), "zhongshan");
        // 定义距离
        Distance distance = new Distance(100, RedisGeoCommands.DistanceUnit.KILOMETERS);
        // 广州为中心点半径100km的元素
        GeoResults<RedisGeoCommands.GeoLocation<Object>> geoResults = redisTemplate.opsForGeo().radius(redisKey, "guangzhou", distance);
        List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = geoResults.getContent();
        for (GeoResult<RedisGeoCommands.GeoLocation<Object>> geoResult : content) {
            RedisGeoCommands.GeoLocation<Object> geoResultContent = geoResult.getContent();
            log.info("广州为中心点半径100km的元素:{}", geoResultContent.getName());
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
广州为中心点半径100km的元素:guangzhou
广州为中心点半径100km的元素:dongguan
广州为中心点半径100km的元素:zhongshan
广州为中心点半径100km的元素:shenzhen
1
2
3
4

# 5、GEOHASH

  • GEOHASH 用于获取一个或多个位置元素的 geohash 值

# 语法

GEOHASH KEY_NAME member [member ...]
1

# 命令操作

127.0.0.1:6379> geoadd geo5 113.883078 22.553291 shenzhen
(integer) 1
127.0.0.1:6379> geoadd geo5 113.273241 23.157921 guangzhou
(integer) 1
127.0.0.1:6379> geoadd geo5 113.751791 23.020672 dongguan
(integer) 1
127.0.0.1:6379> geoadd geo5 113.392616 22.515951 zhongshan
(integer) 1
127.0.0.1:6379> geohash geo5 shenzhen guangzhou dongguan zhongshan
1) "ws0br3xnkn0"
2) "ws0e9xg09v0"
3) "ws0fuqz90u0"
4) "ws08h6cuzm0"
1
2
3
4
5
6
7
8
9
10
11
12
13

# Java操作

@Test
public void geoHash() {
        String redisKey = "geo5";
        redisTemplate.delete(redisKey);
        // 设置深圳的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.883078, 22.553291), "shenzhen");
        // 设置广州的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.273241, 23.157921), "guangzhou");
        // 设置东莞的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.751791, 23.020672), "dongguan");
        // 设置中山的坐标
        redisTemplate.opsForGeo().add(redisKey, new Point(113.392616, 22.515951), "zhongshan");
        // 定义距离
        Distance distance = new Distance(100, RedisGeoCommands.DistanceUnit.KILOMETERS);
        // 获取各个城市的hash
        List<String> hashList = redisTemplate.opsForGeo().hash(redisKey, "shenzhen", "guangzhou", "dongguan", "zhongshan");
        log.info("获取各个城市的hash:{}", hashList);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
取各个城市的hash:[ws0br3xnkn0, ws0e9xg09v0, ws0fuqz90u0, ws08h6cuzm0]
1

# 6、GEOSEARCH

  • 此命令扩展于GEORADIUS,同时增加了对矩形区域的支持

# 语法

GEOSEARCH key FROMMEMBER member | FROMLONLAT longitude latitude BYRADIUS radius M | KM | FT | MI | BYBOX width height M | KM | FT | MI [ ASC | DESC] [ COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
1

v6.2.0开始

选项说明:

  • 指定中心点:

    • FROMMEMBER:以给定名称的位置为中心点
    • FROMLONLAT:以给定的经纬度为中心点
  • 指定区域形状:

    • BYRADIUS:以中心点为圆心,给定半径radius范围内的圆形区域
    • BYBOX:轴对称的矩形区域,具体范围由heightwidth决定
  • 返回值内容:

    • WITHDIST:同时返回匹配项与中心点的距离,距离单位与命令指定的半径单位相同
    • WITHCOORD:同时返回匹配项的经纬度坐标
    • WITHHASH:同时返回匹配项的GEOHASH值
  • 返回匹配项的顺序:

    • ASC:按照距离中心点由近到远的顺序排序
    • DESC:按照距离中心距离由远到近的顺序排序
  • 命令默认返回区域内所有的匹配项,调用方可以通过COUNT参数指定需要返回的匹配项数量,当COUNT参数被提供了ANY参数时,命令将会尽快返回,即只要匹配项个数满足COUNT后立即返回。

# 使用场景

想必大家都打过车,打车软件可以根据你的当前位置搜索附近的车辆:

大家出去玩可能会借用共享充电宝。它也是基于你的位置来搜索附近充电宝:

再就是大家搜索附近的酒店、餐厅等,也是基于位置的搜索。只要是跟距离经纬度相关的都可以考虑

# 代码示例

场景: 网约车相关功能,司机在空闲时,会在司机端定时上报其位置。当乘客下单后,会通过乘客的位置查询附近司机然后进行匹配

# 准备工作

pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <!-- redis -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9

GEO工具类

@Service
public class RedisGeoService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 添加经纬度信息
     * 
     * redis 命令:geoadd key 116.405285 39.904989 "北京"
     */
    public Long geoAdd(String key, Point point, String member) {
        if (redisTemplate.hasKey(key)) {
            redisTemplate.opsForGeo().remove(key, member);
        }
        return redisTemplate.opsForGeo().add(key, point, member);
    }

    /**
     * 查找指定key的经纬度信息,可以指定多个member,批量返回
     * 
     * redis命令:geopos key 北京
     */
    public List<Point> geoGet(String key, String... members) {
        return redisTemplate.opsForGeo().position(key, members);
    }

    /**
     * 返回两个地方的距离,可以指定单位,比如米m,千米km,英里mi,英尺ft
     * 
     * redis命令:geodist key 北京 上海
     */
    public Distance geoDist(String key, String member1, String member2, Metric metric) {
        return redisTemplate.opsForGeo().distance(key, member1, member2, metric);
    }

    /**
     * 根据给定的经纬度,返回半径不超过指定距离的元素
     * 
     * redis命令:georadius key 116.405285 39.904989 100 km WITHDIST WITHCOORD ASC
     * COUNT 5
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> nearByXY(String key, Circle circle, long count) {
        // includeDistance 包含距离
        // includeCoordinates 包含经纬度
        // sortAscending 正序排序
        // limit 限定返回的记录数
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                .includeDistance().includeCoordinates().sortAscending().limit(count);
        return redisTemplate.opsForGeo().radius(key, circle, args);
    }

    /**
     * 根据指定的地点查询半径在指定范围内的位置
     * 
     * redis命令:georadiusbymember key 北京 100 km WITHDIST WITHCOORD ASC COUNT 5
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> nearByPlace(String key, String member, Distance distance,
            long count) {
        // includeDistance 包含距离
        // includeCoordinates 包含经纬度
        // sortAscending 正序排序
        // limit 限定返回的记录数
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                .includeDistance().includeCoordinates().sortAscending().limit(count);
        return redisTemplate.opsForGeo().radius(key, member, distance, args);
    }

    /**
     * 返回的是geohash值
     * 
     * redis命令:geohash key 北京
     */
    public List<String> geoHash(String key, String member) {
        return redisTemplate.opsForGeo().hash(key, member);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

封装司机位置信息

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DriverPosition {

    /** 司机id */
    private String driverId;

    /** 城市编码 */
    private String cityCode;

    /** 经度 */
    private double lng;

    /** 纬度 */
    private double lat;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

controller

@RestController
@RequestMapping("redisGeo")
public class RedisGeoController {

    @Autowired
    private RedisGeoService redisGeoService;

    private final String GEO_KEY = "geo_key";

    /**
     * 使用redis+GEO,上报司机位置
     */
    @PostMapping("addDriverPosition")
    public Long addDriverPosition(String cityId, String driverId, Double lng, Double lat) {
        String redisKey = CommonUtil.buildRedisKey(GEO_KEY, cityId);
        Long addnum = redisGeoService.geoAdd(redisKey, new Point(lng, lat), driverId);

        List<Point> points = redisGeoService.geoGet(redisKey, driverId);
        System.out.println("添加位置坐标点:" + points);
        return addnum;
    }

    /**
     * 使用redis+GEO,查询附近司机位置
     */
    @GetMapping("getNearDrivers")
    public List<DriverPosition> getNearDrivers(String cityId, Double lng, Double lat) {
        String redisKey = CommonUtil.buildRedisKey(GEO_KEY, cityId);

        Circle circle = new Circle(lng, lat, Metrics.KILOMETERS.getMultiplier());
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisGeoService.nearByXY(redisKey, circle, 5);
        System.out.println("查询附近司机位置:" + results);

        List<DriverPosition> list = new ArrayList<>();
        results.forEach(item -> {
            GeoLocation<String> location = item.getContent();
            Point point = location.getPoint();
            DriverPosition position = DriverPosition.builder().cityCode(cityId).driverId(location.getName())
                    .lng(point.getX()).lat(point.getY()).build();
            list.add(position);
        });

        return list;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

通过高德地图 (opens new window)取点4个位置,所对应的坐标分别是:

东方雨林(114.366386, 30.408199)、怡景江南(114.365281, 30.406869)、梅南山居(114.368049, 30.412896)、武汉大学(114.365248, 30.537860)

其中前三个地址是在一起的,最后一个隔的很远

# 测试

使用postman,分别发送如下请求,添加司机的位置:

http://localhost:18081/redisGeo/addDriverPosition?cityId=420000&driverId=000001&lng=114.366386&lat=30.408199
http://localhost:18081/redisGeo/addDriverPosition?cityId=420000&driverId=000002&lng=114.365281&lat=30.406869
http://localhost:18081/redisGeo/addDriverPosition?cityId=420000&driverId=000003&lng=114.368049&lat=30.412896
http://localhost:18081/redisGeo/addDriverPosition?cityId=420000&driverId=000004&lng=114.365248&lat=30.537860
1
2
3
4

使用Redis Desktop Manager工具查看刚添加的数据:

可以看到,保存到redis的数据格式是ZSET,即有序集合。上面的key中包含了城市id,value表示司机id

接下来查询“东方雨林”附近的所有司机位置:http://localhost:18081/redisGeo/getNearDrivers?cityId=420000&lng=114.366386&lat=30.408199

控制台打印日志如下:

GeoResults: [averageDistance: 242.78286666666668 METERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=000001, point=Point [x=114.366386, y=30.408199]), distance: 0.0521 METERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=000002, point=Point [x=114.365281, y=30.406869]), distance: 182.0457 METERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=000003, point=Point [x=114.368049, y=30.412896]), distance: 546.2508 METERS, ]]
1

上面的结果,包含间隔距离的平均值,附近坐标点经纬度、间隔距离,同时结果是按间隔距离正序排序的

[
    {
        "driverId": "000001",
        "cityCode": "420000",
        "lng": 114.36638563871384,
        "lat": 30.408199349640434
    },
    {
        "driverId": "000002",
        "cityCode": "420000",
        "lng": 114.3652805685997,
        "lat": 30.406868621031784
    },
    {
        "driverId": "000003",
        "cityCode": "420000",
        "lng": 114.36804860830307,
        "lat": 30.412896187948697
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

再来试下“武汉大学”附近的司机位置,请求返回结果如下:

[
    {
        "driverId": "000004",
        "cityCode": "420000",
        "lng": 114.36524838209152,
        "lat": 30.537860475825262
    }
]
1
2
3
4
5
6
7
8

# 参考文章

https://mp.weixin.qq.com/s/xAShLTL-CjI9ManD-TVq1A

https://www.cnblogs.com/xuwenjin/p/12715339.html