georadius查得快靠的是zset跳表的整数区间查找而非geohash前缀索引;单zset超50万成员时内存与cpu同步恶化;应改用6~7位geohash分桶并缓存格网映射。

GEORADIUS查得快,但不是靠Geohash前缀索引
很多人以为 GEORADIUS 是靠 Geohash 字符串前缀(比如 ws10e)做快速过滤的,实际完全不是。Redis 内部把经纬度编码成一个 52 位整数(GeoHashBits),存进 ZSet 的 score 字段;所有范围查询本质是 ZRANGEBYSCORE —— 依赖跳表的 O(log N) 整数区间查找,不涉及任何字符串匹配。
这意味着:坐标跨度越大(比如覆盖全国、跨洲),编码后生成的整数区间就越宽;ZRANGEBYSCORE 扫描的候选点就越多,后续还要逐个算 Haversine 距离过滤,CPU 就卡在浮点计算上。
典型现象:GEORADIUS 平均耗时从 2ms 涨到 20ms+,top -H 显示 redis-server 线程 CPU 占用持续 >90%,INFO commandstats 里 cmdstat_georadius 的 usec_per_call 明显偏高。
单 ZSet 成员超 50 万后,内存与 CPU 同步恶化
原生 GEO 命令把每个点都塞进同一个 ZSet(比如 pois:all),成员数一多,问题就集中爆发:
- 内存:ZSet 底层用跳表 + 哈希表双结构,50 万点下跳表层级加深、指针膨胀,内存占用比纯哈希存储高 3–4 倍
- CPU:每次
GEORADIUS都要遍历跳表区间 → 提取所有 member → 对每个调用haversine计算真实距离 → 过滤掉超距点 - 更糟的是:半径稍大(如 10km),一次查询可能拉出上万候选点,全在主线程里算浮点,直接堵死其他请求
实测:ZSet 成员达 80 万时,GEORADIUS QPS 从 800+ 掉到 120,used_memory_rss 比等量 HSET 存储高 65%。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
为什么不用 GEOADD 存“广范围”,而该拆桶
当业务需要覆盖大区域(如“全国充电桩”“亚太门店”),又要求低延迟响应,硬扛原生 GEO 就是自找瓶颈。这时候必须放弃单 key 思路,改用 Geohash 分桶:
- 精度选 6~7 位最稳:6 位(±0.6km)对应 9 个邻近格网,查 9 个 key 并发 fetch,应用层合并去重
- 写入端用
geohash2.encode(lat, lon, precision=6)算出格网码,存到geo:bucket:ws10e这类 key 里(类型用SET或HASH) - 避免 8 位精度:单点可能落入 25+ 格网,key 数爆炸;也别用 5 位:单桶平均含 2000+ 点,过滤开销反而更大
- 注意:分桶后不能直接用
GEORADIUS,得自己实现距离过滤逻辑(可用 Lua 减少网络往返)
这个切换不是“功能降级”,而是把 CPU 密集型计算从 Redis 主线程卸载到客户端或 Lua,同时让内存分布更均匀、可水平扩展。
缓存格网映射比缓存结果更有效
有人想缓存 GEORADIUS 结果(比如 SETEX nearby:116.48,39.92:3km "u1,u2,u3"),但几乎无效:用户拖地图时经纬度小数点后第 5 位一变,key 就完全不同,缓存命中率趋近于 0。
真正值得缓存的是格网映射关系——比如 “116.48,39.92 → ws10e” 这种固定精度的编码结果。它变化频率极低,且 key 空间有限(全球 6 位 Geohash 只有 ~10 亿个),可以用本地缓存(Caffeine)或小 Redis 实例存,查起来零延迟。
复杂点在于:分桶策略一旦上线,写入路径和查询路径就彻底和原生 GEO 不兼容,升级需灰度+双写验证;另外,格网边界处的点可能被漏查,必须严格实现 9 邻域逻辑,不能只查中心格网。










