跳到主要内容

附近商户功能

在现代的应用开发中,地理位置相关的功能和用户行为统计变得越来越重要。本文将对黑马点评项目中附近商户功能进行深入剖析和优化。

GEO数据结构

GEO 数据结构简介:GEO(Geolocation)是 Redis 在 3.2 版本中加入的支持地理坐标信息存储和检索的数据结构。它允许我们根据经纬度来检索数据,常见的命令包括 GEOADD、GEODIST、GEOHASH、GEOPOS、GEOSEARCH 和 GEOSEARCHSTORE 等。

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

练习:

  1. 添加数据
    1. 北京南站 116.378248 39.865275
    2. 北京站116.42803 39.903738
    3. 北京西站 116.322287 39.893729
  2. 计算北京西站到北京站到距离
  3. 搜索天安门 116.397904 39.909005 附近10km内到所有火车站,并按照距离升序排序
GEOADD g1 116.378248 39.865275 bjn 116.42803 39.903738 bj 116.322287 39.893729 bjx

GEODIST g1 bjn bjx km

GEOSEARCH g1 FROMLONLAT 116.397904 39.909005 BYRADIUS 10 km WITHDIST

##导入店铺数据到 GEO

将数据库表中的店铺数据导入到 Redis 的 GEO 中。在 Redis 的 GEO 中,每个 member 对应一个经纬度,为了避免存储大量数据导致 Redis 内存压力过大,我们只存储店铺的 id。同时,为了能够根据商户类型对数据进行筛选,我们按照商户类型进行分组,将类型相同的商户存储在同一个 GEO 集合中,以 typeId 为 key。

    @Test
void loadShopData() {
// 查询店铺信息
List<Shop> list = shopService.list();
// 把店铺分组,按照typeId分组,id一致的放到一个集合
Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
Long typeId = entry.getKey();
List<Shop> shops = entry.getValue();
String key = "shop:geo:" + typeId;
// 写入redis geoadd key longitude latitude member
// for (Shop shop : shops) {
// stringRedisTemplate.opsForGeo()
// .add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
// }
List<RedisGeoCommands.GeoLocation<String>> location = new ArrayList<>(shops.size());
for (Shop shop : shops) {
location.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(), shop.getY()))
);
}
stringRedisTemplate.opsForGeo()
.add(key, location);
}
}

实现附近商户功能

控制器:

/**
* 根据商铺类型分页查询商铺信息
* @param typeId 商铺类型
* @param current 页码
* @return 商铺列表
*/
@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);
}

业务逻辑:

        @Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
// 是否需要根据坐标查询
if (x == null || y == null) {
// 不需要坐标查询,按照数据库查询
Page<Shop> page = this.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;
// 查询redis,按照距离排序,分页 结果:shopId,distance
String key = RedisConstants.SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> result = stringRedisTemplate.opsForGeo()
.search(key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
// 解析出id
if (result == null) {
return Result.ok();
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = result.getContent();
// 截取 from 到 end
if (list.size() <= from) {
return Result.ok();
}
ArrayList<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(res -> {
// 获取店铺id
String shopIdStr = res.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
// 获取距离
Distance distance = res.getDistance();
distanceMap.put(shopIdStr, distance);
});
// 根据id查询shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = this.query().in("id", ids)
.last("ORDER BY FIELD(id," + idStr + ")").list();
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
return Result.ok(shops);
}

在实现附近商户功能时,首先根据请求参数判断是否需要根据坐标查询。如果不需要,则按照数据库查询商铺信息;如果需要,则计算分页参数,然后查询 Redis 中的 GEO 数据,按照距离排序并分页获取商铺 id 和距离信息。最后,根据商铺 id 查询商铺详情,并将距离信息设置到商铺对象中返回。