附近的人mysql实现_附近的人功能实现及原理-程序员宅基地

技术标签: 附近的人mysql实现  

如何查找当前点(118.818747°E,32.074497°N)附近500米的人?

这一类功能很常见(如微信附近的人、共享单车附近的车辆、美团附近的商家),那在java中是如何实现的呢?

1 实现方式

目前普遍的实现方式有三种,下面将依次展开讨论:

Mysql+外接正方形

Mysql+geohash

Redis+geohash

2 Mysql+外接正方形

2.1 实现思路

查找附近500米的人,就是以当前坐标点为圆心,以500米为半径画圆,找出圆内的人。

理论上可以直接计算数据库所有点与圆心的距离,与500米比较。但计算地球上两点距离公式复杂,一旦数据库数据过多,计算起来就更麻烦了。

我们可以通过外接正方形的方式来解决这个问题。这样一来,计算量骤减。[注:设定下图圆心在北半球,东半球]

ec6a3cd8817f

外接正方形

于是:实现附近的人功能实现分为:

① 计算外切正方形最大最小经纬度

② 查询在正方形范围内的数据

③ 过滤得到圆周内的点,即用正方形范围内的点-黄色区域的点(距离超过给定范围500米)

2.2 数据库准备

ec6a3cd8817f

数据库表结构

2.3 代码实现

获取外切正方形最大最小经纬度有两种方法,可以自己实现,也可用开源库实现。

①自己实现getGpsRange方法

/**

* 获取附近x米的人

*

* @param distance 距离范围 单位km

* @param userLng 当前经度

* @param userLat 当前纬度

* @return

*/

public List nearBySearch1(double distance, double userLng, double userLat) {

//1 获取外切正方形最大最小经纬度

double[] point = getGpsRange(userLng, userLat, distance);

//2 获取位置在正方形内的所有用户

// 查询数据库操作,这里用mybatis plus实现

List users = list(Wrappers.lambdaQuery().ge(User::getUserLongitude, point[0]).lt(User::getUserLongitude, point[1]).ge(User::getUserLatitude, point[2]).lt(User::getUserLatitude, point[3]));

//3 过滤掉超过指定距离的用户

users = users.stream().filter(a -> getDistance(a.getUserLongitude(), a.getUserLatitude(), userLng, userLat) <= distance)

.collect(Collectors.toList());

return users;

}

/**

* 查询出某个范围内的最大经纬度和最小经纬度

* 自己计算

*

* @param longitude 当前位置经度

* @param latitude 当前位置纬度

* @param rangeDis 距离范围,单位km

* @return

*/

public static double[] getGpsRange(double longitude, double latitude, double rangeDis) {

//半矢量公式,与圆心在同纬度上,且在圆周上的点到圆点的经度差

double dlng = 2 * Math.asin(Math.sin(rangeDis / (2 * EARTH_RADIUS)) / Math.cos(latitude * Math.PI / 180));

//弧度转为角度

dlng = dlng * 180 / Math.PI;

//半矢量公式,与圆心在同经度上,且在圆周上的点到圆点的纬度差

//弧度转为角度

double dlat = rangeDis / EARTH_RADIUS;

dlat = dlat * 180 / Math.PI;

double minlng = longitude - dlng;

double maxlng = longitude + dlng;

double minlat = latitude - dlat;

double maxlat = latitude + dlat;

return new double[]{minlng, maxlng, minlat, maxlat};

}

/**

* 根据地球上任意两点的经纬度计算两点间的距离(半矢量公式),返回距离单位:km

*

* @param longitude1 坐标1 经度

* @param latitude1 坐标1 纬度

* @param longitude2 坐标2 经度

* @param latitude2 坐标2 纬度

* @return 返回km

*/

public static double getDistance(double longitude1, double latitude1, double longitude2, double latitude2) {

double radLat1 = Math.toDegrees(latitude1);

double radLat2 = Math.toDegrees(latitude2);

double a = radLat1 - radLat2;

double b = Math.toDegrees(longitude1) - Math.toDegrees(longitude2);

double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +

Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));

distance = distance * EARTH_RADIUS;

distance = Math.round(distance * 10000) / 10000.0;

return distance;

}

②也可用开运库计算外接正方形坐标范围

com.spatial4j

spatial4j

0.5

private SpatialContext spatialContext = SpatialContext.GEO;

/**

* 获取附近x米的人

*

* @param distance 距离范围 单位km

* @param userLng 当前经度

* @param userLat 当前纬度

* @return

*/

public List nearBySearch(double distance, double userLng, double userLat) {

//1 获取外切正方形最大最小经纬度

Rectangle rectangle = getRectangle(distance, userLng, userLat);

//2.获取位置在正方形内的所有用户

// 查询数据库操作,这里用mybatis plus实现

List users = list(Wrappers.lambdaQuery().ge(User::getUserLongitude, rectangle.getMinX()).lt(User::getUserLongitude, rectangle.getMaxX()).ge(User::getUserLatitude, rectangle.getMinY()).lt(User::getUserLatitude, rectangle.getMaxY()));

//3.剔除半径超过指定距离的多余用户

users = users.stream().filter(a -> getDistance(a.getUserLongitude(), a.getUserLatitude(), userLng, userLat) <= distance)

.collect(Collectors.toList());

return users;

}

/**

* 利用开源库计算外接正方形坐标

*

* @param distance

* @param userLng 当前经度

* @param userLat 当前纬度

* @return

*/

private Rectangle getRectangle(double distance, double userLng, double userLat) {

return spatialContext.getDistCalc()

.calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat),

distance * DistanceUtils.KM_TO_DEG, spatialContext, null);

}

/***

* 球面中,两点间的距离(第三方库方法)

*

* @param longitude 经度1

* @param latitude 纬度1

* @param userLng 经度2

* @param userLat 纬度2

* @return 返回距离,单位km

*/

public double getDistance(Double longitude, Double latitude, double userLng, double userLat) {

return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),

spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;

}

3 Mysql+geohash

第二种实现方式引入了geohash。

GeoHash是一种地址编码方法。他能够把二维的空间经纬度数据编码成一个字符串。

3.1 geohash算法

3.1.1 geohash算法思想

ec6a3cd8817f

将地球球面沿着180°经线分开,平铺到平面上。

0°经线和0°纬线将此平面划分为四部分。设定西经为负,南纬为负,地球上的经度范围就是[-180°,180°],纬度范围就是[-90°,90°]。

如果纬度范围[-90°, 0°)用二进制0代表,(0°, 90°]用二进制1代表,经度范围[-180°, 0°)用二进制0代表,(0°, 180°]用二进制1代表,那么划分出的四部分用二进制表示为:

ec6a3cd8817f

如果再对此递归对半划分呢?

ec6a3cd8817f

geohash算法就是基于这种思想,划分的次数越多,区域越多,区域面积越小。

3.1.2 geohash算法编码经纬度

geohash算法将经纬度编码分为三步:

①将经纬度变成二进制

以点(118.818747,32.074497)为例。

纬度的范围是(-90,90),以其中间值0将此范围划分为两个区间(-90,0)和(0,90),若给定的纬度在左区间(-90,0),则为0;若给定的纬度在右区间(0,90),则为1;纬度32.074497在右区间,因此为1。

再将(0,90)这个区间以中间值划分为左右区间,按照以上方法判定为1还是0。

依此方法,可得到纬度的二进制表示,如下表:

纬度范围

划分的左区间

划分的右区间

纬度32.074497的二进制表示

(-90,90)

(-90,0)

(0,90)

1

(0,90)

(0,45)

(45,90)

0

(0,45)

(0,22.5)

(22.5,45)

1

(22.5,45)

(22.5,33.75)

(33.75,45)

0

(22.5,33.75)

(22.5,28.125)

(28.125,33.75)

1

……

……

……

……

划分10次后,得到的纬度二进制表示为10101 10110

同样的方法,可得到划分9次后经度二进制表示为110101

②将经纬度合并

合并方法: 经度占偶数位,纬度占奇数位

经纬度合并结果为 11100 11001 11000 10110

③按照Base32进行编码

将②的结果用Base32编码得到字符串wtsq。也就是说点(118.818747,32.074497)可用wtsq表示。

GeoHash字符串越长,表示的位置越精确,字符串长度越长代表在距离上的误差越小。具体的不同精度的距离误差可参考下表:

ec6a3cd8817f

不同精度的距离误差

GeoHash值表示的并不是一个点,而是一个矩形区域。

Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己所在区域,又不至于暴露自己的精确坐标,有助于隐私保护。

距离越近的坐标,转换后的geohash字符串越相似,例如:

ec6a3cd8817f

3.2 实现思路

以上详细介绍了geohash算法,那么如何利用Mysql+geohash实现附近的人功能呢?

添加新用户时计算该用户的geohash字符串,并存储到用户表中。

当要查询某个点附近指定距离的用户信息时,通过比对geohash误差表确定需要的geohash字符串精度。

计算获得当前坐标的geohash字符串,并查询与当前字符串前缀相同的数据。

如果geohash字符串的精度远大于给定的距离范围时,查询出的结果集中必然存在在范围之外的数据。

计算两点之间距离,对于超出距离的数据进行剔除。

3.3 数据库准备

ec6a3cd8817f

数据库表结构

3.4 代码实现

com.spatial4j

spatial4j

0.5

private SpatialContext spatialContext = SpatialContext.GEO;

/**

* 获取附近指定范围的人

*

* @param distance 距离范围 单位km

* @param len geoHash的精度

* @param userLng 当前用户的经度

* @param userLat 当前用户的纬度

* @return

*/

public List nearBySearch2(double distance, int len, double userLng, double userLat) {

//1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码

String geoHashCode = GeohashUtils.encodeLatLon(userLat, userLng, len);

//2.匹配指定精度的geoHash码

//查询数据库操作 mybatis plus实现

List users = list(Wrappers.lambdaQuery().likeRight(User::getGeohash, geoHashCode));

//3.过滤超出距离的

users = users.stream()

.filter(a -> getDistance1(a.getUserLongitude(), a.getUserLatitude(), userLng, userLat) <= distance)

.collect(Collectors.toList());

return users;

}

/***

* 球面中,两点间的距离(第三方库方法)

*

* @param longitude 经度1

* @param latitude 纬度1

* @param userLng 经度2

* @param userLat 纬度2

* @return 返回距离,单位km

*/

public double getDistance(Double longitude, Double latitude, double userLng, double userLat) {

return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),

spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;

}

/**

* 向数据库添加数据

*

* @param user 用户对象

* @return

*/

public boolean save(User user) {

//默认精度12位

String geoHashCode = GeohashUtils.encodeLatLon(user.getUserLatitude(), user.getUserLongitude());

//插入数据库操作 mybatis plus实现

super.save(user.setGeohash(geoHashCode));

}

3.5 边界问题优化

geohash算法提高了效率,但在实际应用场景中存在一些问题。首先就是边界问题。

ec6a3cd8817f

如图,如果当前在红点位置,区域内还有一个黄点。相邻区域内的绿点明显离红点更近。但因为黄点的编码和红点一样,最终找到的将是黄点。这就有问题了。

要解决这个问题,除了要找到当前区域内的点,还要要再查找周边8个区域内的点,看哪个离自己更近。

由此优化代码为:

com.spatial4j

spatial4j

0.5

ch.hsr

geohash

1.0.10

private SpatialContext spatialContext = SpatialContext.GEO;

/**

* 获取附近x米的人,geohash区域+8个周围区域

*

* @param distance 距离范围 单位km

* @param len geoHash的精度

* @param userLng 当前经度

* @param userLat 当前纬度

* @return json

*/

public List nearBySearch4(double distance, int len, double userLng, double userLat) {

//1 根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码

GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len);

//2 获取到用户周边8个方位的geoHash码

GeoHash[] adjacent = geoHash.getAdjacent();

//查询数据库操作 mybatis plus实现

QueryWrapper queryWrapper = new QueryWrapper().likeRight("user_geohash", geoHash.toBase32());

Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("user_geohash", a.toBase32()));

//匹配指定精度的geoHash码

List users = list(queryWrapper);

//3 过滤超出距离的

users = users.stream()

.filter(a -> getDistance(a.getUserLongitude(), a.getUserLatitude(), userLng, userLat) <= distance)

.collect(Collectors.toList());

return users;

}

/***

* 球面中,两点间的距离(第三方库方法)

*

* @param longitude 经度1

* @param latitude 纬度1

* @param userLng 经度2

* @param userLat 纬度2

* @return 返回距离,单位km

*/

public double getDistance(Double longitude, Double latitude, double userLng, double userLat) {

return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),

spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;

}

4 Redis+geohash

基于前两种方案,我们可以发现此功能属于读多写少的情况,如果使用redis来实现附近的人,想必效率会大大提高。

自Redis 3.2开始,Redis基于geohash和有序集合Zset提供了地理位置相关功能。

关于Redis提供的geohash操作命令介绍可移步:Redis 到底是怎么实现“附近的人”这个功能的呢?

4.1 实现思路

用GEOADD方法添加用户坐标信息到redis,redis会将经纬度参数值转换为52位的geohash码,

Redis以geohash码为score,将其他信息以Zset有序集合存入key中

通过调用GEORADIUS命令,获取指定坐标点某一范围内的数据

因geohash存在精度误差,剔除超过指定距离的数据

4.2 代码实现

@Autowired

private RedisTemplate redisTemplate;

//GEO相关命令用到的KEY

private final static String KEY = "user_info";

/**

* 根据当前位置获取附近指定范围内的用户

*

* @param distance 指定范围 单位km ,可根据{@link Metrics} 进行设置

* @param userLng 用户经度

* @param userLat 用户纬度

* @return

*/

public List nearBySearch3(double distance, double userLng, double userLat) {

List users = new ArrayList<>();

//GEORADIUS获取附近范围内的信息

GeoResults> reslut =

redisTemplate.opsForGeo().radius(KEY,

new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)),

RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()

.includeDistance()

.includeCoordinates().sortAscending());

//存入list

List>> content = reslut.getContent();

//过滤掉超过距离的数据

content.forEach(a -> users.add(

new User().setDistance(a.getDistance().getValue())

.setUserLatitude(a.getContent().getPoint().getX())

.setUserLongitude(a.getContent().getPoint().getY())));

return users;

}

/**

* 用户信息存入Redis

*

* @param user 用户对象

* @return

*/

public boolean save(User user) {

Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(

user.getUserAccount(),

new Point(user.getUserLatitude(), user.getUserLatitude()))

);

return flag != null && flag > 0;

}

5 总结

ec6a3cd8817f

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39772352/article/details/113415435

智能推荐

解决vscode顶部窗口没有显示原因_volar编辑器分割功能没看到-程序员宅基地

文章浏览阅读6k次。解决vscode顶部窗口没有显示原因的问题(gitlab)产生的原因解决方法欢迎订阅前端进阶订阅号产生的原因vscode 查看→外观 没有选择全屏模式解决方法按F11按键欢迎订阅前端进阶订阅号如果感觉对您的学习工作有帮助,请将它分享给需要的人,感谢支持..._volar编辑器分割功能没看到

TCP/IP基础知识——TCP/IP分层模型-程序员宅基地

文章浏览阅读4.5k次。1、TCP/IP与OSI参考模型:_ip分层

大专生面试阿里P7居然过了,Java高级程序员面试集合-程序员宅基地

文章浏览阅读853次,点赞13次,收藏20次。从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!ring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。程序员A:B,你这段代码使用的是XXX设计模式对吗?

南加大的计算机科学与工程,2019上海软科世界一流学科排名计算机科学与工程专业排名南加州大学排名第15...-程序员宅基地

文章浏览阅读459次。2019上海软科世界一流学科排名计算机科学与工程专业排名指标首先计算大学在每项指标上的得分,具体为大学在一项指标上的数值除以该项指标的最大值(开根号)再乘以100。然后各指标得分除以100再乘以相应权重进行累加得到该校总分。CNCI为相对指标,论文数量较少时CNCI不够稳定。因此在计算该指标的得分时,一个学科的CNCI最大值设置为该学科所有大学的CNCI平均值的2倍或者该学科所有大学中CNCI的实..._南加大软科排名

css3如何实现字体放大缩小动画_css字体放大缩小动画-程序员宅基地

文章浏览阅读3.2k次。css3实现字体放大缩小动画效果,按钮看起来是动的,以吸引用户_css字体放大缩小动画

RandomAccessFile 读取和存储文件_randomaccessfile读取文件-程序员宅基地

文章浏览阅读375次。RandomAccessFile 读取和存储文件;java读取大文件_randomaccessfile读取文件

随便推点

MATLAB源码-第55期】matlab代码基于m序列的多用户跳频通信系统仿真,输出各节点波形图。_m序列波型-程序员宅基地

文章浏览阅读428次。通常,跳频系统的频率合成器输出什么频率的载波信号是受跳频指令控制的,跳频器是由频率合成器和跳频指令发生器构成的。在时钟的作用下,频率合成器不断地改变其输出载波的频率,跳频指令发生器不断地发出控制指令。首先,为了完成解跳功能,用同相干解调类似的方法将发送信号已知的伪随机的载波与接收信号进行混频,再经过低通滤波器进行滤波,即可得到到解跳后的信号,以便以后基带调制的进行。跳频扩频调制通过伪随机地改变发送载波频率,用跳变的频率来调制基带信号,得到载波频率不断变化的射频信号。3.加性高斯白噪声信道。_m序列波型

Vue工具库VueUse的具体用法_usevue-程序员宅基地

文章浏览阅读2.9w次,点赞11次,收藏71次。前言上次在看前端早早聊大会中, 尤大大再一次提到了 VueUse 的一个库。 好奇了一下,点看看了看。好家伙啊, 我直接好家伙。这不就是曾经我也想自己写一个 vue 版的 hooks 库吗?(因为我觉得 vue3 和 hooks 太像了) 可是我还不太会, 你现在直接把我的梦想给破灭了,下面我们一起来看看吧!VueUse 作者 Anthony Fu 分享可组合的 Vue_哔哩哔哩_bilibili什么是 VueUseVueUse不是Vue.use,它是为Vue 2和3服务的一套Vue Composi_usevue

嵌入式学习之QT学习 ----1 QT环境搭建(Windows)_安装qt程序的时候勾选哪些和嵌入式有关的-程序员宅基地

文章浏览阅读1.7k次。想了又想,之后的设计基于QT系统大概会比较容易一些,就学习一下基础知识吧。1、什么是QT?QT是一个跨平台的、C++、图形用户界面,应用程序开发框架。2、通过学习,将会获得哪些知识?掌握QT环境搭建C++入门掌握使用QT开发windows平台上位机,如:串口调试助手、网络调试助手等掌握发布自己开发的windows上位机掌握在ARM板上移植QT操作系统掌握把QT程序移植到ARM开发板上运行掌握使用QT驱动程序,如:点亮一个LED掌握QT开发手机APP学习远程调试ARM板子上的QT程序_安装qt程序的时候勾选哪些和嵌入式有关的

【算法】三、回溯法_算法策略 回溯法-程序员宅基地

文章浏览阅读978次,点赞2次,收藏10次。回溯法其实就是不断地试探,看前方的路是否可以走,如果不行就退回一步,再换一个办法。_算法策略 回溯法

忘记本地mysql密码修改方法(win10,mysql8.0)_window10清除本地mysql密码-程序员宅基地

文章浏览阅读309次。1.关闭mysql服务2.管理员打开命令提示符,输入mysqld --console --skip-grant-tables --shared-memory输入后回车,放在一旁。3.再新建一个cmd窗口(用管理员身份打开)—win键+R,输入mysql -uroot -p然后回车键(Enter),弹出输入密码,不理,再次回车键(Enter)4.依次输入以下操作先刷新一下权限表:flush privileges;然后我们改密码:输入ALTER USER 'root'@'localh_window10清除本地mysql密码

ZXCLOUD R5300 G3的服务器安装ESXI6.7镜像前篇-制作ESXI6.7定制版-程序员宅基地

文章浏览阅读684次。VMware vSphere Hypervisor (ESXi) Offline Bundle(脱机捆绑包)镜像。服务器安装版本与平时安装版本有所不同,由于服务器缺少阵列卡驱动,需将阵列卡驱动注入。注意:将镜像刻录到U盘时写入方式为USB-HDD+ V2或USB-ZIP+ V2。文件放入文件夹中,在文件夹中再创建一个文件夹,将。定制版镜像,阵列卡驱动完成注入!最新版(自己下载,以上所需文件。在“文件”选项中打开刚才生成的。下载自己去淘宝买一次性下载)文件,选中解除锁定,并应用。注意:下载镜像时需下载。_esxi6.7镜像

推荐文章

热门文章

相关标签