Skip to content
← 返回Redis列表
数据与原子性

Redis Geo(地理位置) ​

本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。

1. 定位:解决什么问题 ​

Geo 是基于 ZSet 的经纬度索引,适合真实地理位置的附近玩家或商家。普通 ZSet 的 score 由业务指定,Geo 的 score 用于空间编码。游戏 x/y/z 坐标、障碍物寻路需要另外的空间模型,不能当作经纬度直接写入。

基本操作示例 ​

下面的坐标用于演示经纬度,不代表游戏地图坐标:

text
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 对引号、>、*、括号等符号进行解释。每个例子独立使用;换例子前重新执行下面的准备段,否则删除、弹出、过期和修改操作会影响后续结果。准备段仅重置列出的教学键,确认这些键不承载业务数据。

text
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)
版本与复杂度补充

下表保留每条命令的引入版本、兼容状态及复杂度符号含义;复杂度不包含全部网络传输、内存分配和等待成本。

命令引入版本与状态复杂度说明
GEOADD3.2.0每成员 O(log N),N 为目标索引大小。
GEOPOS3.2.0每成员 O(1),K 个成员为 O(K)。
GEODIST3.2.0O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。
GEOHASH3.2.0每成员 O(1),K 个成员为 O(K)。
GEOSEARCH6.2.0官方口径 O(B+log M),B 为边界网格候选数,M 为实际查询形状内成员数。
GEOSEARCHSTORE6.2.0官方口径 O(B+log M),B 为边界网格候选数,M 为实际查询形状内成员数。
GEORADIUS3.2.0;从 6.2.0 起弃用(兼容保留)官方口径 O(B+log N),B 为包围区域候选数,N 为整个索引成员数。
GEORADIUS_RO3.2.10;从 6.2.0 起弃用(兼容保留)官方口径 O(B+log N),B 为包围区域候选数,N 为整个索引成员数。
GEORADIUSBYMEMBER3.2.0;从 6.2.0 起弃用(兼容保留)官方口径 O(B+log N),B 为包围区域候选数,N 为整个索引成员数。
GEORADIUSBYMEMBER_RO3.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 参数。成员名称长度也会影响编码。

编码与内存怎么测 ​

在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:

text
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。

go
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 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。

继续阅读 ​

数据类型总览 · 游戏与通用后端实践 · 原子性基础。

参考:Redis 8.2.10 命令定义、go-redis、集群规格。

继续阅读