使用GEO数据结构实现附近商铺功能

2.8k 词

数据存储

商户的地理信息一般用经纬度表示,在这里,我们选择Redis的GEO数据结构进行存储。

Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。

常用的操作有:

  • geoadd:添加地理位置的坐标。
  • geopos:获取地理位置的坐标。
  • geodist:计算两个位置之间的距离。
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
  • geohash:返回一个或多个位置对象的 geohash 值。

导入数据

店铺Shop在包含的主要信息有

  • id:店铺id
  • typeId:店铺类型id
  • x:经度
  • y:维度

因为查询涉及到店铺的类型,所以我们按照以下的形式来进行存储:

  • keytypeId
  • membeerid
  • valuexy的hash值

代码实现流程

  1. 查询店铺
  2. 把店铺分组,按照typeId分组,一致的放到一个集合
  3. 分批写入Redis
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Test
    public void loadShopData() {
    // 获取店铺信息
    List<Shop> shops = shopService.list();
    // 按照typeId进行数据分批
    Map<Long, List<Shop>> shopMap = shops.stream().collect(Collectors.groupingBy(Shop::getTypeId));
    // 分批写入Redis
    for (Map.Entry<Long, List<Shop>> entry: shopMap.entrySet()) {
    Long typeId = entry.getKey();
    List<Shop> shopList = entry.getValue();
    // 所有的店铺信息
    List<RedisGeoCommands.GeoLocation<String>> locations = shopList.stream()
    .map(shop -> new RedisGeoCommands.GeoLocation<String>(
    shop.getId().toString(),
    new Point(shop.getX(), shop.getY())
    ))
    .toList();
    redisTemplate.opsForGeo().add(RedisConstants.SHOP_GEO_KEY + typeId, locations);
    }
    }

数据查询

实现流程:

  1. 判断是否需要根据坐标查询
  2. 计算分页参数
  3. 查询redis, 按照距离排序、分页
  4. 解析出id
  5. 根据id查询shop
  6. 返回
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    @Override
    public List<Shop> queryByType(Integer typeId, Integer current, Double x, Double y) {
    // 判断是否需要分页
    // 根据类型分页查询
    if (x == null && y == null) {
    Page<Shop> page = query()
    .eq("type_id", typeId)
    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
    // 返回数据
    return page.getRecords();
    }
    // 计算分页参数
    int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
    int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
    // 查询redis,并排序分页
    GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo()
    .search(
    RedisConstants.SHOP_GEO_KEY + typeId,
    GeoReference.fromCoordinate(x, y),
    new Distance(5000),
    RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
    );
    // 解析出id
    if (results == null) return Collections.emptyList();
    List<Long> ids = new ArrayList<>();
    Map<Long, Distance> distanceMap = new HashMap<>();
    results.getContent().stream().skip(from).forEach(result -> {
    Long shopId = Long.valueOf(result.getContent().getName());
    ids.add(shopId);
    Distance distance = result.getDistance();
    distanceMap.put(shopId, distance);
    });
    // 根据id查询shop
    if (ids.isEmpty()) return Collections.emptyList();
    String idStr = StrUtil.join(",", ids);
    List<Shop> shops = lambdaQuery()
    .in(Shop::getId, ids)
    .last("ORDER BY FIELD(id, " + idStr + ")")
    .list();
    shops.forEach(shop -> shop.setDistance(distanceMap.get(shop.getId()).getValue()));
    // 返回
    return shops;
    }
    于是,附近商铺功能就实现好了
    注意
    Redis版本必须大于6.2,否则没有geoSearch功能
    这里贴一个windows-redis链接吧,感谢开源大佬
    Windows-Redis-7.2