--- tags: [Redis, 缓存, 数据结构, GEO] create time: 2026-05-24 10:00 --- # Redis GEO 地理位置 ## 概述 GEO 是 Redis 3.2 引入的地理位置数据类型,底层基于 Sorted Set 实现,通过 GeoHash 编码将经纬度转换为可排序的分数值。它让我们能用简洁的命令完成"附近的人/门店"等 LBS(Location-Based Service)场景,无需依赖外部地理数据库。 ## 正文 ### 1. 底层原理 > [!question] GEO 既然是独立类型,为什么说它"没有自己的数据结构"? > 因为 GEO 底层就是 Sorted Set。Redis 只是在 ZSet 之上封装了一层经纬度编解码逻辑,并没有新增底层数据结构。 核心编码流程: ``` 经纬度 (latitude, longitude) ↓ GeoHash 编码 Base32 字符串 ↓ 转换 52-bit 整数 → 存为 ZSet 的 score ↓ member 名称 → 存为 ZSet 的 value ``` GeoHash 的本质是将二维坐标递归二分,交替切分经度和纬度,得到一个二进制串,再编码为 Base32 字符串。切分越细,精度越高(字符串越长)。 > [!tip] 为什么 GeoHash 能支持范围查询? > GeoHash 具有 **前缀匹配特性**:地理上越近的点,其 GeoHash 前缀越相似。将 GeoHash 转为 52-bit 整数后,相邻地理位置的分数也相近,这使得 ZSet 的 `ZRANGEBYSCORE` 天然支持范围查询。 **GeoHash 精度表**: | 字符串长度 | 精度 (km) | 适用场景 | |-----------|----------|---------| | 4 | ~40 | 城市级 | | 5 | ~5 | 区县级 | | 6 | ~0.6 | 街道级 | | 7 | ~0.076 | 建筑级 | | 8 | ~0.019 | 高精度 | Redis GEO 将 GeoHash 编码为 **52-bit 整数**(对应约 10 位字符精度),水平方向误差约 **±1m**,垂直方向约 **±0.6m**,足以满足绝大多数 LBS 场景。 ```mermaid graph TD A["输入经纬度"] --> B["GeoHash 编码"] B --> C["52-bit 整数"] C --> D["存入 ZSet score"] D --> E["范围查询时按 score 排序"] E --> F["解码还原经纬度并计算实际距离"] ``` ### 2. 核心命令 > [!question] GEORADIUS 命令好用,为什么 Redis 6.2 要废弃它? > `GEORADIUS` / `GEORADIUSBYMEMBER` 参数组合繁多,语义复杂。Redis 6.2 引入 `GEOSEARCH` 和 `GEOSEARCHSTORE`,用更清晰的接口统一了圆形和矩形搜索。 #### 常用命令速查 | 命令 | 说明 | |------|------| | `GEOADD key lng lat member` | 添加一个或多个地理位置 | | `GEOPOS key member` | 获取成员的经纬度 | | `GEODIST key m1 m2 [unit]` | 计算两个成员之间的距离 | | `GEOSEARCH key FROMMEMBER/FROMLONLAT ... BYRADIUS/BYBOX ...` | 搜索附近成员 (6.2+) | | `GEOSEARCHSTORE dst src ...` | 搜索结果存入目标 key (6.2+) | | `GEOHASH key member` | 返回成员的 GeoHash 字符串 | **已废弃命令**(6.2+): - `GEORADIUS` — 用 `GEOSEARCH` 替代 - `GEORADIUSBYMEMBER` — 用 `GEOSEARCH ... FROMMEMBER` 替代 #### 基本操作示例 ```bash # 添加门店坐标 GEOADD stores 116.405285 39.904989 "store:1001" GEOADD stores 121.473701 31.230416 "store:1002" # 获取坐标 GEOPOS stores "store:1001" # 计算距离(默认单位:米) GEODIST stores "store:1001" "store:1002" km # 搜索北京某点 5km 内的门店,返回距离,最多 10 个 GEOSEARCH stores FROMLONLAT 116.405285 39.904989 \ BYRADIUS 5 km ASC COUNT 10 WITHDIST ``` ### 3. 附近门店实战 以下是一个完整的 Go 示例,使用 `go-redis/v9` 实现"查找附近门店"功能。 ```go package main import ( "context" "fmt" "log" "github.com/redis/go-redis/v9" ) // Store 门店信息 type Store struct { Name string Dist float64 // 距离,单位:米 } func main() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) defer rdb.Close() // ---- 1. 批量添加门店坐标 ---- locations := []*redis.GeoLocation{ {Name: "store:1001", Longitude: 116.405285, Latitude: 39.904989}, {Name: "store:1002", Longitude: 116.407496, Latitude: 39.908712}, {Name: "store:1003", Longitude: 116.397827, Latitude: 39.906419}, } _, err := rdb.GeoAdd(ctx, "stores", locations...).Result() if err != nil { log.Fatal(err) } // ---- 2. 搜索附近 3km 内的门店 ---- // 按距离升序排列,最多返回 5 个 searchOpts := &redis.GeoSearchLocationQuery{ GeoSearchQuery: redis.GeoSearchQuery{ Longitude: 116.405285, Latitude: 39.904989, Radius: 3, RadiusUnit: "km", Sort: "ASC", Count: 5, }, WithCoord: true, WithDist: true, } results, err := rdb.GeoSearchLocation(ctx, "stores", searchOpts).Result() if err != nil { log.Fatal(err) } // ---- 3. 处理结果 ---- for _, r := range results { fmt.Printf("门店: %s, 距离: %.2f km, 坐标: (%.6f, %.6f)\n", r.Name, r.Dist, r.Longitude, r.Latitude) } } ``` > [!tip] `COUNT` 参数是软限制 > `COUNT 5` 表示最多返回 5 个结果,但 Redis 内部仍会遍历所有候选点再做距离校验。如果业务数据量很大,建议结合分页或缩小搜索半径。 ### 4. 距离计算 `GEODIST` 和 `GEOSEARCH` 的距离计算基于 **Haversine 公式**,该公式通过球面三角学计算地球表面两点之间的大圆距离。 Haversine 公式核心: $$a = \sin^2\left(\frac{\Delta\phi}{2}\right) + \cos\phi_1 \cdot \cos\phi_2 \cdot \sin^2\left(\frac{\Delta\lambda}{2}\right)$$ $$d = 2R \cdot \arcsin(\sqrt{a})$$ 其中 $R \approx 6371$ km(地球半径)。 > [!question] Redis 的距离计算精度如何? > Redis 使用 WGS84 参考椭球体近似为正球体,最大误差约 **0.5%**。对于 LBS 场景(几公里范围),误差通常在几十米以内,完全可接受。 ### 5. 围栏检测 电子围栏(Geofencing)判断某个点是否在指定范围内。对于简单的圆形围栏,可以直接用 `GEODIST`;对于多边形围栏,需要自定义计算。 #### 圆形围栏 ```bash # 判断 member 是否在圆心 1km 范围内 GEOSEARCH fence FROMLONLAT 116.405285 39.904989 \ BYRADIUS 1 km ASC COUNT 1 ``` #### 多边形围栏(射线法) ```go // PointInPolygon 射线法判断点是否在多边形内 func PointInPolygon(lat, lng float64, polygon [][2]float64) bool { inside := false j := len(polygon) - 1 for i := 0; i < len(polygon); i++ { xi, yi := polygon[i][1], polygon[i][0] xj, yj := polygon[j][1], polygon[j][0] if (yi > lat) != (yj > lat) && lng < (xj-xi)*(lat-yi)/(yj-yi)+xi { inside = !inside } j = i } return inside } ``` > [!tip] 围栏方案选择 > - **圆形围栏**:直接使用 GEO 命令,零额外开发成本。 > - **矩形围栏**:使用 `GEOSEARCH ... BYBOX` 指定宽高。 > - **不规则多边形**:先用 GEO 圈定一个粗略范围缩小候选集,再用射线法精确判断,兼顾性能与精度。 ### 6. 附近门店搜索流程 ```mermaid graph TD A["客户端发送请求"] --> B["获取用户经纬度"] B --> C["GEOSEARCH 搜索半径 R"] C --> D{"结果数量 > 0?"} D -->|No| E["扩大半径 R += step"] E --> C D -->|Yes| F["返回门店列表及距离"] F --> G["客户端渲染附近门店"] ``` ### 7. 性能与限制 > [!question] GEO 能存多少数据? > GEO 底层是 ZSet,受 ZSet 最大容量限制:单个 key 最大约 **150GB**(约 $2^{32}$ 个元素),足以存储数十亿个地理位置。 | 限制项 | 说明 | |-------|------| | 单 key 最大元素数 | ~$2^{32}$(约 43 亿) | | 单 key 最大内存 | ~150GB(ZSet 限制) | | GeoHash 精度 | 52-bit,约 0.019m | | 最小搜索半径 | 0.000001(单位取决于指定) | | 经度范围 | -180 ~ 180 | | 纬度范围 | -85.05112878 ~ 85.05112878 | **性能建议**: - 搜索操作时间复杂度为 $O(N + \log M)$,其中 $N$ 为结果集大小,$M$ 为 key 中元素总数。大 key 下应控制搜索半径和 COUNT。 - 如果只需坐标而不需要距离计算,可用 `GEOPOS` 代替 `GEOSEARCH` 的 `WITHDIST`,减少计算开销。 - 对于超高并发场景,考虑将 GEO 数据加载到应用进程内存中做本地计算,减轻 Redis 压力。 ## 关联笔记 - [[hhs/Redis/02-核心数据类型]] - [[hhs/Redis/08-SortedSet精解]] - [[hhs/Redis/README]]