八,Redis的GEO实现查看附件商户

85 阅读7分钟

一,GEO数据结构

GEO就是Geolocation的简写,代表地理坐标。Redis在3.2版本加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来搜索数据

1.1 GeoAdd

GeoAdd:添加一个地理空间信息,包含:经度(longitude),纬度(latitude),值(member)

  • 语法

    GEOADD key [NX|XX] [CH] longitude latitude member [ longitude latitude member...]
    
  • key:存储地理空间数据的键。

  • longitude:地理位置的经度。

  • latitude:地理位置的纬度。

  • member:地理位置的成员名称。

  • [NX|XX]

    • NX(Not Exists):只在成员不存在时才添加。如果指定了 NX,只有当 member 不存在时,longitudelatitude 才会被添加到 key 中。如果 member 已经存在,此操作不会做任何修改。

    • XX(Exists):只在成员已经存在时才添加。如果指定了 XX,只有当 member 已经存在时,longitudelatitude 才会被更新。如果 member 不存在,此操作不会做任何修改。

    • 这两个选项是互斥的,即你不能同时指定 NXXX。这些选项用于控制数据的添加策略,确保数据一致性或避免不必要的操作。

  • [CH]

    • CH(Changed):返回添加或更新的成员数量。默认情况下,GEOADD 命令只返回添加的成员数量。如果你指定了 CH,Redis 将返回所有被修改的成员数量(包括添加和更新的成员)。

举例:

# 只在 "Palermo" 不存在时才添加
GEOADD cities NX 13.361389 38.115556 "Palermo"
# 只在 "Catania" 已经存在时才更新
GEOADD cities XX 15.087269 37.502669 "Catania"
# 返回添加或更新的成员数量
GEOADD cities CH 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"

1.2 GeoDist

GeoDist:计算指定的两个点之间的距离并返回

  • 语法

    GeoDist key member1 member2 [unit]
    
    • key:存储地理空间数据的键。
    • member1:第一个成员。
    • member2:第二个成员。
    • unit(可选):距离的单位,可以是以下几种:
      • m:米(默认)
      • km:千米
      • ft:英尺
      • mi:英里

举例:

#添加信息
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
GEOADD cities 12.496366 41.902783 "Rome"
# 查看两个成员之间的距离
GEODIST cities "Palermo" "Catania"

1.3 GeoHash

GeoHash:将指定member的坐标转化为hash字符串并返回

  • 语法

    GeoHash key member [member ...]
    
    • key:存储地理空间数据的键。
    • member:成员。

举例

#添加信息
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
# 查看member的坐标转化成的hash字符串
GeoHash cities "Palermo" "Catania"

1.4 GeoPos

GeoPos:返回指定member的坐标

  • 语法

    GeoPos key member [member ...]
    
    • key:存储地理空间数据的键。
    • member:成员。

举例

#添加信息
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
# 查看两个member的坐标
GeoPos cities "Palermo" "Catania"

1.5 GeoRadius

GeoRadius:指定圆心,半径,找到该园内所有member,并按照与圆心之间的距离排序后返回(6.2之后已废弃)

  • 语法

    GeoRadius key longitude latitude radius unit [WithCoord] [WithDist] [WithHash] [Count count [Any] ] [ASC|DSC] [Store key] [StoreDist key]
    
    • key:存储地理空间数据的键。
    • longitude:中心点的经度。
    • latitude:中心点的纬度。
    • radius:查询的半径大小。
    • unit:距离的单位,可以是以下几种:
      • m:米
      • km:千米
      • ft:英尺
      • mi:英里
    • WithCoord:返回成员的经纬度。
    • WithDist:返回成员到中心点的距离。
    • WithHash:返回成员的 Geohash 编码。
    • Countcount [ANY]:限制返回的结果数量。ANY 表示可以返回任意数量的结果,用于提高查询效率。
    • ASC|DESC:结果按距离的升序或降序排序。
    • Storekey:将结果成员的名字存储到指定的键。
    • StoreDistkey:将结果成员的名字和距离存储到指定的键。

举例

GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
GEOADD cities 12.496366 41.902783 "Rome"
# 查询以指定经纬度为中心,半径为100公里的成员,并返回成员的经纬度和距离
GEORADIUS cities 15.087269 37.502669 100 km WITHCOORD WITHDIST
# 查询以指定经纬度为中心,半径为100公里的成员,按距离升序排序
GEORADIUS cities 15.087269 37.502669 100 km WITHCOORD WITHDIST ASC
# 查询以指定经纬度为中心,半径为100公里的成员,并将结果存储到指定的键
GEORADIUS cities 15.087269 37.502669 100 km STORE nearby_cities
# 查询以指定经纬度为中心,半径为100公里的成员,并将结果和距离存储到指定的键
GEORADIUS cities 15.087269 37.502669 100 km STOREDIST distances

1.6 GeoSearch

GeoSearch:在指定搜索范围内搜索member,并按照与指定点之间的距离排序后返回,范围可以是圆形,矩形。(6.2新功能)

  • 语法

    GeoSearch [FromMember member] [FromLonlat longitude latitude] [ByRadius radius unit] [ByBox with height unit] [ASC|DESC] [Count count[Any] ] [WithCoord] [WithDist] [WithHash]
    
    • key:存储地理空间数据的键

    • 中心点选项(必选一个)

      • FROMMEMBER member:以指定的成员为中心。
      • FROMLONLAT longitude latitude:以指定的经纬度为中心。
    • 搜索范围选项(必选一个)

      • BYRADIUS radius unit:指定搜索半径和单位。

        • radius:搜索半径。

        • unit:距离单位,可以是 m(米)、km(千米)、ft(英尺)、mi(英里)。

      • BYBOX width height unit:指定搜索矩形的宽度、高度和单位。

        • width:矩形的宽度。
        • height:矩形的高度。
        • unit:距离单位,可以是 m(米)、km(千米)、ft(英尺)、mi(英里)。
    • ASC:结果按距离升序排序。

    • DESC:结果按距离降序排序。

    • COUNT count [ANY]:限制返回结果的数量。ANY 表示可以返回任意数量的结果,以提高查询效率。

    • WITHCOORD:返回成员的经纬度。

    • WITHDIST:返回成员到中心点的距离。

    • WITHHASH:返回成员的 Geohash 编码。

举例

# 使用成员作为中心点,按半径进行搜索
GEOSEARCH cities FROMMEMBER "Catania" BYRADIUS 100 km WITHCOORD WITHDIST
# 使用经纬度作为中心点,按半径进行搜索
GEOSEARCH cities FROMLONLAT 15.087269 37.502669 BYRADIUS 100 km WITHCOORD WITHDIST
# 使用成员作为中心点,按矩形边界进行搜索
GEOSEARCH cities FROMMEMBER "Catania" BYBOX 200 200 km WITHCOORD WITHDIST

1.7 GeoSearchStore

GeoSearchStore:与GeoSearch功能一样,不过可以把结果存储到一个指定的key(6.2新功能)

  • 语法

    GeoSearchStore destination source [FromMember member] [FromLonlat longitude latitude] [ByRadius radius unit] [ByBox with height unit] [ASC|DESC] [Count count[Any] ] [WithCoord] [WithDist] [WithHash]
    
    • destination:存储搜索结果的键。
    • source:存储地理空间数据的键。

举例

# 使用成员作为中心点,按半径进行搜索并存储结果
GEOSEARCHSTORE nearby_cities cities FROMMEMBER "Catania" BYRADIUS 100 km
# 使用经纬度作为中心点,按半径进行搜索并存储结果
GEOSEARCHSTORE nearby_cities cities FROMLONLAT 15.087269 37.502669 BYRADIUS 100 km

二,附件商户搜索

2.1 导入店铺经纬度数据到GEO

image

void loadShopData(){
    //1.查询店铺信息
    List<Shop> list=shopService.list();
    //2.把店铺信息按照type_id分组
    Map<Long,List<Shop>> map=list.stream().collect(Collectors.groupBy(Shop::getTypeId));
    //3.分批完成写入Redis
    for(Map.Entry<Long,List<Shop>> entry : map.entrySet()){
        //获取类型id
        Long typeId=entry.getKey();
        String key="shop:geo:"+typeId;
        //获取同类型店铺集合
        List<Shop> value=entry.getValue();
        //写入redis GeoAdd 经度 纬度 Member(这种写法很低效,只是写出来了解)
        for(Shop shop: value){
            stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY(),shop.getId().toString() ));
        }
        
        //一次性写入(将Shop转化为Localtion)
        List<RedisGeoCommands.GeoLocation<String>> locations=new ArrayList<>();
        for(Shop shop:value){
          RedisGeoCommands.GeoLocation<String> location = new RedisGeoCommands.GeoLocation<>();
            location.setName(shop.getId().toString());
            location.setPoint(new Point(shop.getX(),shop.getY()) );
            locations.add(location);
        }
        stringRedisTemplate.opsForGeo().add(key,locations);
    }
}

2.2 搜索附近商户

控制层接口

    @GetMapping("/of/type")
    public Result queryShopByType(
            @RequestParam(value = "typeId") Integer typeId,
            @RequestParam(value = "current",defaultValue = "1") Integer current,
            @RequestParam(value = "x",required = false) Integer x,
            @RequestParam(value = "y",required = false) Integer y){

        return shopService.queryShopByType(typeId,current,x,y);
    }

Service层实现

public Result queryShopByType(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,10));
        return Result.ok(page.getRecords());
    }
    //2.计算分页参数
    int from=(current-1)*10;
    int end=current*10;
    //3.查询Redis,按照距离排序,分页 结果:shopId,distance
    //GeoSearch key ByLonLat x y ByRadius 5000 WithDistance Count end
    String key="shop:geo:"+typeId;
    GeoResults<RedisGeoCommands.GeoLocations<String> > results= stringRedisTemplage.opsForGeo()
        .search(
        key,
        GeoReference.fromCoordinate(x,y),
        new Distance(5000),
        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance()
        .limit(end)//代表从第一条开始到end条,需要我们手动截取
    );
    //4.解析出id
    if(results == null){
        return Result.ok(Collections.emptyList());
    }
    List<RedisGeoCommands.GeoLocations<String>> list=results.getContent();
    if(list.size() <= from){
        //没有下一页,结束
        return Result.ok(Collections.emptyList());
    }
    //截取from~end的部分
    List<Long> ids=new ArrayList<>();
    Map<String,Distance> distanceMap=new HashMap<>();
    list.skip(from).forEach(result ->{
        //获取店铺id
       String shopIdStr=result.getContent().getName(); 
       ids.add(Long.valueOf(shopIdStr));
       //获取距离
       Distance distance= result.getDistance();
       distanceMap.put(shopIdStr,distance);
    });
    //5.根据id查询出店铺信息
    String idStr=StrUtil.join(",",ids);
    List<Shop> shop = query().in("id",ids).last("ORDER BY FIELD(id,+"idStr"+)").list();
    for(Shop shop: shops){
        Distance dis = distanceMap.get(shopId().toString()).getValue();
        shop.setDistance(dis);
    }
    //6.返回
    return Result.ok(shops);
}