做业务开发时,我们经常遇到这类需求:
✅ 同城筛选附近门店、外卖骑手匹配
✅ 社交APP附近的人、同城动态
✅ 网约车就近派单、地理位置打卡
面对这些地理位置检索场景,无需引入笨重的专业GIS引擎,Redis自带的GEO地理空间索引是一个轻量、高性能的方案。
Redis Geo
Redis Geo 没有创造新结构,完全基于有序集合 Sorted Set(ZSet)实现,底层核心组合:GeoHash 编码 + Sorted Set 索引。
简单来说就是:把二维的经纬度坐标,通过算法压缩成一维整数分值,利用ZSet天然的排序、范围检索能力,实现高效的地理位置查询。
- Key :地理集合名(如
stores:location门店位置集合) - Member:位置唯一标识(店铺ID、用户ID、设备ID)
- Score:经纬度编码后的52位GeoHash整数(排序、检索的核心)
GeoHash 编码
GeoHash的核心价值:将二维经纬度,转化为可排序、可比对的一维字符串。
相近的地理位置,会拥有高度相似的编码前缀,这也是"附近查询"能够高效实现的根本原因。
GeoHash编码流程:
- 区间划分 :针对地球经度范围
[-180, 180]与纬度范围[-90, 90],开展持续的二分操作 - 二进制编码:在每次二分过程中,依据目标点所处区间(左区间记为0,右区间记为1)生成对应的二进制位,且经度与纬度的编码过程交替进行
- Base32编码:将生成的二进制数据流,按每5位为一组转换为Base32字符
核心命令
| 命令 | 作用 |
|---|---|
GEOADD key longitude latitude member [...] |
添加一个或多个地理位置 |
GEOPOS key member [...] |
获取指定成员的经纬度信息 |
GEODIST key member1 member2 [unit] |
计算两个成员之间的直线距离 |
GEOHASH key member [...] |
获取成员对应的GeoHash编码,主要用于调试场景 |
GEOSEARCH key FROMMEMBER member BYRADIUS radius unit [...] |
以已存储的成员为中心,检索指定圆形区域内的位置 |
GEOSEARCH key FROMLONLAT lng lat BYRADIUS radius unit [...] |
以给定的经纬度为中心,检索指定圆形区域内的位置 |
GEOSEARCH key FROMLONLAT lng lat BYBOX width height unit [...] |
检索指定矩形区域内的位置 |
GEORADIUS key FROMLONLAT lng lat BYBOX width height unit [...] |
检索指定矩形区域内的位置,该命令已废弃 |
实战演练:实现「附近店铺」功能
将店铺位置录入Redis
将店铺的经纬度信息添加至Redis数据库
bash
GEOADD storeId:location 116.48 39.95 store:1001
GEOADD storeId:location 116.50 39.96 store:1002
GEOADD storeId:location 116.30 39.90 store:1003
💡 该操作支持批量添加,可一次性录入多个位置信息,能有效提升操作性能。
检索附近的店铺
当用户A位于(116.48, 39.95)时,若需查询5公里范围内的店铺,可执行如下操作:
bash
GEOSEARCH storeId:location \
FROMLONLAT 116.48 39.95 \ # 以指定经纬度为中心
BYRADIUS 5 km \ # 搜索半径5公里
WITHDIST \ # 返回距离中心点的距离
WITHCOORD \ # 返回目标的经纬度
COUNT 20 ASC # 最多20条,由近及远排序
## Redis 6.2 以下旧版本,可使用以下命令
GEORADIUS storeId:location 116.48 39.95 5 km WITHCOORD
Java代码
java
@Service
public class LocationService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String GEO_KEY = "storeId:location";
public void updateLocation(Long storeId, double lng, double lat) {
// 先删旧位置,再添加新位置
redisTemplate.opsForGeo().remove(GEO_KEY, storeId.toString());
redisTemplate.opsForGeo().add(GEO_KEY, new Point(lng, lat), storeId.toString());
}
public GeoResults<RedisGeoCommands.GeoLocation<String>>
findNearby(double lng, double lat, double radius) {
Circle circle = new Circle(new Point(lng, lat),
new Distance(radius, Metrics.KILOMETERS));
return redisTemplate.opsForGeo().radius(GEO_KEY, circle,
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending()
.limit(20));
}
}