Spring Boot 2 实战:利用Redis的Geo功能实现查找附近的位置
1. 前言
老板突然要上线一个需求,获取当前位置方圆一公里的业务代理点。明天上线!当接到这个需求的时候我差点吐血,这时间也太紧张了。赶紧去查相关的技术选型。经过一番折腾,终于在晚上十点完成了这个需求。现在把大致实现的思路总结一下。
2. MySQL 不合适
遇到需求,首先要想到现有的东西能不能满足,成本如何。
MySQL是我首先能够想到的,毕竟大部分数据要持久化到MySQL。但是使用MySQL需要自行计算Geohash。需要使用大量数学几何计算,并且需要学习地理相关知识,门槛较高,短时间内不可能完成需求,而且长期来看这也不是MySQL擅长的领域,所以没有考虑它。
Geohash 参考 https://www.cnblogs.com/LBSer/p/3310455.html
2. Redis 中的GEO
Redis是我们最为熟悉的K-V数据库,它常被拿来作为高性能的缓存数据库来使用,大部分项目都会用到它。从3.2版本开始它开始提供了GEO能力,用来实现诸如附近位置、计算距离等这类依赖于地理位置信息的功能。GEO相关的命令如下:
Redis命令 | 描述 |
---|---|
GEOHASH | 返回一个或多个位置元素的 Geohash 表示 |
GEOPOS | 从key里返回所有给定位置元素的位置(经度和纬度) |
GEODIST | 返回两个给定位置之间的距离 |
GEORADIUS | 以给定的经纬度为中心, 找出某一半径内的元素 |
GEOADD | 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中 |
GEORADIUSBYMEMBER | 找出位于指定范围内的元素,中心点是由给定的位置元素决定 |
Redis会假设地球为完美的球形, 所以可能有一些位置计算偏差,据说<=0.5%,对于有严格地理位置要求的需求来说要经过一些场景测试来检验是否能够满足需求。
2.1 写入地理信息
那么如何实现目标单位半径内的所有元素呢?我们可以将所有的位置的经纬度通过上表中的GEOADD
将这些地理信息转换为52位的Geohash写入Redis。
该命令格式:
geoadd key longitude latitude member [longitude latitude member ...]
对应例子:
redis> geoadd cities:locs 117.12 39.08 tianjin 114.29 38.02 shijiazhuang
(integer) 2
意思是将经度为117.12
纬度为39.08
的地点tianjin
和经度为114.29
纬度为38.02
的地点shijiazhuang
加入key为cities:locs
的 sorted set集合中。可以添加一到多个位置。然后我们就可以借助于其他命令来进行地理位置的计算了。
有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。当坐标位置超出上述指定范围时,该命令将会返回一个错误。
2.2 统计单位半径内的地区
我们可以借助于GEORADIUS
来找出以给定经纬度,某一半径内的所有元素。
该命令格式:
georadius key longtitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]
这个命令比GEOADD
要复杂一些:
- radius 半径长度,必选项。后面的
m
、km
、ft
、mi
、是长度单位选项,四选一。 - WITHCOORD 将位置元素的经度和维度也一并返回,非必选。
- WITHDIST 在返回位置元素的同时, 将位置元素与中心点的距离也一并返回。 距离的单位和查询单位一致,非必选。
- WITHHASH 返回位置的52位精度的Geohash值,非必选。这个我反正很少用,可能其它一些偏向底层的LBS应用服务需要这个。
- COUNT 返回符合条件的位置元素的数量,非必选。比如返回前10个,以避免出现符合的结果太多而出现性能问题。
- ASC|DESC 排序方式,非必选。默认情况下返回未排序,但是大多数我们需要进行排序。参照中心位置,从近到远使用ASC ,从远到近使用DESC。
例如,我们在 cities:locs
中查找以(115.03,38.44)为中心,方圆200km
的城市,结果包含城市名称、对应的坐标和距离中心点的距离(km),并按照从近到远排列。命令如下:
redis> georadius cities:locs 115.03 38.44 200 km WITHCOORD WITHDIST ASC
1) 1) "shijiazhuang"
2) "79.7653"
3) 1) "114.29000169038772583"
2) "38.01999994251037407"
2) 1) "tianjin"
2) "186.6937"
3) 1) "117.02000230550765991"
2) "39.0800000535766543"
你可以加上
COUNT 1
来查找最近的一个位置。
3. 基于Redis GEO实战
大致的原理思路说完了,接下来就是实操了。结合Spring Boot应用我们应该如何做?
3.1 开发环境
需要具有GEO特性的Redis版本,这里我使用的是Redis 4 。另外我们客户端使用 spring-boot-starter-data-redis
。这里我们会使用到 RedisTemplate
对象。
3.2 批量添加位置信息
第一步,我们需要将位置数据初始化到Redis中。在Spring Data Redis中一个位置坐标(lng,lat)
可以封装到org.springframework.data.geo.Point
对象中。然后指定一个名称,就组成了一个位置Geo信息。RedisTemplate
提供了批量添加位置信息的方法。我们可以将章节2.1中的添加命令转换为下面的代码:
Map<String, Point> points = new HashMap<>();
points.put("tianjin", new Point(117.12, 39.08));
points.put("shijiazhuang", new Point(114.29, 38.02));
// RedisTemplate 批量添加 Geo
redisTemplate.boundGeoOps("cities:locs").add(points);
可以结合Spring Boot 提供的ApplicationRunner接口来实现初始化。
@Bean
public ApplicationRunner cacheActiveAppRunner(RedisTemplate<String, String> redisTemplate) {
return args -> {
final String GEO_KEY = "cities:locs";
// 清理缓存
redisTemplate.delete(GEO_KEY);
Map<String, Point> points = new HashMap<>();
points.put("tianjin", new Point(117.12, 39.08));
points.put("shijiazhuang", new Point(114.29, 38.02));
// RedisTemplate 批量添加 GeoLocation
BoundGeoOperations<String, String> geoOps = redisTemplate.boundGeoOps(GEO_KEY);
geoOps.add(points);
};
}
地理数据持久化到MySQL,然后同步到Redis中。
3.3 查询附近的特定位置
RedisTemplate
针对GEORADIUS
命令也有封装:
GeoResults<GeoLocation<M>> radius(K key, Circle within, GeoRadiusCommandArgs args)
Circle
对象是封装覆盖的面积(图1),需要的要素为中心点坐标Point
对象、半径(radius)、计量单位(metric), 例如:
Point point = new Point(115.03, 38.44);
Metric metric = RedisGeoCommands.DistanceUnit.KILOMETERS;
Distance distance = new Distance(200, metric);
Circle circle = new Circle(point, distance);
GeoRadiusCommandArgs
用来封装GEORADIUS
的一些可选命令参数,参见章节2.2中的WITHCOORD
、COUNT
、ASC
等,例如我们需要在返回结果中包含坐标、中心距离、由近到远排序的前5条数据:
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands
.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending()
.limit(limit);
然后执行 radius
方法就会拿到GeoResults<RedisGeoCommands.GeoLocation<String>>
封装的结果,我们对这个可迭代对象进行解析就可以拿到我们想要的数据:
GeoResults<RedisGeoCommands.GeoLocation<String>> radius = redisTemplate.opsForGeo()
.radius(GEO_STAGE, circle, args);
if (radius != null) {
List<StageDTO> stageDTOS = new ArrayList<>();
radius.forEach(geoLocationGeoResult -> {
RedisGeoCommands.GeoLocation<String> content = geoLocationGeoResult.getContent();
//member 名称 如 tianjin
String name = content.getName();
// 对应的经纬度坐标
Point pos = content.getPoint();
// 距离中心点的距离
Distance dis = geoLocationGeoResult.getDistance();
});
}
3.4 删除元素
有时候我们可能需要删除某个位置元素,但是Redis的Geo并没有删除成员的命令。不过由于它的底层是zset
,我们可以借助zrem
命令进行删除,对应的Java代码为:
redisTemplate.boundZSetOps(GEO_STAGE).remove("tianjin");
4. 总结
今天我们使用Redis的Geo特性实现了常见的附近的地理信息查询需求,简单易上手。其实使用另一个Nosql数据库MongoDB也可以实现。在数据量比较小的情况下Redis已经能很好的满足需要。如果数据量大可使用MongoDB来实现。 文中涉及的DEMO可通过我个人博客获取。
关注公众号:Felordcn 获取更多资讯
Spring Boot 2 实战:利用Redis的Geo功能实现查找附近的位置的更多相关文章
- Spring Boot 多站点利用 Redis 实现 Session 共享
如何在不同站点(web服务进程)之间共享会话 Session 呢,原理很简单,就是把这个 Session 独立存储在一个地方,所有的站点都从这个地方读取 Session. 通常我们使用 Redis 来 ...
- Spring Boot 项目实战(五)集成 Dubbo
一.前言 上篇介绍了 Redis 的集成过程,可用于解决热点数据访问的性能问题.随着业务复杂度的提高,单体应用越来越庞大,就好比一个类的代码行数越来越多,分而治之,切成多个类应该是更好的解决方法,所以 ...
- Spring Boot 如何快速集成 Redis 哨兵?
上一篇:Spring Boot 如何快速集成 Redis? 前面的分享栈长介绍了如何使用 Spring Boot 快速集成 Redis,上一篇是单机版,也有粉丝留言说有没有 Redis Sentine ...
- Spring Boot 2.x整合Redis
最近在学习Spring Boot 2.x整合Redis,在这里和大家分享一下,希望对大家有帮助. Redis是什么 Redis 是开源免费高性能的key-value数据库.有以下的优势(源于Redis ...
- Spring boot配置多个Redis数据源操作实例
原文:https://www.jianshu.com/p/c79b65b253fa Spring boot配置多个Redis数据源操作实例 在SpringBoot是项目中整合了两个Redis的操作实例 ...
- 7、Spring Boot 2.x 集成 Redis
1.7 Spring Boot 2.x 集成 Redis 简介 继续上篇的MyBatis操作,详细介绍在Spring Boot中使用RedisCacheManager作为缓存管理器,集成业务于一体. ...
- spring boot 是如何利用jackson进行序列化的?
接上一篇:spring boot 是如何利用jackson进行反序列化的? @RestController public class HelloController { @RequestMapping ...
- “Spring Boot+Marklogic实战应用(1)”
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议.本文链接:http://www.blbk.info Spring Boot+Marklogic应用 摘要: 在前一节的介绍,相信 ...
- Spring Boot 2.x 整合 Redis最佳实践
一.前言 在前面的几篇文章中简单的总结了一下Redis相关的知识.本章主要讲解一下 Spring Boot 2.0 整合 Redis.Jedis 和 Lettuce 是 Java 操作 Redis 的 ...
随机推荐
- SpringCloud(二)- Consul介绍、安装、使用
唯能极于情,故能极于剑有问题或错误请及时联系小编或关注小编公众号 “CodeCow”,小编一定及时回复和改正,期待和大家一起学习交流 此文由四部分组成(Consul简介.安装.实操.总结),别着急,慢 ...
- 使用websocket开发智能聊天机器人
前面我们学习了异步web框架(sanic)和http异步调用库httpx,今天我们学习websocket技术. websocket简介 我们知道HTTP协议是:请求->响应,如果没有响应就一直等 ...
- OpenResty应用实践
一. 安装OpenResty 创建OpenResty用户 # useradd -M www -s /usr/sbin/nologin 安装OpenResty # apt-get install lib ...
- Salesforce LWC学习(十七) 前端知识之 onclick & onblur & onmousedown
在Salesforce LWC学习(八) Look Up组件实现篇中,我们实现了公用的lookup组件,使用的过程中,会发现当我们输入内容以后,搜索出来的列表便无法被清空. 针对此种情况我们打算优化一 ...
- js在运算时的类型转换
日期类型与数字类型的运算 在加法时将日期对象与数字都当作字符串进行连接 字符串的运算方式中没有减法.乘法.除法.转化成数字类型进行运算 字符串类型字符与数字类型的运算 在加法时将二者都转换成字符串进行 ...
- 【大厂面试02期】Redis过期key是怎么样清理的?
PS:本文已收录到1.1K Star数开源学习指南--<大厂面试指北>,如果想要了解更多大厂面试相关的内容,了解更多可以看 http://notfound9.github.io/inter ...
- java方法句柄-----2.方法句柄的获取、变换、特殊方法句柄
目录 1.获取方法句柄 1.1查找构造方法.一般方法和静态方法的方法句柄 1.2 查找类中的特殊方法(类中的私有方法) 1.3 查找类中静态域和一般域 1.4 通过反射API得到的Constructo ...
- Java实现 LeetCode 478 在圆内随机生成点
478. 在圆内随机生成点 给定圆的半径和圆心的 x.y 坐标,写一个在圆中产生均匀随机点的函数 randPoint . 说明: 输入值和输出值都将是浮点数. 圆的半径和圆心的 x.y 坐标将作为参数 ...
- Java实现 POJ 2749 分解因数(计蒜客)
POJ 2749 分解因数(计蒜客) Description 给出一个正整数a,要求分解成若干个正整数的乘积,即a = a1 * a2 * a3 * - * an,并且1 < a1 <= ...
- Java实现 LeetCode 38 外观数列
38. 外观数列 「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述.前五项如下: 1 11 21 1211 111221 1 被读作 "one 1" ...