65.9K
CodeProject 正在变化。 阅读更多。
Home

使用Redis存储和处理地理空间数据

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2022年7月19日

CPOL

6分钟阅读

viewsIcon

12197

学习如何使用Redis及其地理空间索引和命令进行复杂的地理计算

处理地理空间数据臭名昭著的困难,因为纬度和经度是浮点数,需要非常精确。此外,虽然看起来纬度和经度可以表示为网格,但实际上并非如此,仅仅因为地球不是平的,数学也很复杂。

例如,要根据两点在球体上的纬度和经度确定两点之间大圆的距离,可以使用半正矢公式,公式如下

Haversine formula

与纬度和经度相关的另一个常见任务是在地球表面查找半径内的点数。也就是说,给定一个大球体(地球),您正在尝试查找该球体半径内的点。但实际上,地球并非完美的球体,它仍然是一个椭球体。正如您可能猜到的,此类操作的数学计算会相当复杂。

在本文中,我们将探讨 Redis 如何帮助我们最小化处理地理空间数据时的计算量。

Redis,代表远程字典服务器,是一个快速、开源的键值数据存储。由于其速度,Redis 是缓存、会话管理、游戏、分析、地理空间数据等的流行选择。

回到地理空间数据。什么是 Geohash?

Geohash 是一种将坐标表示为字符串的系统。Geohashing 使用 Base32 编码将纬度和经度转换为字符串。例如,圣彼得堡宫殿广场的 geohash 将如下所示:udtscze2chgq。可变 geohash 长度代表可变的位置精度,换句话说,geohash 越短,其表示的坐标精度越低。也就是说,较短的 geohash 将表示相同的地理位置,但精度较低。您可以在 http://geohash.org 上尝试将坐标编码为 geohash。

Redis 如何存储地理空间数据?

Redis 中的地理空间数据存储使用有序集合(ZSET)作为底层数据结构实现,但支持对位置数据进行即时编码和解码以及新的 API。这意味着使用内置命令:GEOADDGEODISTGEORADIUSGEORADIUSBYMEMBER (GEOSEARCH),可以用很少的代码行和最小的努力在 Redis 中实现按特定位置的索引、搜索和排序。

Geo Set 是在 Redis 中处理地理空间数据的基础——它是一种用于管理地理空间索引的数据结构。每个 Geo Set 由一个或多个元素组成,每个元素包含一个唯一的标识符以及一对坐标——经度和纬度。

处理地理空间数据的命令

要将新列表(或向现有列表添加新元素)添加到 Redis 存储中,请使用 GEOADD 命令。为清晰起见,我将提供 Redis 命令的示例,以及用于处理 Redis 的 Ruby 客户端的示例

# Redis example:
GEOADD "buses" -74.00020246342898 40.717855101298305 "Bus A"

# Ruby example:
RedisClient.geoadd("buses", -74.00020246342898, 40.717855101298305, "Bus A")    

这些命令将“buses”Geo Set 中的“Bus A”公交车的位置坐标添加进去。如果 Redis 中尚不存在此名称的 Geo Set,则会创建它。仅当列表中已存在同名(“Bus A”)的条目时,才会向索引添加新条目。也就是说,Bus A 是一个唯一的标识符。

还可以使用单个 GEOADD 调用一次添加多个记录,这有助于减少网络和数据库负载。记录 ID 必须是唯一的

# Redis example:
GEOADD "buses" -74.00020246342898 40.717855101298305 
"Bus A" -73.99472237472686 40.725856700515855 "Bus B"

# Ruby example:
RedisClient.geoadd("buses", -74.00020246342898, 40.717855101298305, "Bus A",
                           -73.99472237472686, 40.725856700515855, "Bus B")

相同的命令也用于更新记录的索引。如果使用已在 Geo Set 中的条目调用 GEOADD,Redis 将仅更新这些条目的数据,一旦 Bus A 开始移动,其位置就可以更新

# Redis example:
GEOADD "buses" -76.99265963484487 38.87275545298483 "Bus A"

# Ruby example:
RedisClient.geoadd("buses", -76.99265963484487, 38.87275545298483, "Bus A")   

除了添加和更新之外,当然也可以从索引中删除条目。提供 ZREM 命令以从 Redis 中的 Geo Set 中删除条目。ZREM 接受要从中删除记录的索引名称以及要删除的记录的 ID

# Redis example:
ZREM buses "Bus A" "Bus B"

# Ruby example:
RedisClient.zrem("buses", "Bis A", "Bus B")

地理索引可以完全删除,由于它存储为 Redis 键,因此可以使用 DEL 命令

# Redis example:
DEL buses

# Ruby example:
RedisClient.del("buses")

但是,对大型列表使用 DEL 可能不是个好主意,因为它可能会长时间阻塞 Redis。因此,最好始终使用 UNLINK 而不是 DEL,即“非阻塞”删除

# Redis example:
UNLINK buses

# Ruby example:
RedisClient.unlink("buses")

请记住,Redis 有一个索引过期机制,如果您不为索引指定过期日期,那么它将永远不会过期并占用内存。为了防止这种情况发生,您需要使用 EXPIRE 命令,传入索引名称和过期秒数

# Redis example:
EXPIRE buses 1000

# Ruby example:
RedisClient.expire("buses", 1000)

Redis 使用半惰性过期机制,这意味着该索引在未读取之前不会过期,如果在读取操作期间发现过期时间已过,则不会返回结果,并且对象本身会从存储中删除。也就是说,直到我们请求 Geo Set,它将无限期地存储在内存中。

但是,Redis 有第二个级别的过期——它是主动的和随机的。它是一个垃圾收集器,会随机读取不同的键,当读取键时,会发生标准的过期检查机制。

不幸的是,Redis 没有直接使索引中的记录过期的能力。此类功能需要独立开发。

如何读取和按地理空间数据搜索?

有几种方法可以从索引中读取条目。您可以从 ZRANGEZSCAN 命令开始。这些命令会迭代索引中的所有条目。例如,要返回索引中的所有条目

# Redis example:
ZRANGE buses 0 -1

# Ruby example:
RedisClient.zrange("buses", 0, -1)

关于地理空间数据,有两个命令可以获取条目在索引中的位置。第一个——GEOPOS 命令返回条目在索引中的坐标

# Redis example:
GEOPOS buses "Bus A"

# Ruby example:
RedisClient.geopos("buses", "Bus A")

第二个命令——GEOHASH 返回以 geohash 编码的条目坐标

# Redis example:
GEOHASH buses "Bus A"

# Ruby example:
RedisClient.geohash("buses", "Bus A")

要获取索引中两个条目之间的距离,可以使用 GEODIST 命令

# Redis example:
GEODIST buses "Bus A" "Bus B"

# Ruby example:
RedisClient.geodist("buses", "Bus A", "Bus B", "km")

该命令的结果默认以米为单位返回。您可以指定所需的测量单位,方法是将第四个参数传递给命令,例如:km 表示千米,m 表示米,mi 表示英里,ft 表示英尺。

为了搜索索引,还使用了 GEORADIUSGEORADIUSBYMEMBER(适用于 6.2 版本之前的 Redis)或 GEOSEARCH(适用于 6.2 版本之前的版本)命令。

GEORADIUSGEORADIUSBYMEMBER 接受参数 WITHDIST(显示结果+到指定点/记录的距离)和 WITHCOORD(显示结果+记录坐标),以及 ASCDESC 排序选项(按与点的距离排序)

# Redis example:
GEORADIUS buses -73 40 200 km WITHDIST

# returns:
1) 1) "Bus A"
  2) "190.4424"
2) 1) "Bus B"
  2) "56.4413"

GEORADIUS buses -73 40 200 km WITHCOORD

# returns:
1) 1) "Bus A"
  2) 1) "-74.00020246342898"
     2) "40.717855101298305"
2) 1) "Bus B"
  2) 1) "-73.99472237472686
     2) "40.725856700515855"

GEORADIUS buses -73 40 200 km WITHDIST WITHCOORD

# returns:
1) 1) "Bus A"
  2) "190.4424"
  3) 1) "-74.00020246342898"
     2) "40.717855101298305"
2) 1) "Bus B"
  2) "56.4413"
  3) 1) "-73.99472237472686
     2) "40.725856700515855"

# Redis example:
GEORADIUSBYMEMBER buses "Bus A" 100 km

# returns:
1) “Bus B”

# Ruby example:
RedisClient.georadiusbymember("buses", "Bus A", 100, "km")

新版本 Redis 的 GEOSEARCH 命令具有类似的语法并执行相同的功能。命令语法如下

# Redis examples:
GEOSEARCH buses FROMMEMBER "Bus A" BYRADIUS 100 km ASC WITHCOORD WITHDIST WITHHASH
# returns all entries in 100km radius from Bus A with coordinates, 
# distances and geohashes

GEOSEARCH buses FROMLONLAT -74.00020246342898 40.717855101298305" 
BYRADIUS 200 mi DESC COUNT 2
# returns maximum 2 entries sorted from the farest to the closest within 
# 200 miles from the center
# with given coordinates 

结论

在 Redis 中实现地理空间数据位置应用的简易性不仅使处理大量地理空间数据变得容易,而且还允许您对数据进行一些复杂的处理。例如,查询半径内的条目可以帮助您实现附近兴趣点的搜索,仅向用户提供离他们最近的选择。如果您的应用程序以任何方式使用地理空间数据,请考虑将复杂计算转移到 Redis,这可能会提高您应用程序的效率。

历史

  • 2022年7月19日:初始版本
© . All rights reserved.