Skip to content

附近商户

要点:

  • GEO数据结构的使用

1.1 GEO数据结构

image-20260421164231324

  • 命令速查;
命令功能描述语法示例
GEOADD添加一个或多个地理位置坐标GEOADD city 116.40 39.90 beijing
GEOPOS获取指定成员的经纬度GEOPOS city beijing
GEODIST计算两个位置之间的距离GEODIST city beijing shanghai km
GEORADIUS以给定的经纬度为中心,寻找半径内的成员GEORADIUS city 116.40 39.90 100 km
GEORADIUSBYMEMBER以指定成员为中心,寻找半径内的成员GEORADIUSBYMEMBER city beijing 100 km
GEOHASH返回成员的 GeoHash 字符串形式GEOHASH city beijing
GEOSEARCH(Redis 6.2+) 综合搜索,支持矩形或圆形区域GEOSEARCH city FROMMEMBER beijing BYRADIUS 50 km
  • 注意:
    • 距离单位:支持 m(米)、km(千米)、mi(英里)和 ft(英尺)。
    • 底层原理:当你执行 GEOADD 时,Redis 实际上是将数据存入了 ZSET。你可以直接用 ZREM 来删除某个地理位置成员,或者用 ZRANGE 查看所有成员。
    • 坐标限制
      • 有效经度范围:$-180^\circ$ 到 $180^\circ$。
      • 有效纬度范围:$-85.05112878^\circ$ 到 $85.05112878^\circ$(靠近极点地区无法直接使用)。
    • 性能:GEO 操作的时间复杂度通常为 $O(\log N)$(添加)或 $O(N + K)$(范围搜索,其中 $K$ 是范围内元素的数量),在大数据量下性能非常出色。

1.2 附近商户搜索

1.2.1 需求

条件查询商户

1.2.2 代码实现

  1. 接口

    image-20260421174717741

  2. 实现思路

    • 将店铺坐标信息导入Redis中,键的设计如下

      image-20260421175717932

    • 分页查询时,先查Redis根据位置信息将店铺排序,后面再做分页。

  3. 代码实现

    • 导入Redis

      java
      /**
       * 将店铺经纬度信息加载到Redis
       *
       */
      @Test
      void loadShopInfo(){
          // 1. 查询店铺信息
          List<Shop> list = shopService.query().list();
          if (list == null || list.isEmpty()) {
              return;
          }
          // 2. 导入 键值对信息: shop:geo:typeId -> [shopId, x坐标, y坐标]
          for (Shop shop : list) {
              stringRedisTemplate.opsForGeo().add(RedisConstants.SHOP_GEO_KEY + shop.getTypeId(),
                      new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX(), shop.getY())));
          }
      }
    • 更换依赖,以支持 GeoSearch

      xml
      <dependency>
          <groupId>org.springframework.data</groupId>
          <artifactId>spring-data-redis</artifactId>
          <version>2.6.2</version>
      </dependency>
      <dependency>
          <groupId>io.lettuce</groupId>
          <artifactId>lettuce-core</artifactId>
          <version>6.1.6.RELEASE</version>
      </dependency>
    • 编写条件查询

      java
          public Result quertShopByLocationAndType(Integer typeId, Integer current, Double x, Double y) {
              // 1. 判断是否需要根据坐标查询
              if (x == null || y == null) {
                  Page<Shop> page = query()
                          .eq("type_id", typeId)
                          .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
                  return Result.ok(page.getRecords());
              }
              // 2. 计算分页查询参数
              int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
              int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
              // 3. 查询Redis,按照距离排序、分页。 结果:shopId、distance
              String key = RedisConstants.SHOP_GEO_KEY + typeId;
              // 0 , end 条,需要手动截取
              GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = stringRedisTemplate.opsForGeo().search(
                      key,
                      GeoReference.fromCoordinate(x, y),
                      new Distance(5000), // 5km
                      RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
              );
              if (geoResults == null || geoResults.getContent().isEmpty()) {
                  // 3.1 没有结果,返回空列表
                  return Result.ok(Collections.emptyList());
              }
              List<Long> ids = new ArrayList<>(SystemConstants.DEFAULT_PAGE_SIZE);
              HashMap<String, Distance> map = new HashMap<>(SystemConstants.DEFAULT_PAGE_SIZE);
      
              // 3.2 如果List的条数小于from,就不需要往下走了
              if (geoResults.getContent().size() <= from) {
                  // 没有下一页了
                  return Result.ok(Collections.emptyList());
              }
      
              geoResults.getContent().stream().skip(from).forEach(result -> {
                  // 4. 解析id
                  String shopIdStr = result.getContent().getName();
                  ids.add(Long.valueOf(shopIdStr));
                  // 获取距离
                  Distance distance = result.getDistance();
                  map.put(shopIdStr, distance);
              });
              // 5. 根据id查询Shop
              List<Shop> list = query().in("id", ids).last("ORDER BY FIELD(id," + StrUtil.join(",", ids) + ")").list();
              list.forEach(shop -> {
                  shop.setDistance(map.get(shop.getId().toString()).getValue());
              });
              // 6. 返回
              return Result.ok(list);
          }