👨🎓作者简介:一位大四、研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集合即可。
编写测试类直接运行即可:
@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的查询结果,否则会报错。
如下所示,当分页的时候就会做分页查询: