Redis GeoHash 全面指南

GEO百科知识2个月前发布 GEO研究员
2,884 0

Redis GeoHash 提供了一套强大的命令集,如 GEOADDGEORADIUSGEODIST,用于高效地存储、查询和计算地理位置信息。通过将经纬度编码为 GeoHash 值并利用 Redis 的有序集合进行存储,可以实现诸如“附近的人”和“附近的商家”等功能。性能优化方面,Redis 7.0 对 Geo 命令进行了显著改进,同时客户端策略(如九宫格算法)和 Lua 脚本也能进一步提升查询效率。GeoHash 编码的特性使其适用于多种场景,包括社交应用、本地服务搜索和实时定位。

Redis 自 3.2 版本起,引入了对地理位置(Geospatial)数据的支持,通过一系列 Geo 命令,开发者可以方便地在 Redis 中存储、查询和计算地理位置信息。这些功能的核心是 GeoHash 算法,它将二维的经纬度坐标编码成一维的字符串,使得地理位置相关的查询操作更为高效。Redis 的 Geo 模块主要包含六个核心命令:GEOADDGEOPOSGEODISTGEOHASHGEORADIUSGEORADIUSBYMEMBER 。这些命令使得 Redis 能够处理诸如“附近的人”、“附近的店铺”等基于地理位置的功能。在 Redis 内部,地理位置信息是使用有序集合(Sorted Set)来存储的,其中成员(member)是地理位置的标识(如用户ID或地点名称),而分数(score)则是该位置的 GeoHash 编码值 。这种设计巧妙地利用了有序集合的特性,使得范围查询等操作非常高效。

GEOADD 命令是 Redis Geo 模块中最基础也是最重要的命令之一,它用于向指定的 key 中添加一个或多个地理位置信息。每个地理位置信息由经度(longitude)、纬度(latitude)和成员(member)三部分组成。成员是一个唯一的字符串标识,用于代表该地理位置,例如用户ID、店铺名称等。当执行 GEOADD 命令时,Redis 会首先将传入的经纬度转换为一个 52 位的 GeoHash 值,然后将这个 GeoHash 值作为分数(score),将成员作为元素(member),存储到一个有序集合(Sorted Set)中 。如果指定的 key 不存在,Redis 会先创建一个新的有序集合。如果成员已经存在于该有序集合中,那么它的分数(即 GeoHash 值)将会被更新。

命令的基本语法如下:

GEOADD key longitude latitude member [longitude latitude member ...] 

其中 key 是存储地理位置的有序集合的键名。longitudelatitude 分别是地理位置的经度和纬度,它们的范围应符合地理规范(经度:-180° ~ 180°,纬度:-90° ~ 90°)。member 是与该经纬度关联的唯一标识符。GEOADD 命令允许一次添加多个地理位置信息,只需按顺序提供多组 longitude latitude member 参数即可 。

例如,要向名为 Sicily 的 key 中添加两个城市的地理位置信息,可以执行以下命令:

GEOADD Sicily 13.3614 40.6384 "Palermo" 15.0845 37.8287 "Catania" 

这条命令会将 “Palermo” 的坐标 (13.3614, 40.6384) 和 “Catania” 的坐标 (15.0845, 37.8287) 添加到名为 Sicily 的有序集合中 。如果添加成功,GEOADD 命令会返回一个整数,表示成功添加到有序集合中的新成员数量(不包括已存在并更新分数的成员)。例如,如果 Sicily 中原本没有 “Palermo” 和 “Catania”,则上述命令会返回 (integer) 2

在实际应用中,例如社交应用中,当用户登录或更新位置时,可以调用 GEOADD 命令将用户的 ID 和当前经纬度添加到 Redis 中 。例如,用户 user123 的经纬度是 (40.7128, -74.0060),则可以执行 GEOADD users 40.7128 -74.0060 user123 。同样,在本地服务搜索场景中,可以将商家信息(如咖啡店)及其经纬度添加到 Redis,例如 GEOADD coffee_shops 121.4737 31.2304 "Starbucks_001"

需要注意的是,Redis 对经纬度的存储范围有要求,经度必须在 -180 到 180 度之间,纬度必须在 -85.05112878 到 85.05112878 度之间。超出这个范围的值会导致命令执行失败。此外,由于 GeoHash 编码的特性,GEOADD 添加的经纬度信息会存在一定的精度误差,这个误差通常在 0.5% 左右 。对于需要极高精度的场景,可能需要额外的处理。

Redis 提供了两个核心命令用于查询指定半径范围内的地理位置:GEORADIUSGEORADIUSBYMEMBER。这两个命令的功能相似,主要区别在于指定中心点的方式不同。GEORADIUS 命令通过给定的经纬度作为中心点进行查询,而 GEORADIUSBYMEMBER 命令则是通过一个已经存在于有序集合中的成员作为中心点进行查询 。这两个命令在实现“附近的人”或“附近的商家”等功能时非常关键。

GEORADIUS 命令的基本语法如下:

GEORADIUS key longitude latitude radius unit [WITHDIST] [WITHCOORD] [WITHHASH] [ASC|DESC] [COUNT count] [STORE key] [STOREDIST key] 

参数说明:

  • key:存储地理位置的有序集合的键名。
  • longitudelatitude:中心点的经度和纬度。
  • radius:查询半径。
  • unit:半径的单位,可以是 m(米)、km(千米)、mi(英里)或 ft(英尺)。
  • WITHDIST:可选参数,返回结果同时包含成员与中心点的距离。
  • WITHCOORD:可选参数,返回结果同时包含成员的经纬度。
  • WITHHASH:可选参数,返回结果同时包含成员的原始 GeoHash 编码值(52位有符号整数)。这个选项主要用于底层调试,实际应用场景较少 。
  • ASC|DESC:可选参数,指定结果按距离排序,ASC 为从近到远,DESC 为从远到近。
  • COUNT count:可选参数,限制返回结果的数量。
  • STORE key:可选参数,将查询到的地理位置信息存储到另一个指定的 key 中(作为有序集合)。
  • STOREDIST key:可选参数,将查询到的地理位置信息及其与中心点的距离存储到另一个指定的 key 中(作为有序集合,距离作为分数)。

例如,要查询以经纬度 (15, 37) 为中心,半径为 100 公里内的最多 5 个地点,并返回这些地点的距离和坐标,可以使用以下命令:

GEORADIUS Sicily 15 37 100 km WITHDIST WITHCOORD COUNT 5 

这条命令会返回 Sicily 这个 key 中,距离中心点 (15, 37) 100 公里范围内的最多 5 个成员,并且每个成员的信息会包含其名称、距离以及经纬度坐标 。

GEORADIUSBYMEMBER 命令的语法与 GEORADIUS 类似,只是将中心点的经纬度参数替换为一个已有的成员名称:

GEORADIUSBYMEMBER key member radius unit [WITHDIST] [WITHCOORD] [WITHHASH] [ASC|DESC] [COUNT count] [STORE key] [STOREDIST key] 

例如,要查询以 “Palermo” 这个成员为中心,半径为 100 公里内的地点,可以使用:

GEORADIUSBYMEMBER Sicily "Palermo" 100 km WITHDIST WITHCOORD COUNT 5 

这条命令会返回 Sicily 这个 key 中,距离 “Palermo” 100 公里范围内的最多 5 个成员,并包含距离和坐标信息 。

在社交应用中,当用户 A 想要查找附近的人时,可以先获取用户 A 的经纬度,然后使用 GEORADIUS 命令查询。例如,用户 A 的经纬度是 (116.054579, 39.030452),要查找 5 公里内的用户,可以执行 GEORADIUS users:locations 116.054579 39.030452 5 km ASC COUNT 10 。如果用户 A 本身的位置信息已经存储在 Redis 中(例如 key 为 user_A_location),那么也可以使用 GEORADIUSBYMEMBER user_A_location user_A_id 5 km ASC COUNT 10 来实现相同的功能 。

需要注意的是,GEORADIUSGEORADIUSBYMEMBER 命令在 Redis 3.2.10 和 Redis 4.0.0 版本之后,如果使用了 STORESTOREDIST 选项,它们会被标记为写入命令,这意味着在集群环境下,这些命令会被路由到主节点执行,可能会对主节点的负载造成影响。为了解决这个问题,Redis 引入了 GEORADIUS_ROGEORADIUSBYMEMBER_RO 这两个只读命令 。在实际应用中,如果只是查询而不需要存储结果,建议使用这两个只读命令以避免潜在的性能问题。

GEOPOS 命令用于从指定的 key 中获取一个或多个成员的经纬度坐标。这个命令非常直接,当你需要知道某个特定地点(由成员名标识)的确切位置时,可以使用它。

命令的基本语法如下:

GEOPOS key member [member ...] 

参数说明:

  • key:存储地理位置的有序集合的键名。
  • member:要查询经纬度的成员名称。可以指定一个或多个成员。

例如,要获取名为 Sicily 的 key 中 “Palermo” 和 “Catania” 这两个成员的经纬度,可以执行:

GEOPOS Sicily "Palermo" "Catania" 

执行结果会返回一个数组,数组中的每个元素对应一个成员的经纬度。如果成员存在,则返回一个包含两个元素的数组,分别是经度和纬度。如果成员不存在于该 key 中,则对应位置返回 nil 。

例如,执行 GEOPOS diner:location zhangsan lisi 可能会返回如下结果:

1) 1) "121.44661813974380493" 2) "31.20559220971455971" 2) 1) "121.44657522439956665" 2) "31.20485207113603821" 

这表示用户 “zhangsan” 的经纬度是 (121.44661813974380493, 31.20559220971455971),用户 “lisi” 的经纬度是 (121.44657522439956665, 31.20485207113603821) 。

需要注意的是,GEOPOS 命令返回的经纬度坐标是当初通过 GEOADD 命令添加时的值,或者通过其他方式更新后的值。由于 GeoHash 编码和解码过程存在一定的精度损失,GEOPOS 返回的坐标可能与最初添加的坐标有微小的差异,但这个差异通常非常小,对于大多数应用场景来说是可以接受的 。例如,如果最初添加的坐标是 (116.48105, 39.996794),通过 GEOPOS 获取到的可能是 (116.48104995489120483, 39.99679348858259686) 。

这个命令在需要获取特定用户或地点位置信息的场景中非常有用。例如,在社交应用中,当用户查看附近的人列表时,可能需要获取列表中每个用户的详细位置信息以在地图上显示。或者在物流配送场景中,需要获取配送员或商家的精确位置。

GEODIST 命令用于计算指定 key 中两个成员之间的直线距离。这个命令在需要知道两个地理位置之间实际距离的场景下非常有用,例如计算用户与附近商家的距离,或者计算两个配送点之间的距离。

命令的基本语法如下:

GEODIST key member1 member2 [unit] 

参数说明:

  • key:存储地理位置的有序集合的键名。
  • member1member2:需要计算距离的两个成员的名称。
  • unit:可选参数,指定返回距离的单位。可以是 m(米)、km(千米)、mi(英里)或 ft(英尺)。如果未指定单位,默认为米 。

例如,要计算名为 Sicily 的 key 中 “Palermo” 和 “Catania” 两个城市之间的距离,并以千米为单位返回,可以执行:

GEODIST Sicily "Palermo" "Catania" km 

如果两个成员都存在,命令会返回它们之间的距离(浮点数)。如果其中一个或两个成员不存在,或者 key 不存在,则命令返回 nil 。

例如,执行 GEODIST diner:location zhangsan lisi m 可能会返回 "82.4241",表示 “zhangsan” 和 “lisi” 之间的距离是 82.4241 米 。执行 GEODIST diner:location zhangsan lisi km 则会返回 "0.0824",表示距离是 0.0824 千米 。

在社交应用中,当用户查看“附近的人”列表时,通常会显示每个人与当前用户的距离,这个距离就可以通过 GEODIST 命令计算得出。例如,用户 A 想查看与用户 B 的距离,可以执行 GEODIST users:locations userA_id userB_id km。在本地服务搜索中,用户搜索附近的咖啡店,搜索结果中显示的店铺与用户的距离也可以通过此命令获取,例如 GEODIST coffee_shops user_location starbucks_001 km

需要注意的是,GEODIST 命令计算的是两个点之间的直线距离(大圆距离),并没有考虑实际的道路网络或地形因素。因此,在需要精确路径距离的场景下,GEODIST 的结果可能仅作为参考。此外,距离的计算精度也受到 GeoHash 编码精度的影响。

GEOHASH 命令用于获取指定 key 中一个或多个成员的 GeoHash 编码。GeoHash 是一种将二维经纬度坐标编码成一维字符串的方法,这个字符串可以用于表示一个矩形区域。GeoHash 编码的一个特性是,编码值越相似,表示对应的地理位置越接近(在大多数情况下,但需要注意边界条件)。

命令的基本语法如下:

GEOHASH key member [member ...] 

参数说明:

  • key:存储地理位置的有序集合的键名。
  • member:要获取 GeoHash 编码的成员名称。可以指定一个或多个成员。

例如,要获取名为 Sicily 的 key 中 “Palermo” 和 “Catania” 这两个成员的 GeoHash 编码,可以执行:

GEOHASH Sicily "Palermo" "Catania" 

命令会返回一个数组,数组中的每个元素是对应成员的 GeoHash 编码字符串。如果成员不存在,则对应位置返回 nil 。

例如,执行 GEOHASH locations Beijing Shanghai 可能会返回类似于 ["wx4g0b7f", "wtw3sjtv"] 这样的结果 。Redis 返回的 GeoHash 编码是经过 Base32 编码后的字符串,其长度默认为 11 个字符。这个长度对应于 Redis 内部使用的 52 位 GeoHash 整数编码的精度 。

GeoHash 编码在实际应用中有多种用途。首先,它可以作为一种紧凑的表示地理位置的方式,方便存储和传输。其次,由于 GeoHash 编码的前缀匹配特性,可以用于快速筛选大致在同一区域内的地点。例如,如果两个地点的 GeoHash 编码的前几位相同,那么它们很可能在同一个较大的区域内。这个特性可以用于数据分区、缓存优化等场景 。例如,可以将具有相同 GeoHash 前缀(如前 5 位或 6 位)的地点数据存储在一起,或者将针对某个 GeoHash 区域的查询结果缓存起来,因为该区域内的用户查询请求的 GeoHash 编码可能具有相同的前缀。

需要注意的是,Redis 返回的 GeoHash 编码是基于其内部 52 位 GeoHash 值的,这个值的精度是有限的。因此,通过 GeoHash 编码还原出的经纬度坐标与原始坐标之间可能存在一定的误差。此外,GeoHash 编码虽然具有“邻近性”特点,但并非绝对,在边界附近的地点,即使 GeoHash 编码差异较大,实际距离也可能很近,反之亦然。因此,在需要精确判断地理位置关系的场景,不能仅仅依赖 GeoHash 编码的相似性,还需要结合其他计算(如实际距离计算)。

Redis 的 GeoHash 功能虽然强大且易于使用,但在大规模数据和高并发查询的场景下,性能优化仍然是一个重要的考虑因素。了解其性能特点、瓶颈以及可用的优化策略,对于构建高效的地理位置服务至关重要。Redis 的 Geo 命令底层依赖于有序集合(Sorted Set)和 GeoHash 算法,其性能通常很高,但不当的使用或大规模数据集仍可能导致性能问题。

Redis 的地理空间索引功能(GeoHash)虽然强大,但在处理大规模数据和高并发查询时,性能瓶颈依然存在。主要的性能挑战源于其底层实现和计算复杂性。Redis 使用有序集合(Sorted Set)存储地理空间数据,其中成员的分数(score)是经过 GeoHash 编码后的 52 位整数。这种编码方式虽然能够将二维的经纬度转换为一维的字符串,便于范围查询,但在进行距离计算和范围搜索时,仍需要进行大量的解码和数学运算。特别是在执行 GEORADIUSGEOSEARCH 命令时,Redis 需要遍历指定范围内的所有潜在 GeoHash 块,对每个候选位置进行解码,计算其与中心点的实际距离(通常使用 Haversine 公式),然后根据用户指定的参数(如距离、数量、排序等)进行过滤和返回。这个过程涉及到大量的 CPU 计算,尤其是在搜索范围较大或数据集内成员密度较高的情况下,性能开销会显著增加。

根据 Redis 官方的性能分析,在 Redis 7.0 之前的版本中,Geo 命令的性能瓶颈主要体现在以下几个方面:首先,冗余的距离计算。在 geohashGetDistanceIfInRectangle 函数中,geohashGetDistance 被调用了三次,其中前两次是为了产生中间结果,例如检查一个点是否超出经度或纬度范围,以避免更耗 CPU 的完整距离计算。然而,这种检查本身也存在计算开销,尤其是在数据量大的情况下,重复的中间计算累积起来会消耗大量 CPU 资源 。其次,不必要的内存分配与释放。在处理大型数据集时,尤其是在许多元素位于搜索范围之外的情况下,Redis 会频繁调用 sdsdupsdsfree 来分配和释放字符串内存,这会导致 CPU 周期的浪费 。再次,复杂的三角函数计算。Haversine 距离公式依赖于大量的三角函数运算(如 sin, cos, asin, sqrt 等),这些运算本身在 CPU 层面就是比较昂贵的操作。在 Redis 的单线程模型中,这些密集的 CPU 计算会阻塞其他命令的执行,从而影响整体的吞吐量和响应延迟 。此外,数据类型转换也是一个潜在的瓶颈,例如在 GEODIST 命令中,将双精度浮点数转换为字符串表示(如使用 snprintf)也会消耗一定的 CPU 时间 。

性能瓶颈还可能出现在以下几个方面:

© 版权声明

相关文章

暂无评论

none
暂无评论...