vault backup: 2026-05-24 21:18:14
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
---
|
||||
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 默认使用 **11 位精度**(约 0.019m),足以满足绝大多数 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]]
|
||||
Reference in New Issue
Block a user