附近商户
要点:
- GEO数据结构的使用
1.1 GEO数据结构

- 命令速查;
| 命令 | 功能描述 | 语法示例 |
|---|---|---|
GEOADD | 添加一个或多个地理位置坐标 | GEOADD city 116.40 39.90 beijing |
GEOPOS | 获取指定成员的经纬度 | GEOPOS city beijing |
GEODIST | 计算两个位置之间的距离 | GEODIST city beijing shanghai km |
GEORADIUS | 以给定的经纬度为中心,寻找半径内的成员 | GEORADIUS city 116.40 39.90 100 km |
GEORADIUSBYMEMBER | 以指定成员为中心,寻找半径内的成员 | GEORADIUSBYMEMBER city beijing 100 km |
GEOHASH | 返回成员的 GeoHash 字符串形式 | GEOHASH city beijing |
GEOSEARCH | (Redis 6.2+) 综合搜索,支持矩形或圆形区域 | GEOSEARCH city FROMMEMBER beijing BYRADIUS 50 km |
- 注意:
- 距离单位:支持
m(米)、km(千米)、mi(英里)和ft(英尺)。 - 底层原理:当你执行
GEOADD时,Redis 实际上是将数据存入了 ZSET。你可以直接用ZREM来删除某个地理位置成员,或者用ZRANGE查看所有成员。 - 坐标限制:
- 有效经度范围:$-180^\circ$ 到 $180^\circ$。
- 有效纬度范围:$-85.05112878^\circ$ 到 $85.05112878^\circ$(靠近极点地区无法直接使用)。
- 性能:GEO 操作的时间复杂度通常为 $O(\log N)$(添加)或 $O(N + K)$(范围搜索,其中 $K$ 是范围内元素的数量),在大数据量下性能非常出色。
- 距离单位:支持
1.2 附近商户搜索
1.2.1 需求
条件查询商户
1.2.2 代码实现
接口

实现思路
将店铺坐标信息导入Redis中,键的设计如下

分页查询时,先查Redis根据位置信息将店铺排序,后面再做分页。
代码实现
导入Redis
java/** * 将店铺经纬度信息加载到Redis * */ @Test void loadShopInfo(){ // 1. 查询店铺信息 List<Shop> list = shopService.query().list(); if (list == null || list.isEmpty()) { return; } // 2. 导入 键值对信息: shop:geo:typeId -> [shopId, x坐标, y坐标] for (Shop shop : list) { stringRedisTemplate.opsForGeo().add(RedisConstants.SHOP_GEO_KEY + shop.getTypeId(), new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX(), shop.getY()))); } }更换依赖,以支持 GeoSearch
xml<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>编写条件查询
javapublic Result quertShopByLocationAndType(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, SystemConstants.DEFAULT_PAGE_SIZE)); return Result.ok(page.getRecords()); } // 2. 计算分页查询参数 int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE; int end = current * SystemConstants.DEFAULT_PAGE_SIZE; // 3. 查询Redis,按照距离排序、分页。 结果:shopId、distance String key = RedisConstants.SHOP_GEO_KEY + typeId; // 0 , end 条,需要手动截取 GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = stringRedisTemplate.opsForGeo().search( key, GeoReference.fromCoordinate(x, y), new Distance(5000), // 5km RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end) ); if (geoResults == null || geoResults.getContent().isEmpty()) { // 3.1 没有结果,返回空列表 return Result.ok(Collections.emptyList()); } List<Long> ids = new ArrayList<>(SystemConstants.DEFAULT_PAGE_SIZE); HashMap<String, Distance> map = new HashMap<>(SystemConstants.DEFAULT_PAGE_SIZE); // 3.2 如果List的条数小于from,就不需要往下走了 if (geoResults.getContent().size() <= from) { // 没有下一页了 return Result.ok(Collections.emptyList()); } geoResults.getContent().stream().skip(from).forEach(result -> { // 4. 解析id String shopIdStr = result.getContent().getName(); ids.add(Long.valueOf(shopIdStr)); // 获取距离 Distance distance = result.getDistance(); map.put(shopIdStr, distance); }); // 5. 根据id查询Shop List<Shop> list = query().in("id", ids).last("ORDER BY FIELD(id," + StrUtil.join(",", ids) + ")").list(); list.forEach(shop -> { shop.setDistance(map.get(shop.getId().toString()).getValue()); }); // 6. 返回 return Result.ok(list); }