7.1 GEO数据结构的认识及其基本使用演示
7.1.1 GEO的介绍
GEO,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:
GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
GEODIST:计算指定的两个点之间的距离并返回
GEOHASH:将指定member的坐标转为hash字符串形式并返回
GEOPOS:返回指定member的坐标
GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能
7.1.2 GEO的基本使用演示
通过 GEOADD group x y member 添加经纬坐标;
以下是通过ADD命令添加的三条数据:
北京南站( 116.378248 39.865275 )
北京站( 116.42803 39.903738 )
北京西站( 116.322287 39.893729 )
通过GEODIST group a地 b地 单位,计算两点间直线距离
以下,分别计算北京南到北京西、北京站到北京西的距离
通过GEOSEARCH ,计算附近xx距离的所有点的信息
以下是搜索天安门附近10km火车站
7.2 附近商户搜索功能的实现
7.2.1 需求分析及接口说明
这个需求需要使用GEO数据结构去统计以当前登录用户的地理位置为中心,向外扩散一段距离的店铺信息。同时,为了更好的对商店信息进行管理,我们需要先将店铺信息按类型存储到Redis中。
7.2.2 功能实现说明
1. 编写测试类,提前将店铺坐标信息存入Redis中
@Testvoid LoadShopData(){//1. 查询店铺信息List<Shop> list = shopService.list();//2. 把店铺按照typeId分组Map<Long,List<Shop>> shopMap = list.stream().collect(Collectors.groupingBy(shop -> shop.getTypeId()));//3. 分批写入redisfor(Map.Entry<Long,List<Shop>> entry : shopMap.entrySet()) {//3.1 获取类型idLong typeId = entry.getKey();String key = RedisConstants.SHOP_GEO_KEY + typeId;//3.2 获取同类型的店铺列表List<Shop> shops = entry.getValue();// Redis 将 name 和 Point 封装在一起的一种用法List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();//3.3 写入Redis GEOADD key x y memberfor(Shop shop : shops){ // stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()),shop.getId().toString());// 先把所有的点存好,再一次性存入Redis,性能更好locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(),shop.getY())));}stringRedisTemplate.opsForGeo().add(key, locations);}}
分类存放:
2. 更换依赖版本
原先的依赖没办法使用GEO的最新语法,我们需要在配置文件中进行版本更替
<!--排除旧版本--><exclusions><exclusion><artifactId>lettuce-core</artifactId><groupId>io.lettuce</groupId></exclusion><exclusion><artifactId>spring-data-redis</artifactId><groupId>org.springframework.data</groupId></exclusion></exclusions></dependency><!--引入新版本依赖--><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>
【推荐插件】Maven helper
用它可以更加方便的进行包管理
3. 修改queryShopByType接口,实现分类分页查询功能
需要传入的参数变成四个
/*** 根据商铺类型分页查询商铺信息* @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);}
实现类中:
【步骤说明】
1. 根据传参判断本次查询任务需不需要坐标,如果不需要,那就直接按照标准分页查询去做
2. 需要查坐标,则计算分页参数:
包括: 起始值 from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
终点值 end = current * SystemConstants.DEFAULT_PAGE_SIZE;
3. 在Redis中使用查询店铺地址信息
opsForGeo().search(...RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end))4. 使用getContent()方法解析出店铺信息、距离信息
5. 使用stream流截取from 到 end 部分的数据进行遍历
6. 获取店铺id、距离信息分装成一一对应的Map集合
7. 通过ids集合,查询店铺信息集合
8. 通过Map集合,匹配店铺信息结合与距离信息
9. 返回结果
/*** 根据类型分页分类查询店铺信息* @param typeId* @param current* @param x* @param y* @return*/@Overridepublic 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, 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 、 distanceString key = SHOP_GEO_KEY + typeId;GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(key,GeoReference.fromCoordinate(x,y),new Distance(Shop_FEO_DISTANCE),RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));//4. 解析出idif(results == null){return Result.ok(Collections.emptyList());}// 解析List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();if(list.size() <= from){return Result.ok(Collections.emptyList());}//4.1 截取 from 到 end 的部分 // list.subList(from,end);// 定义一个集合,保存店铺id和距离的关系List<Long> ids = new ArrayList<>(list.size());Map<String,Distance> distanceMap = new HashMap<>(list.size());list.stream().skip(from).forEach(result -> {// 4.2 获取店铺idString shopIdStr = result.getContent().getName();ids.add(Long.valueOf(shopIdStr));// 4.3 获取距离Distance distance = result.getDistance();distanceMap.put(shopIdStr,distance);});//5. 查询店铺List<Shop> shops = query().in("id",ids).last("ORDER BY FIELD(id," + StrUtil.join(",",ids) + ")").list();// 匹配店铺距离for(Shop shop : shops) {shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());}//6. 返回结果return Result.ok(shops);}