Redis Geo(地理位置)
本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。
1. 定位:解决什么问题
Geo 是基于 ZSet 的经纬度索引,适合真实地理位置的附近玩家或商家。普通 ZSet 的 score 由业务指定,Geo 的 score 用于空间编码。游戏 x/y/z 坐标、障碍物寻路需要另外的空间模型,不能当作经纬度直接写入。
基本操作示例
下面的坐标用于演示经纬度,不代表游戏地图坐标:
GEOADD learn:geo:players 121.47 31.23 player-1 121.48 31.23 player-2
GEOSEARCH learn:geo:players FROMMEMBER player-1 BYRADIUS 2 km WITHDIST查询返回半径内成员与距离;通常也会包含查询中心成员自身。经度在前、纬度在后,距离单位要与业务约定一致。
附近玩家与 NPC
真实地理位置场景可以按地区分片,但查询靠近边界时必须覆盖相邻区域,并在应用层合并、去重。频繁移动需更新坐标;离线成员要及时移除,Geo 本身不会因为某个成员长时间不更新就自动过期。
任意二维、三维游戏地图不能直接把 x/y/z 坐标当成经纬度。此类地图更适合按格子、区域或专门的空间索引建模。查询附近目标与寻路属于不同问题,Geo 不提供障碍物和路径规划能力。
2. 命令速查与示例
按用途分组,共 10 项,覆盖 Redis 8.2.10 的本类型命令及子命令。点击命令名查看官方参数。示例在 redis-cli 中逐行输入,每次换例子前重置下面的教学数据。
准备与重置示例数据
在独立实验实例的 redis-cli 交互模式逐行输入,避免 shell 对引号、>、*、括号等符号进行解释。每个例子独立使用;换例子前重新执行下面的准备段,否则删除、弹出、过期和修改操作会影响后续结果。准备段仅重置列出的教学键,确认这些键不承载业务数据。
UNLINK learn:{geo}:a learn:{geo}:out
GEOADD learn:{geo}:a 121.47 31.23 player-1 121.48 31.23 player-2示例的返回说明以刚完成准备段为前提,随机结果与剩余 TTL 不固定。带同一 {...} 的教学键便于在 Cluster 中满足相关多键操作的同槽要求。
位置与范围查询
| 命令与用途 | 示例与结果 | 复杂度 |
|---|---|---|
| GEOADD 写入经纬度 | GEOADD learn:{geo}:a 121.49 31.23 player-3新增成员返回 1,经度在前、纬度在后。 | 每成员 O(log N) |
| GEOPOS 读取成员坐标 | GEOPOS learn:{geo}:a player-1返回编码后的近似经纬度,可能与输入有微小差异。 | 每成员 O(1) |
| GEODIST 计算两成员距离 | GEODIST learn:{geo}:a player-1 player-2 km返回公里距离;不是障碍物路径长度。 | O(1) |
| GEOHASH 读取 Geohash 字符串 | GEOHASH learn:{geo}:a player-1返回地理哈希字符串。 | 每成员 O(1) |
| GEOSEARCH 按圆或矩形查找 | GEOSEARCH learn:{geo}:a FROMMEMBER player-1 BYRADIUS 2 km ASC COUNT 10 WITHDIST返回范围内最近成员和距离,可能包含中心成员自身。 | 官方口径 O(B+log M) |
| GEOSEARCHSTORE 保存地理查询结果 | GEOSEARCHSTORE learn:{geo}:out learn:{geo}:a FROMMEMBER player-1 BYRADIUS 2 km覆盖 out;未指定 STOREDIST 时保留地理编码分数。 | 官方口径 O(B+log M) |
旧版兼容命令
| 命令与用途 | 示例与结果 | 复杂度 |
|---|---|---|
| GEORADIUS 旧式坐标半径查询 | GEORADIUS learn:{geo}:a 121.47 31.23 2 km WITHDIST新代码用 GEOSEARCH;旧命令还支持写入结果选项。 | 官方口径 O(B+log N) |
| GEORADIUS_RO 旧式只读半径查询 | GEORADIUS_RO learn:{geo}:a 121.47 31.23 2 km WITHDIST不支持保存结果,优先用 GEOSEARCH。 | 官方口径 O(B+log N) |
| GEORADIUSBYMEMBER 旧式成员半径查询 | GEORADIUSBYMEMBER learn:{geo}:a player-1 2 km WITHDIST优先用 GEOSEARCH FROMMEMBER。 | 官方口径 O(B+log N) |
| GEORADIUSBYMEMBER_RO 旧式只读成员半径查询 | GEORADIUSBYMEMBER_RO learn:{geo}:a player-1 2 km WITHDIST优先用 GEOSEARCH FROMMEMBER。 | 官方口径 O(B+log N) |
版本与复杂度补充
下表保留每条命令的引入版本、兼容状态及复杂度符号含义;复杂度不包含全部网络传输、内存分配和等待成本。
| 命令 | 引入版本与状态 | 复杂度说明 |
|---|---|---|
| GEOADD | 3.2.0 | 每成员 O(log N),N 为目标索引大小。 |
| GEOPOS | 3.2.0 | 每成员 O(1),K 个成员为 O(K)。 |
| GEODIST | 3.2.0 | O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。 |
| GEOHASH | 3.2.0 | 每成员 O(1),K 个成员为 O(K)。 |
| GEOSEARCH | 6.2.0 | 官方口径 O(B+log M),B 为边界网格候选数,M 为实际查询形状内成员数。 |
| GEOSEARCHSTORE | 6.2.0 | 官方口径 O(B+log M),B 为边界网格候选数,M 为实际查询形状内成员数。 |
| GEORADIUS | 3.2.0;从 6.2.0 起弃用(兼容保留) | 官方口径 O(B+log N),B 为包围区域候选数,N 为整个索引成员数。 |
| GEORADIUS_RO | 3.2.10;从 6.2.0 起弃用(兼容保留) | 官方口径 O(B+log N),B 为包围区域候选数,N 为整个索引成员数。 |
| GEORADIUSBYMEMBER | 3.2.0;从 6.2.0 起弃用(兼容保留) | 官方口径 O(B+log N),B 为包围区域候选数,N 为整个索引成员数。 |
| GEORADIUSBYMEMBER_RO | 3.2.10;从 6.2.0 起弃用(兼容保留) | 官方口径 O(B+log N),B 为包围区域候选数,N 为整个索引成员数。 |
Geo 底层是 ZSet;删除成员使用 ZREM learn:{geo}:a player-1(单成员 O(log N)),统计数量使用 ZCARD learn:{geo}:a(O(1))。这些共用命令完整列在 Sorted Set。
通用键操作见键与过期时间,这里集中列出类型自身的命令。
3. 底层编码与内存
Geo 底层是 ZSet,TYPE 返回 zset;小集合可为 listpack,大集合为 skiplist(跳表加字典)。位置被编码进 score,不能混用 ZINCRBY 等普通积分操作,否则会破坏空间含义。
沿用 zset-max-listpack-entries 128 与 zset-max-listpack-value 64 默认条件;没有独立的 geo-max-listpack 参数。成员名称长度也会影响编码。
编码与内存怎么测
在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:
GEOADD learn:geo:players 121.47 31.23 player-1 121.48 31.23 player-2
OBJECT ENCODING learn:geo:players
MEMORY USAGE learn:geo:players固定键名、元素字节长度、Redis 版本和分配器,分别记录 1、10、100 个元素或不同值长度时的内存,再跨越编码阈值观察。单元素的增量开销可用相邻两次内存差估计,编码转换时不能套用线性平均;String/Bitmap 的横轴应使用字节数或最高位偏移。比较“一键多元素”和“多个独立键”时,把键名、对象、字典项和 TTL 管理开销一起计入。
MEMORY USAGE 默认对嵌套值采样,SAMPLES 0 会遍历所有嵌套值,仅在小型隔离实验中使用。其结果不等同于进程 RSS;大量键还会增加全局字典、分配器碎片和复制缓冲等成本,需结合 INFO memory。没有适用于所有版本的固定“每 key 几字节”。
依据:8.2.10 配置、对应类型实现、MEMORY USAGE。
4. 性能与阻塞风险
查询范围越大、空间密度越高,候选数量越多;不能认为 GEOSEARCH 永远只需要对数时间。设置合理半径和返回数,评估排序成本。ZSCAN 可用于无序维护,但无法直接替代地理范围查询。
常规命令执行主要占用主执行路径,耗时计算会拖慢同实例其他请求;网络 I/O、后台持久化和释放内存有其他线程/进程参与。命令复杂度不包含所有网络、分配、编码转换与客户端成本。
5. 原子性与并发
GEOADD 单次更新坐标原子;“查附近 → 检查在线 → 领取目标”是多步竞态。若状态位于多个键并且业务允许,可在同槽脚本中处理;跨地图服务的分配仍需业务协议。来自旧心跳的位置更新应通过版本或时间校验,防止倒退。
MULTI/EXEC 保证排队命令连续执行,但执行时单条报错不会回滚其他已成功命令;WATCH 是乐观冲突检测,需要有界重试。Lua 也不会自动回滚运行错误前的写入,应先校验参数与类型,再修改状态。
6. 过期、持久化与生命周期
Geo 只有整键 TTL,没有成员 TTL。可配合按心跳时间排序的 ZSet 定期发现离线成员再 ZREM,或按业务区域维护生命周期;坐标和心跳的更新、清理需处理并发。给某个玩家更新整个 Geo 键的 EXPIRE 不等于该玩家单独续期。
大键会增加 RDB/AOF 重写、复制全量同步和恢复时的处理量,fork 写时复制还可能增加峰值内存。单次同步删除聚合对象可能阻塞;UNLINK 先摘除键,把主要回收工作移到后台,但总回收工作和内存压力仍存在,不能替代限长和拆分。
完成教学实验后,只清理自己创建的键,例如 UNLINK learn:geo:players。
7. 集群与扩展
单个地区 Geo 在一个槽。按区域拆分能分散热点,但边界查询需要同时查询邻区再合并距离与去重。坐标、在线时间等多键脚本需要共同 tag。副本延迟会让附近搜索返回旧位置或已离线玩家,业务需容忍或二次校验。
一个键只在一个分片中,hash tag 控制相关键进入同一槽,不会把一个大键切开。副本读取通常是最终一致观察;操作后立即读到本次写入的需求应选主节点或明确的协调策略。
8. Go 客户端与最佳实践
下面函数使用 github.com/redis/go-redis/v9,调用方提供 context.Context 和已配置认证、超时的 *redis.Client。
func GeoExample(ctx context.Context, rdb *redis.Client) ([]string, error) {
if err := rdb.GeoAdd(ctx, "learn:go:nearby", &redis.GeoLocation{
Name: "player-1", Longitude: 121.47, Latitude: 31.23,
}).Err(); err != nil { return nil, err }
return rdb.GeoSearch(ctx, "learn:go:nearby", &redis.GeoSearchQuery{
Member: "player-1", Radius: 2, RadiusUnit: "km", Count: 20,
}).Result()
}go-redis 用 GeoLocation 和 GeoSearchQuery,明确 Longitude 在前、Latitude 在后。服务端经纬度有范围限制;不同坐标系需统一转换。需要距离和坐标时使用 GeoSearchLocation 的对应选项;普通 GeoSearch 返回成员名列表。
普通 Pipeline 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。
继续阅读
继续阅读
- 数据与原子性
- 数据与原子性
- 完整实践