功能描述
根据用户的经纬度坐标,获得用户周围的营业及非营业店铺。
实现思路
之前没有做过这一类的功能,但是想到redis有一个GEOHASH,就去百度了一下,发现网上大部分实现的思路有大概三种。mysql+外接长方形、mysql+geohash、redis+geohash。
2021年7月30日更新
用MYSQL+GEO方式实现了一下然后测试了一下性能,因为店铺数据感觉不会很多,所以就在表中插入了1W个特定地点的周围1000公里范围的店铺。但是很失望,这种方案测试了一下效率感觉很好了,但是发现有个大兄弟提出了另外一种思路,直接通过sql就处理了这个问题。。效率比geo高20倍左右。。。
方案对比
mysql+geoHash
1、存储时,将经纬度转化为GEOCODE并存到数据表中。
2、根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码
3、通过模糊搜索方式搜索相似的hashcode并过滤。
4、通过距离排序。
5、分页(略)
代码:
实现类
/**
* 查询附近的店铺
*
* @param longitude 经度
* @param latitude 纬度
* @param len geoHash的精度
* @param scope 距离范围 单位km
*
* @return 店铺信息
*/
@Override
public List<ShopGeoHash> selectNearBySearch(double longitude, double latitude, int len, double scope) {
//根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码
GeoHash geoHash = GeoHash.withCharacterPrecision(latitude, longitude, len);
//获取到用户周边8个方位的geoHash码
GeoHash[] adjacent = geoHash.getAdjacent();
//模糊搜索
Set<ShopGeoHash> shops = new HashSet<ShopGeoHash>();
for (GeoHash hash : adjacent) {
List<ShopGeoHash> shopGeoHashes = shopGeoMapper.selectShopGeoLikeRight(hash.toBase32());
//写入用户点位与店铺的距离
for(ShopGeoHash geo:shopGeoHashes){
double distance = getDistance(Double.valueOf(geo.getLongitude()), Double.valueOf(geo.getLatitude()), longitude, latitude);
geo.setDistance(distance);
shops.add(geo);
}
}
//set转list 重写equals、hashCode
List<ShopGeoHash> result = new ArrayList<ShopGeoHash>(shops);
//过滤超出距离的
result = result.stream()
.filter(a ->a.getDistance()<= scope)
.sorted((Comparator.comparing(ShopGeoHash::getDistance)))
.collect(Collectors.toList());
return result;
}
/***
* 球面中,两点间的距离
* @param longitude 经度1
* @param latitude 纬度1
* @param userLng 经度2
* @param userLat 纬度2
* @return 返回距离,单位km
*/
private 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;
}
mapper
select shop_id, shop_name, shop_service_num, shop_star, shop_photo, shop_status, longitude, latitude, geo_code from shop_info where and geo_code like concat(#{geoCode}, '%') and del_flag = '0' and shop_check = '0' and shop_status = '0'
测试
可以看到,需要890MS。调用这个接口。
SQL实现
实现类
/**
* 查询附近的店铺
*
* @param longitude 经度
* @param latitude 纬度
* @param len geoHash的精度
* @param scope 距离范围 单位km
*
* @return 店铺信息
*/
@Override
public List<ShopGeoHash> selectNearBySearch(double longitude, double latitude, int len, double scope) {
//查询附近的店铺
List<ShopGeoHash> shopGeoHashes = shopGeoMapper.selectShopGeoLikeRight(longitude,latitude,scope);
Set<ShopGeoHash> shops = new HashSet<ShopGeoHash>();
for(ShopGeoHash geo:shopGeoHashes){
double distance = getDistance(Double.valueOf(geo.getLongitude()), Double.valueOf(geo.getLatitude()), longitude, latitude);
geo.setDistance(distance);
shops.add(geo);
}
List<ShopGeoHash> result = new ArrayList<ShopGeoHash>(shops);
result = result.stream()
.sorted((Comparator.comparing(ShopGeoHash::getDistance)))
.collect(Collectors.toList());
return result;
}
/***
* 球面中,两点间的距离
* @param longitude 经度1
* @param latitude 纬度1
* @param userLng 经度2
* @param userLat 纬度2
* @return 返回距离,单位km
*/
private 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;
}
sql
select * from shop_info where (acos(sin((#{latitude}*3.1415)/180)
* sin((latitude*3.1415)/180) + cos((#{latitude}*3.1415)/180)
* cos((latitude*3.1415)/180) * cos((#{longitude}*3.1415)/180
- (longitude*3.1415)/180))*6371.393) <![CDATA[<]]> #{scope} and del_flag = '0' and shop_check = '0' and shop_status = '0'
测试
有的时候感觉第一次查询慢,之后会快很多,所以这次在测试前我先清理一下mysql缓存。
reset query cache;
只需87ms...不用加多余字段,暂时看来这种方案很香,之后遇到问题再更新把。