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 的 ...
随机推荐
- 特效 css3 持续动作的渐变背景
html,body{ margin:; padding:; height: 100%; width: 100%; background: linear-gradient(125deg,#2c3e50, ...
- MySQL知识-redis实例
规划.搭建过程:6个redis实例,一般会放到3台硬件服务器注:在企业规划中,一个分片的两个分到不同的物理机,防止硬件主机宕机造成的整个分片数据丢失.端口号:7000-7005 # 1. 安装集群插件 ...
- SpringBoot整合SpringSecurity实现JWT认证
目录 前言 目录 1.创建SpringBoot工程 2.导入SpringSecurity与JWT的相关依赖 3.定义SpringSecurity需要的基础处理类 4. 构建JWT token工具类 5 ...
- Qt版本中国象棋开发(一)
开发目的:实现象棋人机对战简单AI,网络对战,移植到android中. 开发平台:windows10 + Qt5.4 for android 开发语言:C++ 开发过程:1.棋盘绘制: 方法一:重写 ...
- 国家集训队 部落战争 网络流最小路径覆盖 洛谷P2172
洛谷AC传送门! step1: 题目大意 有一张M x N的网格图,有一些点为“ * ”可以走,有一些点为“ x ”不能走,每走一步你都可以移动R * C 个格子(参考象棋中马的走法),且不能回头,已 ...
- [PHP学习教程 - 日期/时间]001.月份第一天&最后一天(Month First Day & Last Day)
引言:在时间处理上,对于前/后台人性化的展示当前月份最大天数,这个是网站必须要处理的一个方面.但通常这一块会由第三方类库直接包装,这里我们做一个简单的Mark. 今天,我们就为大家提供一个函数,获得指 ...
- 搭建SpringCloud微服务框架:一、结构和各个组件
搭建微服务框架(结构和各个组件) 简介 SQuid是基于Spring,SpringBoot,使用了SpringCloud下的组件进行构建,目的是想搭建一套可以快速开发部署,并且很好上手的一套微服务框架 ...
- 04 . Nginx的Rewrite重写
Rewrite简介 # Rewrite对应URL Rewrite,即URL重写,就是把传入web的请求重定向到其他URL的过程. # 当运维遇到要重写情况时,往往是要程序员把重写规则写好后,发给你,你 ...
- 小谢第7问:js前端如何实现大文件分片上传、上传进度、终止上传以及删除服务器文件?
文件上传一般有两种方式:文件流上传和base64方式上传,毫无疑问,当进行大文件上传时候,转为base64是不现实的,因此用formData方式结合文件流,直接上传到服务器 本文主要结合vue的来讲解 ...
- Java Word中的文本、图片替换功能
Word中的替换功能以查找指定文本然后替换为新的文本,可单个替换或全部替换.以下将要介绍的内容,除常见的以文本替换文本外,还将介绍使用不同对象进行替换的方法,具体可包括: 1. 指定字符串内容替换文本 ...