8.0 KiB
tags, create time
| tags | 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 默认使用 11 位精度(约 0.019m),足以满足绝大多数 LBS 场景。
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替代
基本操作示例
# 添加门店坐标
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 实现"查找附近门店"功能。
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;对于多边形围栏,需要自定义计算。
圆形围栏
# 判断 member 是否在圆心 1km 范围内
GEOSEARCH fence FROMLONLAT 116.405285 39.904989 \
BYRADIUS 1 km ASC COUNT 1
多边形围栏(射线法)
// 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. 附近门店搜索流程
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 压力。