Redis GEO Radius 的实现原理

发布时间: 更新时间: 总字数:3062 阅读时间:7m 作者: IP上海 分享 网址

GEO 是 Redis 在 3.2 版本之后新增的地理位置模块,用来实现查找附近的地点功能。

原理

Redis 的 GEO 命令集,包括 GEORADIUS,是基于 sorted set(有序集合)数据结构实现的。每个地理位置信息都通过以下方式存储和处理:

  1. 经纬度到 52 位整数的转换:Redis 使用 Geohash 算法将经纬度坐标(latitude 和 longitude)编码为一个 52 位的整数。Geohash 是一种将二维坐标数据转换成一维字符串或整数的算法,其关键特性是距离相近的坐标通常具有相似的前缀。

  2. 存储到 Sorted Set:Redis 将这个 52 位整数作为 sorted setscore,而地理位置的成员名(例如,一个地点的名称或 ID)作为 member 存储。sorted set 的天然优势在于,它会根据 score 自动进行排序,这样就可以高效地进行范围查询。

  3. 邻近区域的查找GEORADIUS 命令的核心思想就是利用 Geohash 的特性。当我们需要查找一个中心点周围一定半径范围内的位置时,Redis 会执行以下步骤:

    • 首先,将中心点的经纬度编码成一个 Geohash 52 位整数。
    • 根据查询半径,计算出一个 Geohash 编码的范围。这个框可能不完全是圆形,而是一个方形或矩形。
    • 因为 Geohash 编码越长,精度越高,所以 Redis 会根据半径大小,选择合适的 Geohash 编码长度。
    • 它会找到包含这个中心点 Geohash 的所有 8 个邻近区域(Geohash 方格),并将这些区域的 Geohash 编码的范围也计算出来。
    • 然后,利用 sorted setZRANGEBYSCORE 命令,查询所有这些 Geohash 区域内的所有成员。因为 Geohash 已经编码成了一个 52 位整数的 score,这个操作非常高效。
    • 最后,Redis 会对这些查询到的成员进行精确的距离计算(使用 Haversine 公式或其他球面距离计算方法),以筛选出真正位于指定圆形半径内的点,并返回结果。

redis-cli 相关命令

  • geoadd:添加地理位置的坐标
  • geopos:获取地理位置的坐标
  • geodist:计算两个位置之间的距离
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合
  • geohash:返回一个或多个位置对象的 geohash 值

GEOADD

  • 作用:将一个或多个地理空间位置(经纬度)添加到指定的键(key)中。它实际上是将地理位置数据存储在一个 sorted set 中。
  • 语法GEOADD key longitude latitude member [longitude latitude member ...]
  • 参数解释
    • key:一个键名,用于存储地理位置集合。在示例中,这个键是 "charging-stations"
    • longitude:成员的经度,以十进制表示。
    • latitude:成员的纬度,以十进制表示。
    • member:地理位置的名称或标识符。在示例中,这是充电桩的 ID,例如 "StationA"
  • 示例用途:用于将每个充电桩的经纬度信息添加到 "charging-stations" 这个 sorted set 中。每次调用 GeoAdd 方法时,你可以添加一个或多个充电桩。
> GEOADD charging-stations 116.397128 39.916527 StationA
(integer) 1

GEOPS

GEOPOS 命令用于从指定的键(key)中,获取一个或多个成员(member)的经度和纬度。

  • 语法GEOPOS key member [member ...]
  • 参数解释
    • key:存储地理位置数据的键名,例如 "charging-stations"
    • member:要查询的地理位置的成员名,例如 "StationA"
  • 作用:它会返回每个成员的经度和纬度坐标。
> GEOPOS charging-stations StationA
1) 1) "116.39712799986004829"
   2) "39.9165270007873528"

ZRANGE

ZRANGE 命令用于返回有序集合中,指定索引范围内的成员。

  • 语法: ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
  • 参数解释:
    • key: 存储地理位置数据的键名,例如 "charging-stations"
    • startstop: 索引的起始和结束位置。0 表示第一个成员,-1 表示最后一个成员。
  • 作用: 它会返回有序集合中指定范围内的成员名称。

示例:

> GEOADD charging-stations 116.397128 39.916527 StationA 116.452655 39.923055 StationB 121.473701 31.230416 StationC
(integer) 3
> ZRANGE charging-stations 0 -1
1) "StationA"
2) "StationB"
3) "StationC"

这个命令会返回 charging-stations 键中所有的成员名称,按照它们在 sorted set 中的自然顺序排列。

GEORADIUS

  • 作用:以指定的经纬度为中心,查找一个给定半径范围内的所有成员。这个命令是实现附近查找功能的核心。
  • 语法GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]
  • 参数解释
    • key:存储地理位置数据的键名,同样是 "charging-stations"
    • longitudelatitude:中心点的经纬度,即用户所在的位置。
    • radius:查找的半径大小,一个浮点数。
    • unit:半径的单位,可以是 m (米)、km (公里)、mi (英里) 或 ft (英尺)。在示例中,我们使用了 km
    • WITHCOORD:可选参数,如果指定,返回结果中会包含每个匹配到的成员的经纬度。
    • WITHDIST:可选参数,如果指定,返回结果中会包含每个匹配到的成员到中心点的距离。
    • COUNT:可选参数,用于限制返回结果的数量。
    • ASC|DESC:可选参数,用于对结果进行排序。ASC 表示按距离升序排列(从近到远),DESC 表示降序排列(从远到近)。
  • 示例用途:用于查找用户当前位置(例如,天安门广场)附近 5 公里半径内的所有充电桩,并返回每个充电桩的名称、与中心点的距离以及其自身的经纬度。

实现:查找附近的充电桩场景

go-redis 是一个流行的 Redis 客户端库,它提供了对所有 Redis GEO 命令的完整支持。下面是一个使用 go-redis 实现 GEORADIUS 的典型场景:查找附近的服务或设施

假设你正在开发一个电动车充电桩应用,用户需要查找自己附近 5 公里内的所有充电桩。

  • 首先,需要向 Redis 中添加一些充电桩的位置信息。通过 GEOADD 命令来完成这个操作。
  • 现在,假设用户当前的位置是天安门广场附近(经纬度:116.397128, 39.916527),需要查找 5 公里内的所有充电桩。

go-redis 提供了 GeoRadius 方法来调用 GEORADIUS 命令。

// docker run --name my-redis -p 6379:6379 -v /path/to/redis-data:/data -d redis redis-server --appendonly yes
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/redis/go-redis/v9"
)

func main() {
	ctx := context.Background()

	// 建立 Redis 连接
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})

	// 添加一些充电桩的位置信息
	// key: "charging-stations"
	// member: 充电桩ID
	// longitude, latitude: 经纬度
	_, err := rdb.GeoAdd(ctx, "charging-stations",
		&redis.GeoLocation{Name: "StationA", Longitude: 116.397128, Latitude: 39.916527}, // 北京天安门附近
		&redis.GeoLocation{Name: "StationB", Longitude: 116.452655, Latitude: 39.923055}, // 北京东单附近
		&redis.GeoLocation{Name: "StationC", Longitude: 121.473701, Latitude: 31.230416}, // 上海外滩附近
	).Result()
	if err != nil {
		log.Fatalf("Failed to add geo locations: %v", err)
	}
	fmt.Println("Added charging stations.")

	// 使用 ZRange 命令获取所有成员名称
	// ZRange 的参数是 start=0, stop=-1,表示获取所有成员
	stationNames, err := rdb.ZRange(ctx, "charging-stations", 0, -1).Result()
	if err != nil {
		log.Fatalf("Failed to get station names: %v", err)
	}

	fmt.Println("List of all charging station names:")
	for _, name := range stationNames {
		fmt.Printf("- %s\n", name)
	}

	// 获取 "StationA" 和 "StationB" 的经纬度
	// GeoPos 方法接受一个可变参数,可以查询多个成员
	locations, err := rdb.GeoPos(ctx, "charging-stations", "StationA", "StationB").Result()
	if err != nil {
		log.Fatalf("Failed to get geo positions: %v", err)
	}

	fmt.Println("Geo Positions:")
	// locations 是一个 GeoPos 结构体切片,每个元素对应一个查询的成员
	for i, loc := range locations {
		if loc != nil {
			fmt.Printf("Location %d: Longitude: %.6f, Latitude: %.6f\n", i+1, loc.Longitude, loc.Latitude)
		} else {
			fmt.Printf("Location %d: Not found\n", i+1)
		}
	}

	// 下面是查找的代码
	// 假设用户当前位置
	userLongitude := 116.397128
	userLatitude := 39.916527
	radius := 5.0 // 查找半径,单位公里

	// 使用 GeoRadius 命令查找
	results, err := rdb.GeoRadius(ctx, "charging-stations", userLongitude, userLatitude, &redis.GeoRadiusQuery{
		Radius:    radius,
		Unit:      "km",  // 单位为公里
		WithCoord: true,  // 返回坐标
		WithDist:  true,  // 返回距离
		Count:     10,    // 最多返回10个
		Sort:      "ASC", // 按距离升序排列
	}).Result()

	if err != nil {
		log.Fatalf("Failed to find charging stations: %v", err)
	}

	fmt.Printf("Found %d charging stations within %.1f km of user location:\n", len(results), radius)
	for _, res := range results {
		fmt.Printf(" - Station: %s, Distance: %.2f km, Longitude: %.6f, Latitude: %.6f\n",
			res.Name, res.Dist, res.Longitude, res.Latitude)
	}
}

代码解释:

  • rdb.GeoRadius(ctx, "charging-stations", userLongitude, userLatitude, ...):这是核心调用。第一个参数是 Redis key,接下来的两个是中心点的经纬度。
  • redis.GeoRadiusQuery:这是一个查询选项结构体,可以用来配置 GEORADIUS 命令的各种参数。
    • RadiusUnit:指定了查询的半径和单位(米、公里、英里等)。
    • WithCoord:如果设置为 true,返回结果中会包含每个匹配到的地理位置的经纬度。
    • WithDist:如果设置为 true,返回结果中会包含每个位置到中心点的距离。
    • CountSort:用于限制返回结果的数量和排序方式。

运行结果:

对于上面的例子,因为 “StationA” 和 “StationB” 都在北京,距离很近,而 “StationC” 在上海,距离很远,所以运行结果:

go run main.go
Added charging stations.
List of all charging station names:
- StationC
- StationA
- StationB
Geo Positions:
Location 1: Longitude: 116.397129, Latitude: 39.916526
Location 2: Longitude: 116.452656, Latitude: 39.923056
Found 2 charging stations within 5.0 km of user location:
 - Station: StationA, Distance: 0.00 km, Longitude: 116.397129, Latitude: 39.916526
 - Station: StationB, Distance: 4.79 km, Longitude: 116.452656, Latitude: 39.923056

这个例子清晰地展示了如何利用 go-redis 快速实现一个基于地理位置的附近查找功能,其核心是 GEORADIUS 命令及其高效的 Geohash 算法。

参考

  1. https://en.wikipedia.org/wiki/Geohash
本文总阅读量 次 本站总访问量 次 本站总访客数
Home Archives Categories Tags Statistics