欢迎来到我的博客,代码的世界里,每一行都是一个故事
前言
在数字化世界中,了解“你我之间的距离”不仅仅是一句浪漫的话语,也是现代应用不可或缺的功能之一。无论是社交应用中的“附近的人”,还是物流追踪中的“包裹距离”,对距离的精准计算和周围环境的了解都显得至关重要。本文将带你进入Spring Boot和Redis Geo的强大世界,探索如何轻松实现这些看似复杂的功能。
Redis Geo回顾
Redis Geo是Redis数据库提供的一个功能强大的地理空间位置处理模块。它允许你存储地理位置信息并执行各种地理空间查询。下面是一些关键点:
- 基于经纬度:Redis Geo使用经度和纬度来存储地点的位置。
- Geohash编码:内部通过Geohash实现,这种编码将二维的经纬度转换成一维的字符串,便于快速计算和检索。
- 快速查询:提供了一系列命令用于快速计算两地距离,查找某个范围内的地点等,如
GEODIST
,GEORADIUS
和GEOPOS
。 - 应用场景广泛:从社交应用中的“附近的人”功能,到物流行业的配送路线优化,再到城市规划和旅游推荐等,Redis Geo都有广泛的应用。
redis中GEO的优势
Redis Geo与其他数据库系统(如关系型数据库、NoSQL数据库或专用的地理空间数据库)相比,在处理地理空间数据方面具有多项优势。这些优势主要源于Redis的性能、效率以及与地理空间数据处理紧密结合的特定命令。以下是Redis Geo相比其他数据库的主要优势:
1. 性能
- 快速响应:作为内存数据库,Redis提供极快的读写速度,这对于需要快速响应地理查询的实时应用来说非常重要。
- 高效的地理空间索引:通过Geohash和排序集合,Redis Geo能够快速定位和检索地理空间数据,其性能通常优于传统的数据库系统。
2. 存储效率
- 紧凑的数据结构:Redis利用紧凑的数据结构存储地理信息,相比于传统数据库需要更多的存储空间来维护地理空间索引,Redis在空间上更加高效。
- 内存优化:作为内存数据库,Redis对内存的使用高度优化,即使处理大量的地理空间对象也能保持较低的内存占用。
3. 简化的操作和集成
- 简单直观的地理空间命令:Redis提供了一系列直观的命令用于地理空间数据处理,如
GEOADD
,GEODIST
,GEORADIUS
等,无需复杂的SQL查询或专用的地理空间函数。 - 易于集成:Redis广泛应用于各种环境,与多种编程语言和框架(如Spring Boot)的集成也非常简单。
4. 灵活性和可扩展性
- 水平扩展:Redis提供主从复制、持久化和分片等特性,使得它可以轻松扩展以支持更大的数据集和更高的并发。
- 支持多种数据结构:除了地理空间数据,你还可以利用Redis的其他数据结构(如字符串、列表、集合等)来构建复杂的应用逻辑。
5. 成本效益
- 降低资源需求:由于其高效的性能和存储方式,Redis可以帮助你减少对硬件资源的需求,从而降低成本。
- 开源:Redis是开源软件,你可以免费使用,社区支持也非常丰富。
注意事项
虽然Redis Geo有许多优势,但它也有自己的局限性,例如它不支持像PostGIS那样复杂的地理空间查询和分析功能。此外,作为内存数据库,如果没有适当的持久化策略,可能会有数据丢失的风险。因此,选择合适的数据库还需要根据你的具体需求、资源和预期的使用场景来决定。在一些情况下,将Redis Geo与其他数据库系统结合使用,可能会得到更好的效果。
实战
本实战我们主要是实现周围的人这个功能,也可以理解为滴滴打车中根据距离派车这样的逻辑
maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
controller实现
在实际应用中应该分为三层,业务代码还是要写入到service层中
package fun.bo.controller; import lombok.Data; import org.springframework.data.geo.*; import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author xiaobo */ @RestController @RequestMapping("/api") public class GeoController { // 真实业务场景中不建议将业务代码放到controller层,且下面的实体放到dto或者vo中 @Resource private RedisTemplate<String, String> redisTemplate; private static final String GEO_KEY = "locations"; // 新增坐标点 @PostMapping("/addLocation") public ResponseEntity<?> addLocation(@RequestBody GeoLocation location) { // 增加坐标点 redisTemplate.opsForGeo().add(GEO_KEY, new Point(location.getLongitude(), location.getLatitude()), location.getName()); return ResponseEntity.ok("坐标添加成功"); } // 查询附近的点 @PostMapping("/searchNearby") public ResponseEntity<?> searchNearby(@RequestBody SearchRequest request) { Circle circle = new Circle(new Point(request.getLongitude(), request.getLatitude()), new Distance(request.getRadius(), Metrics.KILOMETERS)); // 获取范围坐标点 GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo() .radius(GEO_KEY, circle, RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().limit(10)); return ResponseEntity.ok(results); } // 辅助类定义 @Data public static class GeoLocation { private String name; private double latitude; private double longitude; // 省略getter和setter方法 } @Data public static class SearchRequest { private double latitude; private double longitude; private double radius; // 省略getter和setter方法 } }
前端页面实现
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>地图坐标交互</title> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/> <style> #map { height: 400px; } .input-group { margin: 10px 0; } </style> </head> <body> <div id="map"></div> <div class="input-group"> <input type="text" id="locationName" placeholder="位置名称"> <input type="text" id="coords" placeholder="点击地图获取坐标"> <button onclick="addLocation()">新增</button> </div> <div class="input-group"> <input type="text" id="searchCoords" placeholder="点击地图设置查询中心"> <input type="number" id="radius" placeholder="半径(km)"> <button onclick="searchNearby()">查询附近的点</button> </div> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script> <script> var map = L.map('map').setView([34.3416, 108.9398], 5); var searchCircle; // 用于引用查询圈 L.tileLayer('http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}', { attribution: 'Map data © <a href="https://www.amap.com/">高德地图</a>' }).addTo(map); map.on('click', function(e) { document.getElementById('coords').value = e.latlng.lat + ", " + e.latlng.lng; document.getElementById('searchCoords').value = e.latlng.lat + ", " + e.latlng.lng; // 模拟反向地理编码获取位置名称 }); function addLocation() { var name = document.getElementById('locationName').value || 'Unnamed Location'; var coords = document.getElementById('coords').value.split(", "); fetch('/api/addLocation', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name: name, latitude: coords[0], longitude: coords[1]}), }) .then(response => response.text()) .then(data => alert(data)) .catch(error => console.error('Error:', error)); } function searchNearby() { var searchCoords = document.getElementById('searchCoords').value.split(", "); var radius = document.getElementById('radius').value; // 绘制查询圈 if (searchCircle) { map.removeLayer(searchCircle); } searchCircle = L.circle([searchCoords[0], searchCoords[1]], { color: 'blue', fillColor: '#f03', fillOpacity: 0.1, radius: radius * 1000 // Leaflet的半径单位是米 }).addTo(map); fetch('/api/searchNearby', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({latitude: searchCoords[0], longitude: searchCoords[1], radius: radius}), }) .then(response => response.json()) .then(data => { // console.log(data) data.content.forEach(result => { var point = result.content.point; var name = result.content.name; var marker = L.marker([point.y, point.x]).addTo(map); marker.bindPopup(name).openPopup(); }); }) .catch(error => console.error('Error:', error)); } </script> </body> </html>
效果图(实现)