👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战12(好友关注、Feed流(关注推送)、滚动分页查询)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助
附近的人、附近商铺这种功能现实中很常见,很显然,这种功能需要地理坐标,Redis中刚好就有实现这类功能的数据结构——GEO。
GEO实现附近商铺
- GEO数据结构基本用法
- 导入店铺数据到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集合即可。
编写测试类直接运行即可:
@Testvoid loadShopData(){//查询店铺信息List<Shop> list = shopService.list();//把店铺分组,按照typeId分组,id一致的放到一个集合Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));//分批完成导入Redisfor (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {//获取类型idLong 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:
@Overridepublic 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与distanceString key = SHOP_GEO_KEY + typeId;//SHOP_GEO_KEY = "shop:geo:"//GEOSEARCH key BYLONLAT x y BYRADIUS 5000 WITHDISTANCEGeoResults<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());}//解析出idList<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();//System.out.println(list);if(list.size() <= start){//没有下一页了,没办法执行skip,直接返回return Result.ok(Collections.emptyList());}//收集Long类型的店铺idList<Long> ids = new ArrayList<>(list.size());Map<String, Distance> distanceMap = new HashMap<>(list.size());//截取start到end的分页部分,可以用stream的skip,效率更高list.stream().skip(start).forEach(result -> {//获取店铺idString shopIdStr = result.getContent().getName();//收集起来ids.add(Long.valueOf(shopIdStr));//获取距离Distance distance = result.getDistance();distanceMap.put(shopIdStr, distance);});//System.out.println(distanceMap);//根据id查询shopString 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的查询结果,否则会报错。
如下所示,当分页的时候就会做分页查询: