什么是GEO?
Spring Boot 项目中可以通过 Spring Data Redis
来使用 Redis GEO 功能,主要通过 RedisTemplate
和 GeoOperations
接口来操作地理位置数据。
@Service
public class GeoService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// 存储地理位置public void addGeoLocation(String key, String member, double longitude, double latitude) {GeoOperations<String, Object> geoOperations = redisTemplate.opsForGeo();geoOperations.add(key, new GeoLocation<>(member, longitude, latitude));}// 获取两个地理位置之间的距离public Double getDistance(String key, String member1, String member2) {GeoOperations<String, Object> geoOperations = redisTemplate.opsForGeo();return geoOperations.distance(key, member1, member2, GeoUnit.KILOMETERS).getValue();}// 查找指定经纬度范围内的地理位置public List<GeoLocation<Object>> getNearbyLocations(String key, double longitude, double latitude, double radius) {GeoOperations<String, Object> geoOperations = redisTemplate.opsForGeo();return geoOperations.radius(key, longitude, latitude, radius, GeoUnit.KILOMETERS);}
}
示例调用:
GeoService geoService = new GeoService();// 添加地理位置
geoService.addGeoLocation("cities", "Beijing", 116.4074, 39.9042);
geoService.addGeoLocation("cities", "Shanghai", 121.4737, 31.2304);// 计算距离
Double distance = geoService.getDistance("cities", "Beijing", "Shanghai");
System.out.println("Distance between Beijing and Shanghai: " + distance + " km");// 查找附近的地点
List<GeoLocation<Object>> nearbyCities = geoService.getNearbyLocations("cities", 116.4074, 39.9042, 100);
for (GeoLocation<Object> city : nearbyCities) {System.out.println(city.getName());
}
业务需求:
对附近的商户进行搜索
首先导入redis依赖:
<dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>2.7.18</version>
</dependency>
<dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId><version>6.4.1.RELEASE</version>
</dependency>
随后按照商户类型做分组,类型相同的商户作为一组,以typeId为key存入同一个GEO集合中:
@SpringBootTest
public class HmDianPingApplicationTests {@Autowiredprivate IShopService shopService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Testpublic void loadShopData(){// 查询店铺信息List<Shop> list = shopService.list();// 把店铺按照typeId分组,typeId一致的放入同一集合中Map<Long,List<Shop>> map = list.stream().collect(Collectors.groupingBy(shop -> shop.getTypeId()));for(Map.Entry<Long,List<Shop>> entry:map.entrySet()){Long typeId = entry.getKey();String key = "shop:geo" + typeId;List<Shop> value = entry.getValue();// GeoLocation的泛型是member的数据类型// GeoLocation的内部是member与point,point是经纬度List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();// 写入Redis GEOADD key 经度 纬度 memberfor(Shop shop:value){// 效率低(有多少个店铺就需要存入多少次)//stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(),shop.getY()), shop.getId().toString());// 使用可迭代的集合直接存入locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(), shop.getY())));}stringRedisTemplate.opsForGeo().add(key, locations);}}
}
根据上面的接口文档,在shopController中写入接口:
@RestController
@RequestMapping("/shop")
public class ShopController {@Resourcepublic IShopService shopService;/*** 根据商铺类型分页查询商铺信息* @param typeId 商铺类型* @param current 页码* @return 商铺列表*/@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);}
}
下面通过 Redis 和数据库的结合实现了商店查询功能。使用 Redis 的地理位置功能来加速根据坐标查询商店的速度并支持分页。首先检查是否提供了坐标,如果没有坐标就直接从数据库查询,如果有坐标则通过 Redis 查询地理位置数据并计算距离,最终返回分页后的商店信息。
@Service // 声明为 Spring 服务类,允许 Spring 自动管理该类的生命周期
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {// 引入 Redis 的 StringRedisTemplate,用于执行 Redis 操作@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {// 如果没有提供坐标 x 和 y,则不需要通过 Redis 进行地理查询,直接通过数据库查询商店if(x == null || y == null){// 构建数据库查询条件:根据商店类型查询,并分页Page<Shop> page = query().eq("type_id", typeId) // 根据商店类型 ID 进行筛选.page(new Page<>(current, 1000)); // 分页查询,每页返回 1000 条记录return Result.ok(page.getRecords()); // 返回查询结果}// 如果提供了坐标,计算分页参数int from = (current - 1) * 1000; // 计算当前页起始记录int end = current * 1000; // 计算当前页结束记录// 查询 Redis,按照距离排序进行分页,结果包括 shopId 和距离String key = "shop:geo:" + typeId; // Redis 中存储商店位置的 key,格式为 shop:geo:typeIdGeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(key, // Redis 中的 Geo 数据的 keyGeoReference.fromCoordinate(x, y), // 从提供的坐标 (x, y) 开始查询new Distance(5000), // 设置搜索半径为 5000 米RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end) // 包括距离信息,并限制查询结果数量);// 如果 Redis 查询结果为空,则直接返回空列表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());}// 使用 stream 流式处理查询结果,跳过前 from 条记录,获取分页结果List<Long> ids = new ArrayList<>(list.size()); // 存储查询结果中商店的 IDMap<String, Distance> distanceMap = new HashMap<>(list.size()); // 存储商店 ID 与其对应的距离list.stream().skip(from).forEach(result -> {// 获取店铺的 ID,GeoLocation.getName() 方法返回的是商店 IDString shopIdStr = result.getContent().getName();ids.add(Long.parseLong(shopIdStr)); // 将商店 ID 转换为 Long 类型并存入 ids 列表// 获取商店距离Distance distance = result.getDistance();distanceMap.put(shopIdStr, distance); // 将商店 ID 和距离存入 distanceMap});// 构建 SQL 查询,查询所有符合条件的商店String idStr = StrUtil.join(",", ids); // 将 ids 列表转化为逗号分隔的字符串List<Shop> shops = query().in("id", ids) // 查询商店 ID 在 ids 列表中的所有商店.last("order by ids " + idStr + ")") // 通过 SQL 排序,根据商店 ID 排序.list(); // 执行查询并返回商店列表// 遍历商店信息,将每个商店的距离装入到 Shop 对象中for (Shop shop : shops) {shop.setDistance(distanceMap.get(shop.getId().toString()).getValue()); // 设置商店的距离}// 返回查询结果return Result.ok(shops); // 返回商店信息}
}
通过将 Redis 的地理位置数据与数据库中的商店信息结合,优化了查询的效率,尤其适合商店类型和位置相关的查询场景。