Redis - 位置检索 Geolocation

GEO 数据结构

  1. 定义:Geolocation,代表地理坐标

  2. 功能:存储地理坐标信息,根据经纬度检索数据

  3. GeoLocation 类

    @Data
    @RequiredArgsConstructor
    class GeoLocation<T> {
    	private final T name;
    	private final Point point;
    }
    
  4. 常用命令

    命令功能
    GEOADD key longtitude1 latitude1 member1 …添加地理空间信息 (经度 longtitude / 纬度 latitude / 值 member)
    GEODIST key member1 member2 [mkm
    GEOHASH key member转换 member 坐标为hash字符串
    GEOPOS key member查询指定 member 的坐标
    GEORADIUS(废弃)指定圆心 / 半径,找到园内包含的所有 member,按照与圆心之间的距离排序后返回
    GEOSEARCH key
    [FROMMMEMBER memberFROMLONLAT longtitude latitude]
    [BYRADIUS radiusBYBOX width height ]
    [mkm
    GEOSEARCHSTORE类似GEOSEARCH,不过可以将结果存储到指定的 key

功能点

  1. 查询商户与当前登录用户的距离
  2. 获取距离当前登录用户最近的商铺列表

业务方案

  1. Redis 数据预热
    1. MySQL 中的商铺位置数据转存至 Redis 中

    2. 将同一类型的商户以同一 key 存储到 GEO 集合中

    3. 数据结构:

      KeyValueScore
      prefix : typeshopId地理坐标

业务逻辑

  1. 请求方式:GET
  2. 请求路径:/shop/of/type
  3. 请求参数
    1. typeId:商户类型
    2. current:当前页码(滚动查询)
    3. x:longtigude,当前登录用户所在经度
    4. y:latitude,当前登录用户所在纬度
  4. 返回值: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);
	}
	
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值