前言
交友软件中附近的小姐姐、外卖软件中附近的美食店铺、地图附近的地铁等等,那附近各种形形色色的XXX地址位置选择是如何实现的?
本文讲解其中相关实现原理,这里会涉及到Redis的高级语法结构GEO,Redis的GEO数据结构是一种用于存储地理位置信息的数据类型。它支持对地理位置进行半径搜索、矩形搜索和附近点搜索等多种操作,可以用于实现诸如查找最近地铁口等功能。本文将介绍如何使用Redis的GEO数据结构来实现最近地铁口的搜索。
一、为什么要用GEO
先使用MySQL存储各个地铁的经纬度的方案,来实现寻找最近地铁口的需求。
都知道地球上的地理位置是使用二维的经纬度表示,经度范围(-180,180],纬度范围(-90,90],只要我们确定一个点的经纬度就可以得他在地球的位置。
例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,然后当我们要找车时,在数据库中查找距离我们(坐标x0,y0)附近r公里范围内部的车辆
使用如下SQL即可:
代码语言:sql复制select taxi from position where x0-r < x < x0 r and y0-r < y < y0 r
但是这样会有什么问题呢?
- 查询性能问题,如果并发高,数据量大这种查询是要搞垮数据库的
- 这个查询的是一个矩形访问,而不是以我为中心r公里为半径的圆形访问。
- 精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差
所以使用MySQL的方案处理地理位置相关问题是有问题,所以引入Redis的GEO。
Redis的GEO可以解决上述数据库出现的问题,得益于GEO原理:
- 数据结构
GEO数据结构使用了Redis的内置数据结构,包括哈希表和有序集合。哈希表用于存储地理位置的元数据,例如地点名称、地址等;有序集合用于存储地理位置的坐标信息,例如经度和纬度。
- 坐标编码
GEO数据结构使用经纬度表示地理位置的坐标信息。经纬度是一种常用的地理坐标系统,它使用经度和纬度来表示地球上的位置。在GEO数据结构中,经度和纬度被编码为一个64位的整数,以便进行高效的计算和比较。
- 距离计算
GEO数据结构使用Haversine公式来计算两个地理位置之间的距离。Haversine公式是一种常用的距离计算方法,它可以计算地球上两点之间的距离,考虑到地球的曲率。在GEO数据结构中,Haversine公式被用于计算两个地理位置之间的距离,以便进行搜索和排序。
- 搜索算法
GEO数据结构使用了一种基于跳表的搜索算法来实现高效的地理位置搜索。跳表是一种基于链表的数据结构,它可以实现快速的查找、插入和删除操作。在GEO数据结构中,跳表被用于存储地理位置的坐标信息,以便进行高效的搜索和排序。
二、GEO实现最近地铁口查询
1.数据准备
在使用Redis的GEO数据结构之前,我们需要准备一些地铁口的数据。这些数据可以包括地铁口的ID、名称、坐标等信息。我们可以将这些数据存储在一个哈希表中,例如:
代码语言:lua复制GEOADD city 113.42231 , 23.11034 "三溪" 113.40815 , 23.11577 "东圃" 113.43893 , 23.10612 "鱼珠",
113.39656 , 23.12196 "车陂南"GEORADIUS city 116.418017 39.914402 10 km withdist withcoord withhash count 10 desc
2.搜索最近地铁口
当我们需要查找距离给定位置最近的地铁口时,可以使用Redis的GEOPOS
命令来实现。例如,我们可以查找距离三溪,1公里以内的地铁口:
GEORADIUS city 116.418017 39.914402 1 km withdist withcoord withhash count 10 desc
在上述命令中,我们使用GEOPOS
命令查找距离给定位置(三溪站)1公里以内的地铁口。GEOPOS
命令返回的结果包括地铁口的ID、经纬度和距离。
三、Springboot整合Redis的GEO
定义接口,主要根据上述命令,新增添加数据坐标以及根据坐标查询地址两个接口
代码语言:javascript复制public interface GeoService {
String geoAdd();
GeoResults radiusByxy();
}
接口实现类,radiusByxy方法,默认当前位置为三溪 116.418017, 39.914402
代码语言:javascript复制@Service
public class GeoServiceImpl implements GeoService {
public static final String CITY = "city";
@Autowired
private RedisTemplate redisTemplate;
@Override
public String geoAdd() {
Map<String, Point> map = new HashMap<>();
map.put("三溪",new Point(113.42231 , 23.11034));
map.put("东圃",new Point(113.40815 , 23.11577));
map.put("鱼珠",new Point(113.43893 , 23.10612));
map.put("车陂南",new Point(113.39656 , 23.12196));
// 其实也是zset操作
redisTemplate.opsForGeo().add(CITY,map);
return map.toString();
}
@Override
public GeoResults radiusByxy() {
//通过经度,纬度查找附近的,当前位置 116.418017, 39.914402,距离1 km 的地铁
Point center = new Point(113.42377 , 23.10822);
Distance radius = new Distance(1, Metrics.KILOMETERS);
Circle circle = new Circle(center, radius);
// 返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance()
.includeCoordinates().sortDescending().limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().radius(CITY, circle, args);
return geoResults;
}
}
编写控制层,先调用新增坐标接口存储数据,再查询距离三溪 116.418017, 39.914402 1km的地铁
代码语言:javascript复制@Slf4j
@RestController
@RequestMapping("/geo")
public class GeoController {
@Resource
private GeoService geoService;
/**
* 添加坐标,模拟添加坐标,将百度真实坐标保存到redis
*/
@PostMapping(value = "/geoadd")
public String geoAdd()
{
return geoService.geoAdd();
}
/**
* 获取两个给定位置之间的距离
* @param member1
* @param member2
* @return
*/
@PostMapping(value = "/geodist")
public Distance distance(String member1, String member2)
{
return geoService.distance(member1,member2);
}
/**
* 通过经度纬度查找当前位置:经纬度:113.42377 , 23.10822 附近的地铁
* @return
*/
@PostMapping(value = "/georadius")
public GeoResults radiusByxy()
{
return geoService.radiusByxy();
}
}
添加数据返回结果
查询距离三溪 116.418017, 39.914402 1km的地铁
总结
通过以上步骤,我们可以使用Redis的GEO数据结构来实现最近地铁口的搜索。Redis的GEO数据结构支持多种搜索方式,可以灵活地满足不同的查找需求。在实际应用中,我们可以将地铁口的数据存储在一个哈希表中,然后将坐标添加到GEO数据结构中。当我们需要查找最近地铁口时,可以使用GEOPOS
命令来实现。这种方法可以大大提高查找效率,为用户提供更好的导航体验。
我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!