附近店铺功能实现(基于Redis GEO)
一、功能简介
结合数据库分页 与 Redis GEO 地理位置检索,实现按店铺类型、用户经纬度查询周边店铺。不传经纬度时走普通数据库分页;传入经纬度时,利用 Redis GEO 按距离由近到远排序查询,提升检索性能。
二、依赖引入
引入 Redis 相关依赖,解决版本冲突,保证 GEO 相关方法正常使用:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
<exclusion>
<artifactId>spring-data-redis</artifactId>
<groupId>org.springframework.data</groupId>
</exclusion>
</exclusions>
</dependency>
<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.9.RELEASE</version>
</dependency>
三、接口与分层代码
1. Controller 接口
接收类型ID、页码、用户经纬度,转发业务请求:
java
@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);
}
2. Service 接口
java
public interface IShopService extends IService<Shop> {
Result queryShopByType(Integer typeId, Integer current, Double x, Double y);
}
3. Service 实现类
java
@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());
}
// 有经纬度:Redis GEO 地理位置查询
int pageSize = SystemConstants.DEFAULT_PAGE_SIZE;
int from = (current - 1) * pageSize;
int end = current * pageSize;
String key = SHOP_GEO_KEY + typeId;
// GEO 检索:5公里范围内店铺,返回距离信息,限制最大条数
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
.search(
key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
.includeDistance()
.limit(end)
);
if (results == null) {
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
if (list.size() <= from) {
return Result.ok(Collections.emptyList());
}
// 内存分页,截取当前页数据
List<Long> ids = new ArrayList<>();
Map<String, Distance> distanceMap = new HashMap<>();
list.stream().skip(from).forEach(result -> {
String shopIdStr = result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
distanceMap.put(shopIdStr, result.getDistance());
});
// 根据ID查库,保留距离排序
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query()
.in("id", ids)
.last("ORDER BY FIELD(id," + idStr + ")")
.list();
// 封装距离字段
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
return Result.ok(shops);
}
四、核心知识点说明
1. MyBatis-Plus 分页对象 Page
-
page.getRecords():获取当前页数据列表 -
page.getTotal():数据总条数 -
page.getPages():总页数 -
page.getCurrent():当前页码
底层数组、数据库索引从0 开始,业务页码从1 开始,分页计算需做
页码-1偏移换算。
2. Redis GEO 核心 API
-
stringRedisTemplate.opsForGeo():专门操作 Redis GEO 地理位置结构。 -
.search():Redis6.2+ 附近地点检索,以用户坐标为圆心、指定半径范围查询;-
.includeDistance():返回两点间距离; -
.limit():限制返回数据条数。
-
-
GeoResults.getContent():剥离包装对象,转为普通集合。
3. GEO 分页方案
Redis GEO 不支持原生偏移分页,采用内存分页:
-
Redis 查询当前页及之前所有数据;
-
通过
Stream.skip()跳过前置数据,截取当前页内容。
4. 距离字段处理
-
Distance.getValue():提取距离数值(double 类型)。 -
实体类非数据库字段定义:
java
@TableField(exist = false)
private Double distance;
五、关键设计要点
-
Redis Key 设计 :
shop:geo:{typeId},按店铺类型拆分 GEO 集合,提升查询效率。 -
排序保证 :使用
ORDER BY FIELD强制沿用 Redis 距离排序,避免数据库打乱顺序。 -
数据一致性:Redis 仅存储店铺ID与经纬度,店铺详情从数据库查询。
六、整体流程
-
未传经纬度:直接使用 MyBatis-Plus 完成数据库分页;
-
传入经纬度:通过 Redis GEO 按距离检索店铺ID与距离;
-
Java 内存分页截取当前页数据,根据ID查询店铺详情;
-
封装距离字段,统一返回前端。