GEO 数据结构
-
定义:Geolocation,代表地理坐标
-
功能:存储地理坐标信息,根据经纬度检索数据
-
GeoLocation 类
@Data @RequiredArgsConstructor class GeoLocation<T> { private final T name; private final Point point; }
-
常用命令
命令 功能 GEOADD key longtitude1 latitude1 member1 … 添加地理空间信息 (经度 longtitude / 纬度 latitude / 值 member) GEODIST key member1 member2 [m km GEOHASH key member 转换 member 坐标为hash字符串 GEOPOS key member 查询指定 member 的坐标 GEORADIUS(废弃) 指定圆心 / 半径,找到园内包含的所有 member,按照与圆心之间的距离排序后返回 GEOSEARCH key [FROMMMEMBER member FROMLONLAT longtitude latitude] [BYRADIUS radius BYBOX width height ] [m km GEOSEARCHSTORE 类似GEOSEARCH,不过可以将结果存储到指定的 key
功能点
- 查询商户与当前登录用户的距离
- 获取距离当前登录用户最近的商铺列表
业务方案
Redis 数据预热
:-
MySQL 中的商铺位置数据转存至 Redis 中
-
将同一类型的商户以同一 key 存储到 GEO 集合中
-
数据结构:
Key Value Score prefix : type shopId 地理坐标
-
业务逻辑
请求方式
:GET请求路径
:/shop/of/type请求参数
- typeId:商户类型
- current:当前页码(滚动查询)
- x:longtigude,当前登录用户所在经度
- y:latitude,当前登录用户所在纬度
返回值
:List<Shop>,符合要求的商户信息
代码实现
ApplicationTests.java
@SpringBootTest
class ApplicationTests {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IShopService shopService;
// 预热商铺location数据
@Test
void loadShopData {
// 1. 查询店铺信息
List<Shop> shopList = shopService.list();
// 2. 将店铺分组, 按照typeId分组
Map<Long, List<Shop>> shopTypeMap = shopList.stream().collect( Collectors.groupingBy(Shop::getTypeId) );
// 3. 不同typeId的坐标信息写入Redis的不同key中
for( Map.Entry<Long, List<Shop>> entry : shopTypeMap.entrySet() ) {
// 3.1. 通过typeId生成redisKey
String redisKey = "shop:type:" + entry.getKey();
// 3.2. 将shops转化为坐标集合locations(shopId, longitude, latitude)
List<Shop> shops = entry.getValue();
List<GeoLocation<String>> locations = new ArrayList<>( shops.size() );
for( Shop shop : value ) {
locations.add( new GeoLocation<>( shop.getId().toString(), new Point(shop.getX(), shop.getY()) );
}
// 3.3. 将店铺id及坐标写入redis
stringRedisTemplate.opsForGeo().add(key, locations);
}
}
}
ShopController
@RestController
@RequestMapping("/shop")
public class ShopController {
@Resource
private IShopService shopService;
// 根据类型查询店铺, 用户可以选择是否查询距离信息
@GetMapping("/of/type")
public Result queryShopByType(
@RequestParam("typeId") Integer typeId,
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam("x", require = false) Double x,
@RequestParam("y", require = false) Double y ) {
return shopService.queryShopByType( typeId, current, x, y );
}
}
ShopServiceImpl
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final Integer DEFAULT_PAGE_SIZE = 10;
private static final String SHOP_GEO_KEY = "shop:geo:";
// 通过商铺类型及位置查询商铺列表
@Override
public 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, DEFAULT_PAGE_SIZE);
return Result.ok(page.getRecords());
}
// 2. 计算分页参数
int from = (current - 1) * DEFAULT_PAGE_SIZE;
int end = current * DEFAULT_PAGE_SIZE;
// 3. 查询redis中目标页面的数据
// 3.1. 查询0~end条商铺数据
String redisKey = SHOP_GEO_KEY + typeId;
GeoResults<GeoLocation<String>> shopResults = stringRedisTemplate.opsForGeo()
.search(
redisKey, // 查找typeId类型的shop
GeoReference.fromCoordinate(x, y), // 查找源点为(x,y)
new Distance(5000), // 查询半径为5000 m
GeoSearchCommandArgs().includeDistance()).limit(end) // 查询结果包含距离信息,并且仅获取end条数据
);
// 3.2. 查询店铺结果为空, 直接返回
if(shopResults == null)
return Result.ok(Collections.emptyList());
// 4. 解析shopId
List<GeoResult<GeoLocation<String>>> shopList = shopResults.getContent();
if(list.size() <= from)
return Result.ok(Collections.emptyList());
List<Long> shopIds = new ArrayList<>(shopList.size());
Map<String, Distance> distanceMap = new HashMap<>(shopList.size());
// 4.1 截取from~end数据部分
shopList.stream().skip(from).forEach(result -> {
String shopIdStr = result.getContent().getName();
// 从shopList(GeoResult)获取shopId放到shopIds(Long)中
shopIds.add(Long.valueOf( shopIdStr );
// 从shopList(GeoResult)获取distance放到distanceMap中
distanceMap.put(shopIdStr, result.getDistance());
}
);
// 5. 查询shop信息
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
// 6. 给shop信息添加distance变量
for(Shop shop : shops) {
shop.setDistance( distanceMap.get( shop.getId().toString() ).getValue() );
}
// 6. 返回店铺信息
return Result.ok(shops);
}
}