如何在Redis 中处理地理空间数据

1,585 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情

如果你的应用程序正在处理地理空间数据,可以利用 Redis 其强大并且易于使用的地理空间索引和命令来进行复杂的计算。

众所周知,引用地理空间数据非常困难,因为纬度和经度是浮点数,应该非常精确。另外,看起来经纬度可以用网格表示,但实际上不能,因为地球不是平的,而数学是一门复杂的科学。

例如,要确定球体上两点之间大圆的距离,根据它们的纬度和经度,使用半正弦公式,如下所示:

image.png

与纬度和经度相关的另一个常见任务是查找地球表面半径内的点数。也就是说,给定一个大球,您正试图在这个球的半径内找到点。但事实上,地球并不是一个完美的球体,它仍然是一个椭球体。正如您可能猜到的那样,这种操作的数学计算变得相当复杂。

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

让我们回到地理空间数据,什么是 Geohash?

Geohash 是将坐标表示为字符串的系统。Geohashing 使用 Base32 编码将纬度和经度转换为字符串。例如,圣彼得堡皇宫广场坐标的 geohash 将如下所示:udtscze2chgq。一个可变的geohash长度代表一个可变的位置精度;换句话说,geohash 越短,它所代表的坐标就越不精确。也就是说,较短的 geohash 将代表相同的地理位置,但准确性较低。你可以尝试在 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")    

这些命令将“ BUS A”的位置坐标添加到名为“BUS”的地理集中。如果具有此名称的 Geo Set 尚未存储在 Redis 中,则会创建它。仅当列表中没有具有相同名称的条目时,才会将新条目添加到索引中。也就是说,“ BUS A”是唯一标识符。

也可以通过一次GEOADD调用一次添加多条记录,这有助于减少网络和数据库负载。Record 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 有第二级过期——它是主动且随机的。它是一个垃圾收集器,随机读取不同的key,当key被读取时,就会出现标准的过期检查机制。

不幸的是,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,这可能会提高你应用程序的效率。