「黑马点评」十、附近商户

2025 年 4 月 28 日 星期一
1

「黑马点评」十、附近商户

GEO 数据结构

Geolocation 代表地理坐标,以下为常见坐标

image.png|500

image.png|500

练习 Redis GEO

  1. 添加以下信息
    • 北京南站 116.378248 39.865275
    • 北京站 116.42803 39.903738
    • 北京西站 116.322287 39.893729
  2. 计算北京西站到北京站的距离
  3. 搜索天安门 116.397904 39.909005 附近 10km 内的所有火车站,并按照距离升序排列

添加 命令 GEOADD key longitude latitude member [longitude latitude member] 示例 GEOADD g1 116.378248 39.865275 bjn 116.42803 39.903738 bjz 116.322287 39.893729 bjx

实际上观察 Redis 可以发现 GEO 底层使用的是 ZSET,value 即为 member,经纬度转换成数字存储为 score;add 到顺序也是,先 score/geo 后 value/member

距离 命令 GEODIST key member1 member2 示例 GEODIST g1 bjn bjx [m/km]

搜索 命令 GEOSEARCH key FROMLONLAT longitude latitude BYRADIUS radius m/km WITHDIST 示例 GEOSEARCH g1 FROMLONLAT 116.397904 39.909005 BYRADIUS 10 km WITHDIST

哈希:将经纬度转化为二进制再对应成 hash 命令 GEOHASH key member 示例 GEOHASH g1 bjz

导入店铺位置到 Redis

按照商户类型分组,以 typeId 作为 key 存入 GEO 集合

演示了如何利用 java 代码,将数据库的商户一次性按类型分组导入到 Redis

@Test
void loadShopData() {
    // 1.查询店铺信息
    List<Shop> list = shopService.list();
    // 2.把店铺分组,按照typeId分组,typeId一致的放到一个集合
    Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
    // 3.分批完成写入Redis
    for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
        // 3.1.获取类型id
        Long typeId = entry.getKey();
        String key = SHOP_GEO_KEY + typeId;
        // 3.2.获取同类型的店铺的集合
        List<Shop> value = entry.getValue();
        List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
        // 3.3.写入redis GEOADD key 经度 纬度 member
//            for(Shop shop : value){ // 这样要建立 value.size() 次连接,慢
//                stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()),shop.getId().toString()) ;
//            }
        for (Shop shop : value) {
            locations.add(new RedisGeoCommands.GeoLocation<>(
                    shop.getId().toString(),
                    new Point(shop.getX(), shop.getY())
            ));
        }
        stringRedisTemplate.opsForGeo().add(key, locations);
    }
}

附近商户实现

获取附近商户:GET /api/shop/of/type?&typeId={typeId}&current={currentPage}&x={longitude}&y={latitude}

注意这里批量查询 shop 也需要 order by filed('id', ids),保证按照距离最近的顺序返回

这里 Redis 使用的命令只能设置 radius,并且能限制查询个数 limit,但是无法跳过 from,所以手动截取分页,利用 steam 流 skip(from)

这里要注意哪怕 list 不为空,跳过了 from 个也可能为空,所以需要做判断

@GetMapping("/of/type")
public Result<List<Shop>> queryShopByType(
        @RequestParam("typeId") Integer typeId,
        @RequestParam(value = "current", defaultValue = "1") Integer current,
        @RequestParam(value = "x", required = false) Double x,
        @RequestParam(value = "y", required = false) Double y
) {
    return Result.success(shopService.queryShopByType(typeId, current, x, y));
}

@Override
public List<Shop> queryShopByType(Integer typeId, Integer current, Double x, Double y) {
    if (x == null || y == null) {
        int offset = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        List<Shop> shopList = shopMapper.selectByType(typeId, offset, SystemConstants.DEFAULT_PAGE_SIZE);
        return shopList;
    }
    int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
    int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
    String key = RedisConstants.SHOP_GEO_KEY + typeId;
    GeoResults<RedisGeoCommands.GeoLocation<String>> result = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 km WITHDIST
        .search(
            key, 
            GeoReference.fromCoordinate(x, y),
            new Distance(5000),
            RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
        );
    if (result == null) {
        return Collections.emptyList();
    }
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = result.getContent();
    if (list.size() <= from) {
        return Collections.emptyList();
    }
    List<Long> ids = new ArrayList<>(end - from);
    Map<String, Distance> distanceMap = new HashMap<>(end - from);
    list.stream().skip(from).forEach(geoResult -> {
        String shopIdStr = geoResult.getContent().getName();
        ids.add(Long.valueOf(shopIdStr));
        Distance distance = geoResult.getDistance();
        distanceMap.put(shopIdStr, distance);
    });
    List<Shop> shops = shopMapper.selectShopByIds(ids);
    shops.forEach(shop -> {
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
    });
    return shops;
}

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...