Redis实战 - 11 Redis GEO 实现附近的人功能
各种社交软件里面都有附件的人的需求,在该应用中,我们查询附近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的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。字符串越长,表示的范围越精确。
# 计算某个位置的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<NearMeDinerVO> 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<NearMeDinerVO> 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<Point> 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<RedisGeoCommands.GeoLocation> geoResult = redisTemplate.opsForGeo().radius(key, circle, args);// 构建有序 MapMap<Integer, NearMeDinerVO> nearMeDinerVOMap = Maps.newLinkedHashMap();// 完善用户头像昵称信息geoResult.forEach(result -> {RedisGeoCommands.GeoLocation<Integer> 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<ShortDinerInfo> 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"}]
}
Redis实战 - 11 Redis GEO 实现附近的人功能相关推荐
- Redis实战之Redis + Jedis
用Memcached,对于缓存对象大小有要求,单个对象不得大于1MB,且不支持复杂的数据类型,譬如SET 等.基于这些限制,有必要考虑Redis! 相关链接: Redis实战 Redis实战之Redi ...
- redis(二)redis实战 使用redis进行文章的排序
2019独角兽企业重金招聘Python工程师标准>>> http://www.beckbi.cn/?p=172 redis实战使用redis进行文章的排序 转载于:https://m ...
- Redis实战 - 09 Redis BitMaps 实现用户签到,统计签到次数,统计签到情况等功能
文章目录 1. 需求分析 2. 设计思路 3. 用户签到和统计连续签到的次数 1. 签到控制层 SignController 2. 签到业务逻辑层 SignService 3. 测试 4. 按月统计用 ...
- Redis 实战篇:GEO 助我邂逅附近女神
码老湿,阅读了你的巧用数据类型实现亿级数据统计之后,我学会了如何游刃有余的使用不同的数据类型(String.Hash.List.Set.Sorted Set.HyperLogLog.Bitmap)去解 ...
- Redis实战案例及问题分析之好友关注功能(关注、共同好友、消息推送)
关注和取关 需求:基于该表数据结构,实现两个接口 关注和取关接口 @Overridepublic Result follow(Long followUserId, Boolean isFollow) ...
- redis之二十八 -- Redis实战:Redis 主从同步
主从同步(主从复制)是 Redis 高可用服务的基石,也是多机运行中最基础的一个.我们把主要存储数据的节点叫做主节点 (master),把其他通过复制主节点数据的副本节点叫做从节点 (slave),如 ...
- 基于redis(v3.2+)实现“附近的人”功能
背景介绍:目前随着电商.社交.游戏和代购等的流行,"附近的人"这一功能提供了一种便捷的方式允许同一地区或者一定距离范围内的用户进行相互交流的途径,一般都是在用户点击某个菜单或按钮时 ...
- Redis 实战篇:Geo 算法查找附近的人
一.什么是面向 LBS 应用 经纬度是经度与纬度的合称组成一个坐标系统.又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置(小数点后7位,精度 ...
- Redis 实战篇:Geo 算法教你找出附近 “女朋友”!
产品经理说他有一个 idea,为广大少男少女提供一个连接彼此的机会. 让处于这最美的年龄的少男少女能在每一个十二时辰里能邂逅到那个 Ta. 所以就想开发一款 App,用户登陆后能发现附近的那个 Ta, ...
最新文章
- 网站优化时需注意哪些事项可有效防止排名下降?
- 初学__Python——Python中文支持、Python计算器
- I see IC的破冰之旅
- clob类型用java怎么存,Java 储存和读取 oracle CLOB 类型字段的实用方法
- vue 监听map数组变化_vuex state中的数组变化监听实例
- 实现call和apply
- 计算机科学与技术 军校,清华大学计算机科学与技术系
- Magento:Paypal付款不成功返回后不要清空购物车产品的解决方案
- android 6.0 蓝牙进程,Android6.0-蓝牙权限问题
- 用脚本管理服务器日志
- Halcon 例程学习之频域自相关变换( correlation_fft)
- 控制鼠标滚动,滚动指定的距离
- ListView嵌套ScrollView后,设置dividerHeight属性后高度计算
- 鸿蒙系统简介ppt,鸿蒙来了!华为到底采用的是什么逆天的研发体系?500页PPT详解...
- 使用iconfont阿里多色矢量图标
- Linux 终端快捷键
- Lemmatization VS Stemming
- Java并发编程面试题合集
- 移动 PC 的数据加密工具包 - 安全分析
- java实现上传文件夹