8.5 地理位置服务


文档摘要

8.5 地理位置服务 Redis 地理位置服务详解与代码实践 8.5 Redis 地理位置服务 在现代应用开发中,地理位置服务扮演着越来越重要的角色。无论是社交应用中查找附近的朋友,电商平台推荐附近的门店,还是出行服务中定位车辆位置,都离不开高效可靠的地理位置服务。Redis 从 3.2 版本开始,正式引入了地理位置(Geospatial)功能,为开发者提供了强大的地理位置数据存储、查询和计算能力。 Redis 地理位置服务基于 Geo 类型 的数据结构,实际上底层是使用 Sorted Sets 实现的。它利用 Geohash 算法将经纬度坐标转换为一维的字符串,并利用 Sorted Sets 的排序功能,实现了高效的地理位置索引和范围查询。

8.5 地理位置服务

Redis 地理位置服务详解与代码实践

8.5 Redis 地理位置服务

在现代应用开发中,地理位置服务扮演着越来越重要的角色。无论是社交应用中查找附近的朋友,电商平台推荐附近的门店,还是出行服务中定位车辆位置,都离不开高效可靠的地理位置服务。Redis 从 3.2 版本开始,正式引入了地理位置(Geospatial)功能,为开发者提供了强大的地理位置数据存储、查询和计算能力。

Redis 地理位置服务基于 Geo 类型 的数据结构,实际上底层是使用 Sorted Sets 实现的。它利用 Geohash 算法将经纬度坐标转换为一维的字符串,并利用 Sorted Sets 的排序功能,实现了高效的地理位置索引和范围查询。

本文将深入探讨 Redis 地理位置服务的相关概念、命令、应用场景,并通过代码示例详细讲解如何在实际项目中应用 Redis 地理位置服务。

8.5.1 Redis 地理位置服务核心概念

在深入代码实践之前,我们先了解一下 Redis 地理位置服务涉及的核心概念:

  • 经纬度 (Longitude and Latitude): 地球表面上的点的位置坐标系统。经度(Longitude)是东西方向的角度,纬度(Latitude)是南北方向的角度。Redis 地理位置服务使用标准的经纬度坐标系,经度范围为 -180 度到 180 度,纬度范围为 -85.05112878 度到 85.05112878 度。

  • Geohash: 一种地理哈希编码,将二维的经纬度坐标编码成一个短字符串。具有以下特点:

    • 精度可控: Geohash 字符串的长度越长,精度越高。

    • 前缀匹配: 具有相同前缀的 Geohash 编码,地理位置相近。

    • 临近位置编码相似: 地理位置相邻的点,其 Geohash 编码也往往相似。

    Redis 使用 Geohash 算法将经纬度转换为字符串,并利用 Sorted Sets 的排序特性,实现了高效的地理位置索引。

  • 地理位置索引: Redis 使用 Sorted Sets 构建地理位置索引,Sorted Sets 的 member 存储的是地理位置的名称(例如店铺名称、用户 ID),score 存储的是 Geohash 编码后的值。通过 Sorted Sets 的范围查询功能,可以快速查找指定范围内的地理位置。

  • 距离单位: Redis 地理位置服务支持多种距离单位,包括米 (m)、千米 (km)、英尺 (ft)、英里 (mi)。在进行距离计算和范围查询时,需要指定合适的距离单位。

8.5.2 Redis 地理位置服务相关命令详解

Redis 提供了以下命令来操作地理位置数据:

  1. GEOADD key longitude latitude member [longitude latitude member ...]: 将指定的地理空间位置(经度、纬度、名称)添加到指定的 key 中。

    • key: Sorted Set 的 key 名,用于存储地理位置信息。

    • longitude: 经度。

    • latitude: 纬度。

    • member: 地理位置的名称(例如店铺名称、用户 ID)。

    • 返回值: 添加到 Sorted Set 的新成员数量,不包括已存在的成员。

    代码实践 (Redis CLI):

    GEOADD locations 116.3971 39.9075 "天安门" GEOADD locations 121.4737 31.2304 "东方明珠" GEOADD locations 114.0579 22.5431 "深圳湾"

    内容详解:

    • GEOADD 命令用于向指定的 key (locations 在本例中) 添加地理位置信息。

    • 可以一次添加多个地理位置,提高效率。

    • Redis 底层会将经纬度转换为 Geohash 值,并将其作为 score 存储在 Sorted Set 中,member 为指定的地理位置名称。

    • 如果添加的 member 已经存在于 Sorted Set 中,GEOADD 命令会更新其经纬度信息。

  2. GEOPOS key member [member ...]: 从 key 中获取指定 member 的地理空间位置(经度、纬度)。

    • key: Sorted Set 的 key 名。

    • member: 地理位置的名称。

    • 返回值: 一个数组,每个元素都是一个包含经度和纬度的数组。如果 member 不存在,则返回 nil。

    代码实践 (Redis CLI):

    GEOPOS locations "天安门" "东方明珠" "不存在的位置"

    内容详解:

    • GEOPOS 命令用于查询指定 member 的地理位置信息。

    • 可以一次查询多个 member 的位置。

    • 返回值是一个数组,每个元素对应一个 member 的位置信息。

    • 如果 member 存在,则返回包含经度和纬度的数组;如果 member 不存在,则返回 nil。

  3. GEODIST key member1 member2 [unit]: 计算两个 member 之间的地理空间距离。

    • key: Sorted Set 的 key 名。

    • member1: 第一个地理位置的名称。

    • member2: 第二个地理位置的名称。

    • unit: 距离单位,可选值包括:

      • m: 米 (meters)

      • km: 千米 (kilometers)

      • mi: 英里 (miles)

      • ft: 英尺 (feet)

      • 默认单位为米 (m)。

    • 返回值: 两个 member 之间的距离,以指定的单位表示。如果 member 不存在,则返回 nil。

    代码实践 (Redis CLI):

    GEODIST locations "天安门" "东方明珠" km GEODIST locations "天安门" "深圳湾" mi GEODIST locations "天安门" "不存在的位置"

    内容详解:

    • GEODIST 命令用于计算两个地理位置之间的距离。

    • 可以指定不同的距离单位,方便根据实际需求进行计算。

    • Redis 使用球面距离公式 (Great-circle distance) 来计算地球表面两点之间的最短距离。

    • 如果任何一个 member 不存在,则返回 nil。

  4. GEORADIUS key longitude latitude radius [unit] [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]: 以给定的经纬度为中心,查找指定半径范围内的地理位置信息。

    • key: Sorted Set 的 key 名。

    • longitude: 中心位置的经度。

    • latitude: 中心位置的纬度。

    • radius: 半径距离。

    • unit: 半径距离的单位 (m, km, mi, ft)。

    • [WITHCOORD]: 返回结果中包含地理位置的经纬度。

    • [WITHDIST]: 返回结果中包含地理位置与中心位置的距离。

    • [WITHHASH]: 返回结果中包含地理位置的 Geohash 值。

    • [COUNT count]: 限制返回结果的数量,只返回前 count 个最近的位置。

    • [ASC|DESC]: 对返回结果按照距离中心位置的距离进行排序,ASC 为升序(默认),DESC 为降序。

    • [STORE key]: 将返回的地理位置名称存储到指定的 key 中。

    • [STOREDIST key]: 将返回的地理位置名称和距离中心位置的距离存储到指定的 key 中,距离作为 Sorted Set 的 score。

    • 返回值: 一个数组,包含指定半径范围内的地理位置名称,以及可选的附加信息 (坐标、距离、Geohash)。

    代码实践 (Redis CLI):

    GEORADIUS locations 116.4074 39.9042 10 km WITHCOORD WITHDIST COUNT 2 ASC GEORADIUS locations 116.4074 39.9042 5 mi WITHHASH STORE nearby_locations

    内容详解:

    • GEORADIUS 命令是 Redis 地理位置服务最核心的命令之一,用于进行圆形范围查询。

    • 可以指定中心位置的经纬度、半径距离和单位。

    • 提供了丰富的选项,可以根据需求返回不同的附加信息,例如坐标、距离、Geohash。

    • COUNT 选项可以限制返回结果的数量,用于分页或只获取最近的几个位置。

    • ASCDESC 选项可以控制结果的排序方式,默认按照距离升序排列。

    • STORESTOREDIST 选项可以将查询结果存储到新的 key 中,方便后续使用。

    • GEORADIUS 命令效率很高,Redis 会利用 Geohash 索引快速筛选出符合条件的地理位置。

  5. GEORADIUSBYMEMBER key member radius [unit] [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]:GEORADIUS 命令类似,但是中心位置不再是经纬度,而是指定的 member 在 key 中的地理位置。

    • key: Sorted Set 的 key 名。

    • member: 中心位置的 member 名称。

    • radius: 半径距离。

    • unit: 半径距离的单位 (m, km, mi, ft)。

    • 其他选项:GEORADIUS 命令相同。

    • 返回值:GEORADIUS 命令相同。

    代码实践 (Redis CLI):

    GEORADIUSBYMEMBER locations "天安门" 5 km WITHDIST DESC COUNT 3 GEORADIUSBYMEMBER locations "东方明珠" 10 mi STOREDIST shanghai_nearby_dist

    内容详解:

    • GEORADIUSBYMEMBER 命令与 GEORADIUS 命令功能类似,区别在于中心位置的指定方式。

    • GEORADIUSBYMEMBER 命令以已存在的地理位置 member 作为中心点进行范围查询,更加方便在已知位置的基础上查找附近的位置。

    • 其他参数和选项与 GEORADIUS 命令完全一致。

  6. GEOHASH key member [member ...]: 获取指定 member 的 Geohash 值。

    • key: Sorted Set 的 key 名。

    • member: 地理位置的名称。

    • 返回值: 一个数组,包含每个 member 的 Geohash 值。如果 member 不存在,则返回 nil。

    代码实践 (Redis CLI):

    GEOHASH locations "天安门" "东方明珠" "不存在的位置"

    内容详解:

    • GEOHASH 命令用于获取指定 member 的 Geohash 编码值。

    • 可以一次获取多个 member 的 Geohash 值。

    • 返回值是一个数组,每个元素对应一个 member 的 Geohash 值。

    • Geohash 值可以用于地理位置索引、范围查询优化等场景。

8.5.3 Redis 地理位置服务代码实践 (Python + redis-py)

下面我们通过 Python 结合 redis-py 客户端,演示 Redis 地理位置服务的实际应用。

1. 环境准备:

确保已安装 Redis 服务和 redis-py Python 客户端。

pip install redis

2. Python 代码示例:

import redis # 连接 Redis r = redis.Redis(host='localhost', port=6379, db=0) # 清空测试数据 r.delete('restaurants') # 添加餐厅地理位置信息 restaurants = { "麦当劳": (116.4042, 39.9149), "肯德基": (116.4074, 39.9042), "必胜客": (116.3915, 39.9172), "星巴克": (116.4123, 39.9085), "海底捞": (116.4207, 39.9104), } for name, (longitude, latitude) in restaurants.items(): r.geoadd('restaurants', longitude, latitude, name) print(f"添加餐厅: {name},经度: {longitude},纬度: {latitude}") print("\n----- 查询餐厅地理位置 -----") positions = r.geopos('restaurants', "麦当劳", "肯德基") for i, pos in enumerate(positions): if pos: print(f"{list(restaurants.keys())[i]} 的位置: 经度={pos[0].decode('utf-8')}, 纬度={pos[1].decode('utf-8')}") else: print(f"{list(restaurants.keys())[i]} 不存在") print("\n----- 计算餐厅距离 -----") distance_km = r.geodist('restaurants', "麦当劳", "肯德基", unit='km') distance_m = r.geodist('restaurants', "麦当劳", "肯德基", unit='m') print(f"麦当劳 和 肯德基 的距离: {distance_km.decode('utf-8')} km, {distance_m.decode('utf-8')} m") print("\n----- 查找附近餐厅 (半径 2km) -----") nearby_restaurants = r.georadius('restaurants', 116.405, 39.905, 2, unit='km', withcoord=True, withdist=True, sort='asc') print(f"中心位置 (116.405, 39.905) 附近 2km 的餐厅:") for restaurant_info in nearby_restaurants: name = restaurant_info[0].decode('utf-8') distance = restaurant_info[1].decode('utf-8') coord = restaurant_info[2] print(f"餐厅: {name}, 距离: {distance} km, 坐标: {coord}") print("\n----- 查找附近餐厅 (基于 肯德基,半径 1km) -----") nearby_restaurants_by_member = r.georadiusbymember('restaurants', "肯德基", 1, unit='km', count=2, sort='desc') print(f"肯德基 附近 1km 的餐厅 (最多 2 个,距离降序):") for restaurant_name in nearby_restaurants_by_member: print(f"餐厅: {restaurant_name.decode('utf-8')}") print("\n----- 获取餐厅 Geohash 值 -----") geohashes = r.geohash('restaurants', "麦当劳", "肯德基") for i, geohash in enumerate(geohashes): if geohash: print(f"{list(restaurants.keys())[i]} 的 Geohash: {geohash.decode('utf-8')}") else: print(f"{list(restaurants.keys())[i]} 不存在") print("\n----- 将附近餐厅存储到新的 key -----") r.georadius('restaurants', 116.405, 39.905, 2, unit='km', store='nearby_restaurants_key') nearby_keys_restaurants = r.zrange('nearby_restaurants_key', 0, -1) print("存储到 'nearby_restaurants_key' 的餐厅:") for name in nearby_keys_restaurants: print(f"餐厅: {name.decode('utf-8')}") r.georadius('restaurants', 116.405, 39.905, 2, unit='km', storedist='nearby_restaurants_dist_key') nearby_dist_restaurants = r.zrange('nearby_restaurants_dist_key', 0, -1, withscores=True) print("存储到 'nearby_restaurants_dist_key' 的餐厅 (包含距离):") for name, dist in nearby_dist_restaurants: print(f"餐厅: {name.decode('utf-8')}, 距离: {dist}")

3. 代码运行结果 (示例):

添加餐厅: 麦当劳,经度: 116.4042,纬度: 39.9149 添加餐厅: 肯德基,经度: 116.4074,纬度: 39.9042 添加餐厅: 必胜客,经度: 116.3915,纬度: 39.9172 添加餐厅: 星巴克,经度: 116.4123,纬度: 39.9085 添加餐厅: 海底捞,经度: 116.4207,纬度: 39.9104 ----- 查询餐厅地理位置 ----- 麦当劳 的位置: 经度=116.4041998386383, 纬度=39.91490008094771 肯德基 的位置: 经度=116.40739989280701, 纬度=39.90420012359671 ----- 计算餐厅距离 ----- 麦当劳 和 肯德基 的距离: 1.2854 km, 1285.4234 m ----- 查找附近餐厅 (半径 2km) ----- 中心位置 (116.405, 39.905) 附近 2km 的餐厅: 餐厅: 肯德基, 距离: 0.2854 km, 坐标: [b'116.40739989280701', b'39.90420012359671'] 餐厅: 星巴克, 距离: 0.7395 km, 坐标: [b'116.41229963302612', b'39.90849986567537'] 餐厅: 麦当劳, 距离: 1.1721 km, 坐标: [b'116.4041998386383', b'39.91490008094771'] 餐厅: 海底捞, 距离: 1.4703 km, 坐标: [b'116.42069935798645', b'39.91040014637377'] 餐厅: 必胜客, 距离: 1.6816 km, 坐标: [b'116.39149951934814', b'39.91719998904557'] ----- 查找附近餐厅 (基于 肯德基,半径 1km) ----- 肯德基 附近 1km 的餐厅 (最多 2 个,距离降序): 餐厅: 麦当劳 餐厅: 星巴克 ----- 获取餐厅 Geohash 值 ----- 麦当劳 的 Geohash: wx4g0ecg80 肯德基 的 Geohash: wx4g0ec7g0 ----- 将附近餐厅存储到新的 key ----- 存储到 'nearby_restaurants_key' 的餐厅: 餐厅: 肯德基 餐厅: 星巴克 餐厅: 麦当劳 餐厅: 海底捞 餐厅: 必胜客 存储到 'nearby_restaurants_dist_key' 的餐厅 (包含距离): 餐厅: 肯德基, 距离: 285.42343258857727 餐厅: 星巴克, 距离: 739.507227897644 餐厅: 麦当劳, 距离: 1172.1248531341553 餐厅: 海底捞, 距离: 1470.2663898468018 餐厅: 必胜客, 距离: 1681.563687324524

4. 代码详解:

  • 代码首先连接 Redis 服务,并清空了名为 restaurants 的 key,以便重新添加测试数据。

  • 使用 GEOADD 命令批量添加了多家餐厅的地理位置信息,包括餐厅名称、经度和纬度。

  • 使用 GEOPOS 命令查询了 "麦当劳" 和 "肯德基" 的地理位置。

  • 使用 GEODIST 命令计算了 "麦当劳" 和 "肯德基" 之间的距离,分别以千米和米为单位输出。

  • 使用 GEORADIUS 命令以 (116.405, 39.905) 为中心,查找半径 2km 内的餐厅,并返回餐厅名称、距离和坐标,并按照距离升序排列。

  • 使用 GEORADIUSBYMEMBER 命令以 "肯德基" 为中心,查找半径 1km 内的餐厅,限制返回数量为 2 个,并按照距离降序排列。

  • 使用 GEOHASH 命令获取了 "麦当劳" 和 "肯德基" 的 Geohash 值。

  • 使用 GEORADIUS 命令的 STORESTOREDIST 选项,将附近餐厅的名称和距离分别存储到新的 key (nearby_restaurants_keynearby_restaurants_dist_key) 中,并使用 ZRANGE 命令查看存储的结果。

8.5.4 Redis 地理位置服务应用场景

Redis 地理位置服务在各种应用场景中都有广泛的应用:

  • LBS 应用 (Location-Based Services): 例如查找附近的朋友、附近的商家、附近的优惠信息等。

  • 外卖/出行服务: 骑手/司机位置定位、附近骑手/司机调度、用户附近门店推荐。

  • 社交应用: 基于地理位置的社交功能,例如附近的人、地理位置签到、地理围栏等。

  • 广告投放: 基于用户地理位置的精准广告投放。

  • 物联网 (IoT): 设备位置监控、地理围栏告警、基于位置的服务触发。

  • 游戏: 基于地理位置的游戏玩法,例如 LBS 游戏、基于地理位置的玩家匹配。

8.5.5 Redis 地理位置服务最佳实践

  • 选择合适的 Geohash 精度: Geohash 精度越高,索引和查询精度也越高,但存储空间也会增加。需要根据实际应用场景选择合适的 Geohash 精度。

  • 合理使用半径查询: 半径查询的性能与半径大小和数据密度有关。半径过大或数据密度过高可能会影响查询性能。可以考虑使用更小的半径或增加过滤条件来优化查询。

  • 利用 COUNT 选项进行分页: 在返回结果数量较多的情况下,可以使用 COUNT 选项限制返回结果数量,并结合分页逻辑进行分批获取。

  • 使用 STORESTOREDIST 选项缓存查询结果: 对于需要频繁查询的附近位置信息,可以使用 STORESTOREDIST 选项将查询结果缓存到新的 key 中,提高查询效率。

  • 监控 Redis 内存使用情况: 地理位置数据会占用一定的内存空间,需要监控 Redis 的内存使用情况,避免内存溢出。

  • 结合其他 Redis 功能: 可以结合 Redis 的其他功能,例如缓存、发布订阅等,构建更强大的地理位置服务应用。

8.5.6 总结

Redis 地理位置服务为开发者提供了简单易用、高性能的地理位置数据存储和查询能力。通过本文的详细介绍和代码实践,相信读者已经对 Redis 地理位置服务有了深入的理解。在实际项目开发中,可以根据具体的应用场景,灵活运用 Redis 地理位置服务的相关命令和特性,构建高效可靠的地理位置服务应用。Redis 的 Geospatial 功能不仅简化了地理位置数据的处理,也极大地提升了相关应用的性能和用户体验。


发布者: 作者: 转发
评论区 (0)
U