【小众点评项目】 开发日记 DAY 06 附近商铺

1.Redis中的GEO数据结构

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

  • GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
  • GEODIST:计算指定的两个点之间的距离并返回
  • GEOHASH:将指定member的坐标转为hash字符串形式并返回
  • GEOPOS:返回指定member的坐标
  • GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.以后已废弃
  • GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
  • GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能

2.附近商铺功能

2.1 应用场景

当我们点击美食之后,会出现一系列的商家,商家中可以按照多种排序方式,我们此时关注的是距离,这个地方就需要使用到我们的GEO,向后台传入当前app收集的地址(我们此处是写死的) ,以当前坐标作为圆心,同时绑定相同的店家类型type,以及分页信息,把这几个条件传入后台,后台查询出对应的数据再返回。
在这里插入图片描述

2.2 思路分析

使用Redis的GEO数据类型为我们基于位置查询商铺的功能。我们需要做一下的工作

  • 首先就是要将所有的商品的信息和地理位置坐标都保存到redis中。但是考虑到redis基于内存,空间受限,因此在redis中仅仅保存地理位置坐标和商铺的id。
  • 其次是用户会根据类别来查询商铺的距离,因此我们不能将所有的商铺信息都保存到一个redis的key中,这样我们在查找的时候无法区别商铺的类型,因此应该分别保存。
  • 因此我们在redis中应该这样保存商铺的信息
    • key使用geo:shop:TypeId
    • 键就是shopId和地理位置坐标

在这里插入图片描述

2.3 代码实现

将商铺信息写入redis,这里我们直接在测试类中将信息写入。

@Test
public void loadDataToRedis() {
    // 获取所有商家信息
    List<Shop> shopList = shopService.list();
    // 按照商家类型分组
    Map<Long, List<Shop>> map = shopList.stream().collect(Collectors.groupingBy(Shop::getTypeId));
    // 遍历所有商家 将商家添加到redis中
    for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
        Long typeId = entry.getKey();
        String key = SHOP_GEO_KEY + typeId;
        List<Shop> list = entry.getValue();
        // 这里面的那个泛型指的是geo里面保存的那个member的值的类型 这里我们保存的是shopId
        List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(list.size());
        // 遍历当前typeId的所有商铺信息
        for (Shop shop : list) {
            RedisGeoCommands.GeoLocation<String> geoLocation = new RedisGeoCommands.GeoLocation<>(
                    shop.getId().toString(),
                    new Point(shop.getX(), shop.getY())
            );
            locations.add(geoLocation);
        }
        stringRedisTemplate.opsForGeo().add(key, locations);
    }
}

题外话:这里我们首先查询了所有的商铺信息,然后根据TypeId分别生成不同的list。使用了stream流中相关语法,可以快速的根据某一个值进行分组,返回值为map。key是分组的那个字段,值为每一个的list

附近商铺功能相关代码

/**
 * @param typeId  商铺类型ID
 * @param current 页号
 * @param x       经度
 * @param y       纬度
 * @return
 */
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
    // TODO 如果没有位置坐标,则按照之前的查询方式查询
    if (x == null || y == null) {
        // 根据类型分页查询
        Page<Shop> page = super.query()
                .eq("type_id", typeId)
                .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
        // 返回数据
        return Result.ok(page.getRecords());
    }

    // 计算分页信息
    int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
    int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

    String key = SHOP_GEO_KEY + typeId;
    // TODO 如果有位置坐标 则需要根据位置坐标到Redis中查询店铺的ID
    GeoResults<RedisGeoCommands.GeoLocation<String>> result = stringRedisTemplate.opsForGeo().radius(
            key,
            new Circle(new Point(x, y), new Distance(5, Metrics.KILOMETERS)), // 方圆5KM
            RedisGeoCommands.GeoRadiusCommandArgs.
                    newGeoRadiusArgs().
                    includeDistance(). // 查询结果包含距离
                    sortAscending(). // 按照距离从近到远
                    limit(end) // 分页 显示从第一条到第end条
    );

    // TODO 根据从redis中查询到的结果封装
    if (result == null) {
        return Result.ok(Collections.emptyList());
    }
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = result.getContent();

    if (content.size() <= from) {
        return Result.ok(Collections.emptyList());
    }

    List<Long> shopIdList = new ArrayList<>(content.size());
    Map<String, Distance> distanceMap = new HashMap<>(content.size());
    content.stream().skip(from).forEach(geoResult -> {
        String shopIdStr = geoResult.getContent().getName();
        shopIdList.add(Long.valueOf(shopIdStr));
        Distance distance = geoResult.getDistance();
        distanceMap.put(shopIdStr, distance);
    });

    // 根据shopId查询商铺信息

    String idsStr = StrUtil.join(",", shopIdList);
    List<Shop> shopList = super.query().in("id", shopIdList).last("order by field(id, " + idsStr + ")").list();
    for (Shop shop : shopList) {
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
    }
    return Result.ok(shopList);
}

需要注意的是,使用geo查询附近的商铺指定查询多少条记录都是从第一条记录开始。比如指定查询5条,那么结果就是从第1条到第5条。换句话说只能指定终止位置,起始位置始终是第1条记录。

GeoResults<RedisGeoCommands.GeoLocation<String>> result = stringRedisTemplate.opsForGeo().radius(
            key,
            new Circle(new Point(x, y), new Distance(5, Metrics.KILOMETERS)), // 方圆5KM
            RedisGeoCommands.GeoRadiusCommandArgs.
                    newGeoRadiusArgs().
                    includeDistance(). // 查询结果包含距离
                    sortAscending(). // 按照距离从近到远
                    limit(end) // 分页 显示从第一条到第end条 只能指定终止位置,起始位置始终是第1条记录。
    );

因此我们如果查询第2页的内容也只能是跳过一部分元素

content.stream().skip(from)

另外,判断是否还有元素没有读取的方法也变成了下面这样。

// 计算分页信息
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE; // 起始位置
int end = current * SystemConstants.DEFAULT_PAGE_SIZE; // 终止位置

// 如果起始位置的长度都大于等于content的长度了,说明没有新店铺了。
if (content.size() <= from) {
        return Result.ok(Collections.emptyList());
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值