Skip to content

第二十四章:地理位置检索(Geo) ​

Q74:Redis 的 Geo 是怎么实现的?底层用的是什么数据结构? ​

答:

Redis 从 3.2 版起提供了 Geo(地理位置)相关命令,用于存储经纬度坐标并按距离检索(如「附近的人」「附近的店」)。它的底层实现其实就是 ZSet(有序集合),Geo 只是在 ZSet 之上套了一层「经纬度 ↔ 分数」的编码转换。

一、底层是 ZSet ​

Geo 类型在 Redis 内部就是一个 ZSet:

  • member = 位置点名称(如用户 id、店铺 id);
  • score = 经纬度经过 Geohash 编码后得到的 52 位整数。
text
普通 ZSet:member → score(自定义分数)
Geo(本质是 ZSet):member → score(Geohash 编码的经纬度)

这也是为什么 Geo 支持 ZSet 的部分命令(如 ZREM 删除位置点),因为它就是 ZSet。

二、Geohash 是什么 ​

Geohash 是一种把「二维的经纬度」编码成「一维字符串/整数」的算法。核心思想是把地图递归地二分划分,越分越细,用一串二进制位表示位置落在哪个小格子里。

text
经纬度 (116.397, 39.908)  ──Geohash──►  "wx4g0b9xr" 或 一串二进制

两个关键特性:

  1. 前缀相近 = 位置相近:Geohash 前缀相同的点,地理位置也接近(可用于范围检索);
  2. 一维表示二维:把经纬度变成单一数值,正好可以当作 ZSet 的 score。

更准确地说,Redis Geo 用的是 Geohash 的一个变体(52 位整数),直接作为 ZSet 的 score 存储。这样就把「地理距离」问题,转化成了「ZSet 上一维数值的排序和范围查询」问题。

三、注意:Geohash 的「边界问题」 ​

Geohash 有一个经典缺陷:两个在地理上很近的点,如果恰好落在相邻的不同格子里,它们的 Geohash 字符串可能完全不同(前缀不相近)。

text
格子 A  │ 格子 B
   甲 ●  │  乙 ●   ← 甲、乙实际很近,但被分到了不同格子
        │

所以 Geo 在「搜附近」时,通常要同时搜相邻的 8 个格子(九宫格),才能避免漏掉边界附近的点。Redis 的 GEORADIUS/GEOSEARCH 内部已经处理了这个问题,使用者一般无需关心。

四、为什么选 ZSet 而不是别的 ​

需求ZSet 提供的能力
按距离排序score(编码后的位置)天然有序,ZRANGEBYSCORE 范围查询
快速范围查询跳表实现 O(log N + M) 范围查询
存储海量位置点ZSet 内存效率高
删除/更新位置ZREM / ZADD 直接复用

五、小结 ​

  • Geo 底层就是 ZSet,Geo 命令是 ZSet 的一层「经纬度编码」封装;
  • 经纬度经 Geohash 52 位整数编码后作为 score 存储;
  • Geohash 有「格子边界」问题,搜附近要覆盖相邻格子(Redis 已处理)。

Q75:怎么用 Redis 实现「附近的人」?GEOSEARCH 的用法和参数是什么? ​

答:

「附近的人」「附近的店」本质是:给定一个坐标 + 半径,找出这个半径内的所有点,并按距离排序。Redis 的 GEOSEARCH 一条命令就能完成。

一、添加位置:GEOADD ​

bash
# GEOADD key 经度 纬度 成员
redis-cli GEOADD users:location 116.397 39.908 user:1001
redis-cli GEOADD users:location 116.410 39.910 user:1002
redis-cli GEOADD users:location 116.420 39.920 user:1003
python
redis_client.geoadd("users:location", (116.397, 39.908, "user:1001"))
redis_client.geoadd("users:location", (116.410, 39.910, "user:1002"))

注意参数顺序是 经度(longitude)在前、纬度(latitude)在后,写反了位置就错了。

二、搜索附近:GEOSEARCH(Redis 6.2+) ​

GEOSEARCH 支持以「中心点坐标」或「已存的某个成员」为中心,配合半径搜索。

bash
# 以 (116.397, 39.908) 为中心,搜索 5 公里内的点,带距离和坐标
redis-cli GEOSEARCH users:location FROMLONLAT 116.397 39.908 BYRADIUS 5 km WITHDIST ASC
python
results = redis_client.geosearch(
    "users:location",
    longitude=116.397, latitude=39.908,
    radius=5, unit="km",
    withdist=True,
    sort="ASC",
)

三、核心参数详解 ​

参数含义说明
FROMLONLAT以「经纬度坐标」为中心后跟 经度 纬度
FROMMEMBER以「已存在的某个成员」为中心后跟成员名,自动用该成员坐标为中心
BYRADIUS按「半径」搜索后跟 半径值 + 单位(m/km/ft/mi)
BYBOX按「矩形」搜索后跟 宽 高 + 单位
WITHDIST返回结果带「距离」距离单位与 radius 一致
WITHCOORD返回结果带「经纬度」便于前端展示
WITHHASH返回结果带「Geohash 值」一般用于调试
ASC / DESC按距离 升序 / 降序 排序默认未指定则不保证顺序
COUNT n只返回前 n 个需要 ANY 配合
COUNT n ANY返回任意 n 个(不一定最近的)性能更好,不必全量排序

四、完整示例 ​

bash
# 以 user:1001 的位置为中心,搜 3 公里内的人,按距离升序,带距离
redis-cli GEOSEARCH users:location FROMMEMBER user:1001 BYRADIUS 3 km WITHDIST ASC

# 返回示例(member、距离、坐标)
1) 1) "user:1001"
   2) "0.0000"
   3) 1) "116.3970"
      2) "39.9080"
2) 1) "user:1002"
   2) "1.1234"
   3) 1) "116.4100"
      2) "39.9100"

五、计算两点距离:GEODIST ​

bash
redis-cli GEODIST users:location user:1001 user:1002 km
# 返回 1.1234(公里)

六、获取坐标 / Geohash ​

bash
redis-cli GEOPOS users:location user:1001     # 经纬度
redis-cli GEOHASH users:location user:1001    # Geohash 字符串

七、实际「附近的人」业务要点 ​

  1. 位置更新频繁:用户移动要更新 GEOADD,注意是覆盖写入;
  2. 隐私 / 分层:一般展示「距离」而非精确坐标,或只返回附近的人 id,再由业务层模糊处理;
  3. 过滤条件:Geo 只能按距离搜,「性别/年龄」等其他条件要在业务层过滤;
  4. 大量用户:附近的人配合 COUNT n 限制返回数量,避免一次返回太多;
  5. 坐标精度:经纬度保留到小数点后 5~6 位即可(约 1 米级精度),太精确反而浪费。

八、小结 ​

  • 用 GEOADD 存位置(经度在前、纬度在后);
  • GEOSEARCH ... BYRADIUS 搜附近,WITHDIST/ASC 带距离排序;
  • GEODIST 算距离、GEOPOS 查坐标、GEOHASH 查编码;
  • 业务层要处理位置更新、隐私、额外过滤条件。

Q76:Geo 和「经纬度距离计算」相比,什么时候用 Geo,什么时候直接算距离更合适? ​

答:

「附近的人」有两种实现思路:用 Redis Geo 做范围检索,或者自己算出所有点的距离再排序。两者适用场景不同,核心区别在于数据量和检索方式。

一、两种方式对比 ​

维度Redis Geo自己计算距离
实现方式GEOSEARCH 直接范围查询取所有点,应用层算 Haversine 距离再排序
数据量适合海量位置点(百万级)适合少量点(几十、几百个)
效率O(log N + M),靠索引快速过滤O(N) 全量计算,N 大时很慢
是否需存 Redis是,位置点存 ZSet否,点数据可以在应用侧(内存/DB)
精度距离是近似(Geohash 网格)可精确计算(Haversine 公式)
扩展检索支持半径、矩形、排序、Count需自己写

二、什么时候用 Geo ​

  • 位置点数量大(几千、几万、百万级),需要快速过滤出「附近」的点;
  • 需要频繁的范围查询(半径、矩形);
  • 位置点需要持久化在 Redis,且愿意为此占内存;
  • 需要按距离排序取 Top N。

典型:附近的人、附近的商家、外卖骑手就近派单、打车就近匹配。

三、什么时候自己算距离更合适 ​

  • 位置点数量很少(如只有几十个门店),直接全量算距离排序更快更简单,不值得引入 Geo;
  • 位置点不在 Redis,是「静态」或「低频变化」的数据,用不着为它建一个 ZSet;
  • 需要高精度距离(Geo 的 Geohash 距离是近似值,虽误差很小,但极致精确要求下可自己算);
  • 只是偶尔一次的「算两个点距离」,用 GEODIST 或直接 Haversine 公式即可。

四、Haversine 公式(自己算距离时用) ​

两个经纬度点之间的距离,用 Haversine 公式计算球面距离:

python
import math

def haversine(lon1: float, lat1: float, lon2: float, lat2: float) -> float:
    R = 6371  # 地球半径(公里)
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi = math.radians(lat2 - lat1)
    dlambda = math.radians(lon2 - lon1)

    a = math.sin(dphi/2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda/2)**2
    return 2 * R * math.asin(math.sqrt(a))

五、选择决策 ​

text
海量点、要范围检索 ──► Redis Geo
少量点、一次性计算 ──► 自己 Haversine 算
只需两点距离 ──► GEODIST 或 Haversine
要持久化 + 频繁附近查询 ──► Redis Geo
点数据在别处、不愿入 Redis ──► 应用层算距离

六、小结 ​

  • 数据量大、频繁范围查询 → Geo;
  • 数据量小、一次性/少量计算 → 自己算 Haversine;
  • Geo 的距离是近似值,但误差极小(一般米级以下),业务通常可忽略;
  • 选型核心是「点的数量级」和「是否需要索引式的高效范围检索」。