Redis实战 - 11 Redis GEO 实现附近的人功能
创始人
2024-03-30 10:52:26

各种社交软件里面都有附件的人的需求,在该应用中,我们查询附近1公里的食客,同时只需查询出20个即可。

文章目录

    • 1. Redis GEO常用命令
    • 2. 上传用户地理位置
      • 1. RedisKeyConstant
      • 2. 控制层 NearMeController
      • 3. 业务层 NearMeService
      • 4. 项目测试
      • 5. jmeter 构造数据
    • 3. 查找附近的人
      • 1. 视图 NearMeDinerVO
      • 2. 控制层 NearMeController
      • 3. 业务层 NearMeService
      • 4. 项目测试

1. Redis GEO常用命令

命令功能参数
GEOADD添加地理位置GEOADD key longitude latitude member [longitude latitude member …]
GEODIST两点间的距离GEODIST key member1 member2 [unit]
GEOHASH返回标准的Geohash值GEOHASH key member [member …]
GEOPOS返回key中给定元素的位置信息(经纬度)GEOPOS key member [member …]
GEOREDIUS返回以某点为圆心,距离为半径的其他位置元素GEOREDIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
GEORADIUSBYMEMBER跟GEOREDIUS一样,只不过圆心是给定的member元素GEORADIUSBYMEMBER key longitude latitude radius m | km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]

GEOADD key longitude latitude member [longitude latitude member …]

将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。

# 添加单个位置
127.0.0.1:6379> GEOADD diner:location 121.446617 31.205593 'zhangsan'
(integer) 1# 添加多个位置信息
127.0.0.1:6379> GEOADD diner:location 121.4465774 31.20485103  'lisi' 121.44534  31.2031 'wangwu'  121.4510648 31.2090667 'zhangliu'
(integer) 3

GEODIST key member1 member2 [unit]

返回两个给定位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。 其中unit为单位 m|km|ft(英尺)|mi(英里)。

# 计算两点间的距离,返回距离的单位是米(m)
127.0.0.1:6379> GEODIST diner:location zhangsan lisi m
"82.4241"

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

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。范围可以使用以下其中一个单位:m 表示单位为米。km 表示单位为千米。

在给定以下可选项时, 命令会返回额外的信息:

WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。

WITHCOORD: 将位置元素的经度和维度也一并返回。

WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。

命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

ASC: 根据中心的位置, 按照从近到远的方式返回位置元素。

DESC: 根据中心的位置, 按照从远到近的方式返回位置元素。

# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其位置
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHCOORD
1) 1) "wangwu"2) 1) "121.44534140825271606"2) "31.20310057881493293"
2) 1) "lisi"2) 1) "121.44657522439956665"2) "31.20485207113603821"
3) 1) "zhangsan"2) 1) "121.44661813974380493"2) "31.20559220971455971"
4) 1) "zhangliu"2) 1) "121.45106524229049683"2) "31.20906731242401833"# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其距离(单位是米)
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHDIST
1) 1) "wangwu"2) "302.6202"
2) 1) "lisi"2) "82.5066"
3) 1) "zhangsan"2) "0.1396"
4) 1) "zhangliu"2) "573.0651"# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其距离(单位是米) 由近及远
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHDIST ASC
1) 1) "zhangsan"2) "0.1396"
2) 1) "lisi"2) "82.5066"
3) 1) "wangwu"2) "302.6202"
4) 1) "zhangliu"2) "573.0651"# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其GeoHash值
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHHASH
1) 1) "wangwu"2) (integer) 4054756135204337
2) 1) "lisi"2) (integer) 4054756138536712
3) 1) "zhangsan"2) (integer) 4054756138736536
4) 1) "zhangliu"2) (integer) 4054756186304127# 以121.446617 31.205593(张三位置)为圆心,3000m为半径,查询返回用户及其GeoHash值去2个
127.0.0.1:6379> GEORADIUS diner:location 121.446617 31.205593 3000 m WITHHASH COUNT 2
1) 1) "zhangsan"2) (integer) 4054756138736536
2) 1) "lisi"2) (integer) 4054756138536712

GEOPOS key member [member …]

key里返回所有给定位置元素的位置(经度和纬度)。

GEOHASH key member [member …]

返回一个或多个位置元素的 Geohash 表示。保存到 Redis 中是用 Geohash 位置52点整数编码。

GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的GeoHash字符串都是WX4ER,所以可以把WX4ER当作key,把该区域的餐馆信息当作value来进行缓存,而如果不使用GeoHash的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。字符串越长,表示的范围越精确。

09185339-add66a56b3da417ab00370e354c74667.png

# 计算某个位置的GeoHash值
127.0.0.1:0>GEOHASH diner:location zhangsan1)  "wtw3e8f9z20"

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

这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点指定成员的位置被用作查询的中心。

127.0.0.1:6379> geoadd haha 12.1 13.1 zhangsan 12.2 13.2 lisi
(integer) 2
127.0.0.1:6379> geodist haha zhangsan lisi
"15524.7516"
127.0.0.1:6379> georadius haha 13 14 200 km withdist
1) 1) "zhangsan"2) "139.6108"
2) 1) "lisi"2) "124.0861"
127.0.0.1:6379> georadius haha 13 14 200 km withcoord
1) 1) "zhangsan"2) 1) "12.10000008344650269"2) "13.10000039220049217"
2) 1) "lisi"2) 1) "12.19999819993972778"2) "13.20000021137872892"
127.0.0.1:6379> georadius haha 13 14 200 km withdist withcoord
1) 1) "zhangsan"2) "139.6108"3) 1) "12.10000008344650269"2) "13.10000039220049217"
2) 1) "lisi"2) "124.0861"3) 1) "12.19999819993972778"2) "13.20000021137872892"
127.0.0.1:6379> geopos haha zhangsan lisi
1) 1) "12.10000008344650269"2) "13.10000039220049217"
2) 1) "12.19999819993972778"2) "13.20000021137872892"
127.0.0.1:6379> geohash haha zhangsan lisi
1) "s62de2fcn30"
2) "s62ejdgz8d0"
127.0.0.1:6379> georadiusbymember haha zhangsan 100 km
1) "zhangsan"
2) "lisi"
127.0.0.1:6379>

2. 上传用户地理位置

在ms-diners服务中编写功能

1. RedisKeyConstant

@Getter
public enum RedisKeyConstant {/*** redis 的 key*/diner_location("diner:location", "diner地理位置Key"),;private String key;private String desc;RedisKeyConstant(String key, String desc) {this.key = key;this.desc = desc;}
}

GEOADD key longitude latitude member [longitude latitude member …]

2. 控制层 NearMeController

传入登录用户的lon(经度)和lat(纬度)信息,一般根据实际情况,客户端要定时去获取用户的地理位置进行上传(5s中一般刷新一次)

@RestController
@RequestMapping("nearme")
public class NearMeController {@Resourceprivate HttpServletRequest request;@Resourceprivate NearMeService nearMeService;/*** 更新食客坐标** @param access_token* @param lon* @param lat* @return*/@PostMappingpublic ResultInfo updateDinerLocation(String access_token,@RequestParam Float lon,@RequestParam Float lat) {nearMeService.updateDinerLocation(access_token, lon, lat);return ResultInfoUtil.buildSuccess(request.getServletPath(), "更新成功");}
}

3. 业务层 NearMeService

保存的key为:diner:location,member 为 dinerId

@Service
public class NearMeService {@Resourceprivate DinersService dinersService;@Value("${service.name.ms-oauth-server}")private String oauthServerName;@Resourceprivate RestTemplate restTemplate;@Resourceprivate RedisTemplate redisTemplate;/*** 更新食客坐标** @param accessToken 登录用户 token* @param lon         经度* @param lat         纬度*/public void updateDinerLocation(String accessToken, Float lon, Float lat) {// 参数校验AssertUtil.isTrue(lon == null, "获取经度失败");AssertUtil.isTrue(lat == null, "获取纬度失败");// 获取登录用户信息SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);// 获取 key diner:locationString key = RedisKeyConstant.diner_location.getKey();// 将用户地理位置信息存入 RedisRedisGeoCommands.GeoLocation geoLocation = new RedisGeoCommands.GeoLocation(signInDinerInfo.getId(), new Point(lon, lat));redisTemplate.opsForGeo().add(key, geoLocation);}
}

4. 项目测试

http://localhost/diners/nearme?access_token=76fc8f1c-1633-4dde-995b-7627d34616d8&lon=121.446617&lat=31.205593

在这里插入图片描述

5. jmeter 构造数据

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3. 查找附近的人

传入登录用户token,同时传入查询范围(默认3000m)以及当前用户的lon(经),lat(纬)度,为什么要传入用户此时的经纬度呢,这样查出来的结果更加准确。有可能用户处于移动状态。

1. 视图 NearMeDinerVO

@Getter
@Setter
@ApiModel(description = "关注食客信息")
public class ShortDinerInfo implements Serializable {@ApiModelProperty("主键")public Integer id;@ApiModelProperty("昵称")private String nickname;@ApiModelProperty("头像")private String avatarUrl;}
@ApiModel(description = "附近的人")
@Getter
@Setter
public class NearMeDinerVO extends ShortDinerInfo {@ApiModelProperty(value = "距离", example = "98m")private String distance;
}

2. 控制层 NearMeController

@RestController
@RequestMapping("nearme")
public class NearMeController {@Resourceprivate HttpServletRequest request;@Resourceprivate NearMeService nearMeService;/*** 获取附近的人** @param access_token 登录凭证* @param radius 半径* @param lon 登录用户的经度* @param lat 登录用户的纬度*/@GetMappingpublic ResultInfo nearMe(String access_token,Integer radius,Float lon, Float lat) {List nearMe = nearMeService.findNearMe(access_token, radius, lon, lat);return ResultInfoUtil.buildSuccess(request.getServletPath(), nearMe);}
}

3. 业务层 NearMeService

  • 获取登录用户id
  • 获取查询半径,以米为单位,默认3000m
  • 获取用户的经纬度,如果客户端没上传经纬度,那么从Redis中读取经纬度
  • 格式化查询的半径,使用RedisTemplate的Distance对象
  • 查询限制条件:限制20,返回包含距离,按由近及远排序
  • 格式化结果,将其封装到Map中,Key为dinerId,Value构建返回的VO,同时格式化distance属性,方便客户端展示
  • 查询附近的人的信息,并添加到对应的VO中
  • 返回结果
@Service
public class NearMeService {@Resourceprivate DinersService dinersService;@Value("${service.name.ms-oauth-server}")private String oauthServerName;@Resourceprivate RestTemplate restTemplate;@Resourceprivate RedisTemplate redisTemplate;/*** 获取附近的人** @param accessToken 用户登录 token* @param radius      半径,默认 1000m* @param lon         经度* @param lat         纬度* @return*/public List findNearMe(String accessToken,Integer radius,Float lon, Float lat) {// 获取登录用户信息SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);// 食客 IDInteger dinerId = signInDinerInfo.getId();// 处理半径,默认 3000mif (radius == null) {radius = 3000;}// 获取 keyString key = RedisKeyConstant.diner_location.getKey();// 获取用户经纬度Point point = null;if (lon == null || lat == null) {// 如果经纬度没传,那么从 Redis 中获取List points = redisTemplate.opsForGeo().position(key, dinerId);AssertUtil.isTrue(points == null || points.isEmpty(), "获取经纬度失败");point = points.get(0);} else {point = new Point(lon, lat);}// 初始化距离对象,单位 mDistance distance = new Distance(radius, RedisGeoCommands.DistanceUnit.METERS);// 初始化 Geo 命令参数对象RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs();// 附近的人限制 20,包含距离,按由近到远排序args.limit(20).includeDistance().sortAscending();// 以用户经纬度为圆心,范围 3000mCircle circle = new Circle(point, distance);// 获取附近的人 GeoLocation 信息GeoResults geoResult = redisTemplate.opsForGeo().radius(key, circle, args);// 构建有序 MapMap nearMeDinerVOMap = Maps.newLinkedHashMap();// 完善用户头像昵称信息geoResult.forEach(result -> {RedisGeoCommands.GeoLocation geoLocation = result.getContent();// 初始化 Vo 对象NearMeDinerVO nearMeDinerVO = new NearMeDinerVO();nearMeDinerVO.setId(geoLocation.getName());// 获取距离Double dist = result.getDistance().getValue();// 四舍五入精确到小数点后 1 位,方便客户端显示String distanceStr = NumberUtil.round(dist, 1).toString() + "m";nearMeDinerVO.setDistance(distanceStr);nearMeDinerVOMap.put(geoLocation.getName(), nearMeDinerVO);});// 获取附近的人的信息(根据 Diner 服务接口获取)Integer[] dinerIds = nearMeDinerVOMap.keySet().toArray(new Integer[]{});List shortDinerInfos = dinersService.findByIds(StrUtil.join(",", dinerIds));// 完善昵称头像信息shortDinerInfos.forEach(shortDinerInfo -> {NearMeDinerVO nearMeDinerVO = nearMeDinerVOMap.get(shortDinerInfo.getId());nearMeDinerVO.setNickname(shortDinerInfo.getNickname());nearMeDinerVO.setAvatarUrl(shortDinerInfo.getAvatarUrl());});return Lists.newArrayList(nearMeDinerVOMap.values());}
}

4. 项目测试

在这里插入图片描述

{"code": 1,"message": "Successful.","path": "/nearme","data": [{"id": 26,"nickname": "test","avatarUrl": null,"distance": "0.0m"},{"id": 27,"nickname": "test","avatarUrl": null,"distance": "0.0m"},{"id": 28,"nickname": "test","avatarUrl": null,"distance": "0.0m"},{"id": 29,"nickname": "test","avatarUrl": null,"distance": "0.0m"},{"id": 30,"nickname": "test","avatarUrl": null,"distance": "0.0m"},{"id": 5,"nickname": "test","avatarUrl": null,"distance": "0.0m"},{"id": 9,"nickname": "test","avatarUrl": null,"distance": "0.0m"},{"id": 6,"nickname": "test","avatarUrl": null,"distance": "82.4m"},{"id": 7,"nickname": "test","avatarUrl": null,"distance": "302.6m"},{"id": 21,"nickname": "test","avatarUrl": null,"distance": "504.7m"},{"id": 22,"nickname": "test","avatarUrl": null,"distance": "504.7m"},{"id": 23,"nickname": "test","avatarUrl": null,"distance": "504.7m"},{"id": 24,"nickname": "test","avatarUrl": null,"distance": "504.7m"},{"id": 25,"nickname": "test","avatarUrl": null,"distance": "504.7m"},{"id": 10,"nickname": "test","avatarUrl": null,"distance": "1413.0m"},{"id": 11,"nickname": "test","avatarUrl": null,"distance": "1413.0m"},{"id": 12,"nickname": "test","avatarUrl": null,"distance": "1413.0m"},{"id": 13,"nickname": "test","avatarUrl": null,"distance": "1413.0m"},{"id": 14,"nickname": "test","avatarUrl": null,"distance": "1413.0m"},{"id": 15,"nickname": "test","avatarUrl": null,"distance": "1413.0m"}]
}

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...