GEO 地理定位与附近的人

举报
yd_94907100 发表于 2026/11/07 08:11:08 2026/11/07
【摘要】 基于 GEO 命令实现附近的人,讲清 geohash 精度与分页细节

附近的门店、附近的人、按距离排序的骑手调度,这类"以某点为圆心找周边"的需求,用 GEO 类型几条命令就能实现。GEO 在 Redis 6.2 之后统一到 GEOSEARCH 命令,旧的 GEORADIUS 已废弃。本文以 Redis 7 为基准,从底层存储讲到工程化细节。

一、GEO 的底层就是 ZSet

GEOADD 把经纬度编码成 52 位的 geohash 整数,塞进 ZSet 的 score,member 是业务 ID。所以 GEO 的 TYPE 查出来是 zset,删除用 ZREM,遍历用 ZSCAN,过期用 EXPIRE——并没有 GEODEL 这类专属命令:

127.0.0.1:6379> GEOADD store:geo 116.481028 39.99884 s1001
(integer) 1
127.0.0.1:6379> TYPE store:geo
zset
127.0.0.1:6379> GEOPOS store:geo s1001
1) 1) "116.48102651387453"
   2) "39.998839288084254"

geohash 的思想是把二维坐标按经度、纬度交替二分,得到一维编码串,前缀相同的点地理上相邻。把编码塞进 ZSet 的 score 后,"附近"就变成了 score 范围查询,这正是 GEO 高效的原因。

geohash 的编码位数决定格子大小,这也是理解边界误差的关键:

| geohash 长度 | 格子大致边长 |

| --- | --- |

| 4 | 39 km |

| 5 | 4.9 km |

| 6 | 1.2 km |

| 7 | 153 m |

| 8 | 38 m |

Redis 内部用 52 bit 编码,精度折算到 9 位 geohash 字符串以上,量化误差在米级以内。也正因为底层是 ZSet,GEO 天然继承 ZSet 的能力:ZRANGE 看写入总量、EXPIRE 整体过期、Cluster 按 key 分片,几乎没有额外学习成本。还有一点要提前知道:Cluster 下一个超大城市的 GEO key 仍是单个槽,集群不会自动均衡,超大规模只能靠业务拆 key。注意 GEOPOS 返回的是编码还原后的坐标,与写入值有极小的量化偏差,属正常现象。GEOHASH 命令可以返回成员的 geohash 字符串,方便与第三方地图服务对齐校验;写入口还要在应用层统一做坐标系处理,国内 GCJ-02 与 WGS84 之间存在数百米偏移,坐标系混用是"附近的人查不出附近"这类事故的第一大原因。批量导入门店坐标时用 pipeline 打包 GEOADD,并核对返回的新增数与数据源行数:经纬度非法时整条 GEOADD 报错不执行,导入脚本要按行预校验,否则一个坏坐标会让整批失败。

二、写入与距离计算

写入时经度范围是 [-180, 180],纬度范围是 [-85.05112878, 85.05112878]——这是 Web Mercator 投影的边界,不是 ±90,越界直接报 invalid longitude,latitude pair:

127.0.0.1:6379> GEOADD store:geo 116.5 88.0 s2001
(error) ERR invalid longitude,latitude pair 116.500000 88.000000

GEOADD 对已存在的 member 是覆盖坐标,加 CH 选项让返回值变成"新增 + 修改"的数量,NX/XX 控制是否允许覆盖。GEODIST 计算两成员间距离,默认米,可指定 m/km/ft/mi;任一成员不存在返回 nil:

127.0.0.1:6379> GEODIST store:geo s1001 s1002 km
"1.7070"

距离按球面模型计算,与真实椭球距离存在最高约 0.5% 的偏差,打车费、配送费这类对距离敏感的计费不要直接用 GEODIST,应在应用层用精确公式复算。GEODIST 传给不存在的成员返回 nil 而不是报错,客户端把它当 0 处理会算出错误账单,判空后再用。

三、GEOSEARCH:附近的人

Redis 6.2 起 GEOSEARCH 取代 GEORADIUS 成为标准入口:

# 以坐标为圆心,5 km 内最近的 10 个成员,附带距离,由近到远
GEOSEARCH store:geo FROMLONLAT 116.48 39.99 BYRADIUS 5 km ASC COUNT 10 WITHDIST
# 以已有成员为圆心
GEOSEARCH store:geo FROMMEMBER s1001 BYRADIUS 3 km ASC COUNT 10
# 矩形范围(两个对角坐标围成)
GEOSEARCH store:geo FROMLONLAT 116.4 39.9 BYBOX 20 20 km ASC COUNT 10

选项语义:FROMMEMBER 以某成员为圆心,FROMLONLAT 以坐标为圆心;BYRADIUS 圆形范围,BYBOX 矩形范围;ASC 由近到远、DESC 由远到近;WITHDIST 附带距离,WITHCOORD 附带坐标;COUNT 默认返回严格最近的前 N 个,加 ANY 表示凑够 N 个就提前返回,更快但不保证严格最近。需要把结果写回另一个 key 做二次处理时用 GEOSEARCHSTORE。

一个容易混淆的点:不加 ASC/DESC 时返回顺序没有保证,要"最近的 N 个"必须显式 ASC;WITHDIST 返回的距离以所选单位保留 4 位小数,可以直接用于前端展示"距离您 1.2 km"。COUNT 与 ANY 的取舍按场景:门店列表要严格最近就不加 ANY,附近的人这种展示流加 ANY 换性能更划算。

四、精度、分页与规模

三个工程化要点。精度:geohash 量化本身有几米级误差,圆周边界上的成员可能被判进或判出,严谨场景用 BYBOX 扩一圈,再在应用层按直线距离二次过滤。分页:GEOSEARCH 没有原生 offset,常规做法是 COUNT 取前 N 个,下一页用"排除已返回成员 + COUNT ANY"继续取,或者一次取大 COUNT 由客户端切片。规模:单 key 千万级 member 时 GEOSEARCH 仍是毫秒级,但建议按城市或网格拆 key(如 store:geo:{city}),既降内存又降延迟;用户位置高频更新的场景(附近的人)要控制写入频率,比如 30 秒批量更新一次,而不是每条轨迹点都写。

附近的人的完整落地还要再加几层:按城市拆 key 后,用户跨城要在两个 key 间迁移位置;结果集要过滤拉黑与隐身用户,COUNT 要预留过滤损耗;防刷上,同一用户频繁改坐标会制造写热点,限频加坐标变化阈值(移动小于 50 米不更新)都能显著降写入量。隐私方面,位置属于敏感个人信息,保留时长与授权范围要做合规评审。规模再往上,就该换专业地理位置服务或 Elasticsearch 的 geo_point,Redis 方案的舒适区在单城市千万级成员以下。半径的选择同样影响成本:半径越大,参与计算与排序的成员越多,COUNT 100 加 50 km 半径远贵于 1 km 半径;产品上用"半径分级"——先查小圈,空结果再逐级放宽,既压成本,也让空结果更快返回。

常见坑 / 最佳实践

  1. 纬度合法范围是 ±85.05112878 而非 ±90。 应用层先校验坐标合法性,避免把越界错误抛给用户。
  2. 删除、遍历、过期回到 ZSet 命令。 ZREM、ZSCAN、EXPIRE 才是正解,GEO 没有 GEODEL;修改门店坐标用 GEOADD 直接覆盖。
  3. 大集合上控制 COUNT 并优先加 ANY。 严格最近排序要全量计算,COUNT 太大或省略会导致全集合排序,响应时间不可控。
  4. 距离数字有约 0.5% 的模型误差。 计费、合约距离用应用层精确计算,GEO 只做圈选。
  5. 按城市或网格拆 key。 单一巨型 ZSet 在内存与延迟上都不划算,拆分后还要注意 Cluster 下的槽位分布。

小结

  • GEO 底层是 ZSet:geohash 编码进 score,删除遍历用 ZREM/ZSCAN,没有专属删除命令。
  • 经纬度有合法范围限制,纬度边界是 ±85.05112878;GEODIST 是球面近似,误差最高约 0.5%。
  • GEOSEARCH(6.2+)统一了附近查询:圆心二选一、圆与矩形两种范围、ASC/DESC 与 COUNT/ANY 控制结果。
  • 工程上要处理 geohash 边界误差、无 offset 的分页与大集合拆 key,位置更新频率要限流。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。