'附近的店铺'功能实现记录

'附近的店铺'功能实现记录

Scroll Down

功能描述

根据用户的经纬度坐标,获得用户周围的营业及非营业店铺。

实现思路

之前没有做过这一类的功能,但是想到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'
   

测试

image.png
可以看到,需要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;

image.png

只需87ms...不用加多余字段,暂时看来这种方案很香,之后遇到问题再更新把。