Redis:原理速成+项目实战——Redis实战13(GEO实现附近商铺、滚动分页查询)

发布时间:2024年01月13日

👨?🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战12(好友关注、Feed流(关注推送)、滚动分页查询)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助

附近的人、附近商铺这种功能现实中很常见,很显然,这种功能需要地理坐标,Redis中刚好就有实现这类功能的数据结构——GEO。

GEO数据结构基本用法

GEO全称Geolocation,代表地理坐标,Redis允许其存储地理坐标,帮助我们根据经纬度来检索数据。

GEOADD:添加地理空间信息,包含经度、维度、值
GEODIST:计算两点距离并返回
GEOHASH:将指定member的坐标转为hash字符串形式并返回
GEOPOS:返回指定member的坐标
GEOSEARCH:在指定返回内搜索member,并按照与指定点之间的距离排序后并返回。范围可以是圆形或矩形
GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key

搜索北京天安门附近10km内的所有火车站,并按照升序排序,即可用GEO相关命令,其底层也正好是SortedSet:

GEOSEARCH g1 FROMLONLAT 经度 维度 BYRADIUS 10 km WHITDIST

其中,g1存储了北京所有火车站的经纬度,FROMLONLAT表示输入的内容是经纬度,BYRADIUS表示按照圆来搜索,WHITDIST表示带上半径。

导入店铺数据到GEO

当点击某种类型的商户的时候,就应该要发出GET请求,将商户类型,页码,经纬度都作为请求参数,并且最后根据距离位置来排序,返回List<Shop>。
因为我们要利用Redis来实现距离的计算,因此所有的商户的经纬度信息都应该要存储进去,而GEO的存储结构key-value结构,其中value是经纬度和member,这里的member我们只需要将商铺的id传进去即可。
商铺的查询是根据商铺类型来做分组的,所以要将类型相同的商铺作为同一组,将typeId为key存入GEO集合即可。

编写测试类直接运行即可:

	@Test
    void loadShopData(){
        //查询店铺信息
        List<Shop> list = shopService.list();
        //把店铺分组,按照typeId分组,id一致的放到一个集合
        Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        //分批完成导入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 key 经度 维度 member
//            for (Shop shop : value) {
//                stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
//            }
            //上述方式更慢,可以直接传位置集合的迭代器
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
            for (Shop shop : value){
                locations.add(new RedisGeoCommands.GeoLocation<>(
                        shop.getId().toString(),
                        new Point(shop.getX(), shop.getY())));
            }
            stringRedisTemplate.opsForGeo().add(key, locations);
        }
    }

在这里插入图片描述

实现附近商户功能

SpringDataRedis2.3.9不支持GEOSEARCH命令,一次你我们需要提示其版本,修改POM文件,首先我们需要将下面两个依赖exclude:
在这里插入图片描述
在这里插入图片描述
接着手动添加依赖,并指定版本:

	<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>

ShopController:

	@GetMapping("/of/type")
    public Result 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 shopService.queryShopByType(typeId, current, x, y);
    }

ShopServiceImlp:

    @Override
    public Result queryShopByType(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 Result.ok(page.getRecords());
        }
        //计算分页参数
        int start = (current - 1) * DEFAULT_BATCH_SIZE;
        int end = current * DEFAULT_BATCH_SIZE;
        //查询Redis,按照距离来进行排序、分页,结果:shopId与distance
        String key = SHOP_GEO_KEY + typeId;//SHOP_GEO_KEY = "shop:geo:"
        //GEOSEARCH key BYLONLAT x y BYRADIUS 5000 WITHDISTANCE
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),//方圆5公里以内的店铺
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance()//WITHDISTANCE
                                .limit(end)//查询到的结果还要满足分页的情况,但是只能指定[0,end],剩下要逻辑分页
                );
        if(results == null){
            return Result.ok(Collections.emptyList());
        }
        //解析出id
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        //System.out.println(list);
        if(list.size() <= start){
            //没有下一页了,没办法执行skip,直接返回
            return Result.ok(Collections.emptyList());
        }

        //收集Long类型的店铺id
        List<Long> ids = new ArrayList<>(list.size());
        Map<String, Distance> distanceMap = new HashMap<>(list.size());

        //截取start到end的分页部分,可以用stream的skip,效率更高
        list.stream().skip(start).forEach(result -> {
            //获取店铺id
            String shopIdStr = result.getContent().getName();
            //收集起来
            ids.add(Long.valueOf(shopIdStr));
            //获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        //System.out.println(distanceMap);
        //根据id查询shop
        String idStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        //需要将距离参数传入shops,返还到前端
        for (Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }

        return Result.ok(shops);
    }

这个代码中,查询很容易,比较有难度的地方就是做分页的时候,除了要用limit限定最低的end的范围,还要自己手动去写逻辑分页的代码,这部分比较复杂,而且我们必须要判断list.size()是否比start小,是的话才能实现这部分的逻辑分页,否则直接返回到end的查询结果,否则会报错。

如下所示,当分页的时候就会做分页查询:
在这里插入图片描述

文章来源:https://blog.csdn.net/m0_52380556/article/details/135566822
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。