Redis的GEO使用
# 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 ...]
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"
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));
}
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]
2
# 2、GEODIST
- GEODIST 用于返回两个给定位置之间的直线距离
# 语法
GEODIST key member1 member2 [ M | KM | FT | MI]
选项说明:
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"
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());
}
2
3
4
5
6
7
8
9
10
11
12
获取两个城市之间的距离:91.8118 千米
# 3、GEORADIUS
- GEORADIUS 根据给定的经纬度,返回半径不超过指定距离的元素
# 语法
GEORADIUS key longitude latitude radius M | KM | FT | MI [WITHCOORD] [WITHDIST] [WITHHASH] [ COUNT count [ANY]] [ ASC | DESC] [STORE key] [STOREDIST key]
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"
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());
}
}
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
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]
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"
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());
}
}
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
2
3
4
# 5、GEOHASH
- GEOHASH 用于获取一个或多个位置元素的 geohash 值
# 语法
GEOHASH KEY_NAME member [member ...]
# 命令操作
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"
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
取各个城市的hash:[ws0br3xnkn0, ws0e9xg09v0, ws0fuqz90u0, ws08h6cuzm0]
# 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]
v6.2.0开始
选项说明:
指定中心点:
- FROMMEMBER:以给定名称的位置为中心点
- FROMLONLAT:以给定的经纬度为中心点
指定区域形状:
- BYRADIUS:以中心点为圆心,给定半径
radius范围内的圆形区域 - BYBOX:轴对称的矩形区域,具体范围由
height、width决定
- BYRADIUS:以中心点为圆心,给定半径
返回值内容:
- 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>
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);
}
}
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;
}
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;
}
}
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
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, ]]
上面的结果,包含间隔距离的平均值,附近坐标点经纬度、间隔距离,同时结果是按间隔距离正序排序的
[
{
"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
}
]
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
}
]
2
3
4
5
6
7
8
# 参考文章
https://mp.weixin.qq.com/s/xAShLTL-CjI9ManD-TVq1A
https://www.cnblogs.com/xuwenjin/p/12715339.html