「黑马点评」十、附近商户
GEO 数据结构
Geolocation 代表地理坐标,以下为常见坐标

image.png|500
练习 Redis GEO
- 添加以下信息
- 北京南站 116.378248 39.865275
- 北京站 116.42803 39.903738
- 北京西站 116.322287 39.893729
- 计算北京西站到北京站的距离
- 搜索天安门 116.397904 39.909005 附近 10km 内的所有火车站,并按照距离升序排列
添加
命令 GEOADD key longitude latitude member [longitude latitude member]
示例 GEOADD g1 116.378248 39.865275 bjn 116.42803 39.903738 bjz 116.322287 39.893729 bjx
实际上观察 Redis 可以发现 GEO 底层使用的是 ZSET,value 即为 member,经纬度转换成数字存储为 score;add 到顺序也是,先 score/geo 后 value/member
距离
命令 GEODIST key member1 member2
示例 GEODIST g1 bjn bjx [m/km]
搜索
命令 GEOSEARCH key FROMLONLAT longitude latitude BYRADIUS radius m/km WITHDIST
示例 GEOSEARCH g1 FROMLONLAT 116.397904 39.909005 BYRADIUS 10 km WITHDIST
哈希:将经纬度转化为二进制再对应成 hash
命令 GEOHASH key member
示例 GEOHASH g1 bjz
导入店铺位置到 Redis
按照商户类型分组,以 typeId 作为 key 存入 GEO 集合
演示了如何利用 java 代码,将数据库的商户一次性按类型分组导入到 Redis
@Test
void loadShopData() {
// 1.查询店铺信息
List<Shop> list = shopService.list();
// 2.把店铺分组,按照typeId分组,typeId一致的放到一个集合
Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
// 3.分批完成写入Redis
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
// 3.1.获取类型id
Long typeId = entry.getKey();
String key = SHOP_GEO_KEY + typeId;
// 3.2.获取同类型的店铺的集合
List<Shop> value = entry.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
// 3.3.写入redis GEOADD key 经度 纬度 member
// for(Shop shop : value){ // 这样要建立 value.size() 次连接,慢
// stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()),shop.getId().toString()) ;
// }
for (Shop shop : value) {
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(), shop.getY())
));
}
stringRedisTemplate.opsForGeo().add(key, locations);
}
}
附近商户实现
获取附近商户:GET /api/shop/of/type?&typeId={typeId}¤t={currentPage}&x={longitude}&y={latitude}
注意这里批量查询 shop 也需要 order by filed('id', ids),保证按照距离最近的顺序返回
这里 Redis 使用的命令只能设置 radius,并且能限制查询个数 limit,但是无法跳过 from,所以手动截取分页,利用 steam 流 skip(from)
这里要注意哪怕 list 不为空,跳过了 from 个也可能为空,所以需要做判断
@GetMapping("/of/type")
public Result<List<Shop>> 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 Result.success(shopService.queryShopByType(typeId, current, x, y));
}
@Override
public List<Shop> queryShopByType(Integer typeId, Integer current, Double x, Double y) {
if (x == null || y == null) {
int offset = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
List<Shop> shopList = shopMapper.selectByType(typeId, offset, SystemConstants.DEFAULT_PAGE_SIZE);
return shopList;
}
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
String key = RedisConstants.SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> result = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 km WITHDIST
.search(
key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
if (result == null) {
return Collections.emptyList();
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = result.getContent();
if (list.size() <= from) {
return Collections.emptyList();
}
List<Long> ids = new ArrayList<>(end - from);
Map<String, Distance> distanceMap = new HashMap<>(end - from);
list.stream().skip(from).forEach(geoResult -> {
String shopIdStr = geoResult.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
Distance distance = geoResult.getDistance();
distanceMap.put(shopIdStr, distance);
});
List<Shop> shops = shopMapper.selectShopByIds(ids);
shops.forEach(shop -> {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
});
return shops;
}