Files
cs-note/hhs/Redis/16-GEO.md
T
2026-05-26 00:03:40 +08:00

8.1 KiB
Raw Blame History

tags, create time
tags create time
Redis
缓存
数据结构
GEO
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 场景。

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 压力。

关联笔记