众所周知,使用地理空间数据非常困难,因为纬度和经度是浮点数,应该非常精确。另外,看起来经纬度可以用网格表示,但实际上不能,因为地球不是平的,数学很难。
例如,要确定球体上两点之间大圆的距离,根据它们的纬度和经度,使用半正弦公式,如下所示:
与纬度和经度相关的另一个常见任务是查找地球表面半径内的点数。也就是说,给定一个大球(地球),您正试图在这个球的半径内找到点。但事实上,地球并不是一个完美的球体,它仍然是一个椭球体。正如您可能猜到的那样,这种操作的数学计算变得相当复杂。
在本文中,我们将了解 Redis 如何帮助我们在处理地理空间数据时最小化计算。
Redis 代表远程字典服务器,是一种快速、开源的键值对数据存储。由于其速度,Redis 是缓存、会话管理、游戏、分析、地理空间数据等的热门选择。
让我们回到地理空间数据。什么是 Geohash?
Geohash 是将坐标表示为字符串的系统。Geohashing 使用 Base32 编码将纬度和经度转换为字符串。例如,圣彼得堡皇宫广场的 geohash 将如下所示:udtscze2chgq。一个可变的 geohash 长度代表一个可变的位置精度,换句话说,geohash 越短,它所代表的坐标越不精确。也就是说,较短的 geohash 将代表相同的地理位置,但准确性较低。您可以在http://geohash.org尝试在 geohash 中编码坐标。
Redis 如何存储地理空间数据?
地理空间数据存储在 Redis 中使用排序列表 (ZSET) 作为底层数据结构来实现,但具有位置数据的动态编码和解码以及新的 API。这意味着使用内置命令:GEOADD
、GEODIST
和。GEORADIUS
GEORADIUSBYMEMBER (GEOSEARCH)
Geo Set 是在 Redis 中处理地理空间数据的基础——它是一种旨在管理地理空间索引的数据结构。每个 Geo Set 由一个或多个元素组成,每个元素都包含一个唯一标识符和一对坐标 - 经度和纬度。
用于处理地理空间数据的命令
要在 Redis 存储中添加新列表(或向现有列表中添加新元素),请使用该GEOADD
命令。为了清楚起见,我将给出 Redis 中的命令示例,以及使用 Redis 的 Ruby 客户端中的命令示例:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
GEOADD "buses" -74.00020246342898 40.717855101298305 "Bus A"
# Ruby example:
RedisClient.geoadd("buses", -74.00020246342898, 40.717855101298305, "Bus A") </span></span></span></span>
这些命令将公共汽车“公共汽车 A”的位置坐标添加到名为“公共汽车”的地理集中。如果具有此名称的 Geo Set 尚未存储在 Redis 中,则会创建它。仅当列表中没有具有相同名称(“总线 A”)的条目时,才会将新条目添加到索引中。也就是说,总线 A 是唯一标识符。
还可以通过一次GEOADD
调用一次添加多条记录,这有助于减少网络和数据库负载。记录 ID 必须是唯一的:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># 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")</span></span></span></span>
相同的命令用于更新记录的索引。如果GEOADD
调用 Geo Set 中已有的条目,Redis 只需更新这些条目的数据,只要总线 A 开始移动,它的位置就可以更新:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
GEOADD "buses" -76.99265963484487 38.87275545298483 "Bus A"
# Ruby example:
RedisClient.geoadd("buses", -76.99265963484487, 38.87275545298483, "Bus A") </span></span></span></span>
除了添加和更新之外,当然还可以从索引中删除条目。该ZREM
命令用于从 Redis 中的 Geo Set 中删除条目。ZREM 获取要从中删除记录的索引的名称和要删除的记录的 ID:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
ZREM buses "Bus A" "Bus B"
# Ruby example:
RedisClient.zrem("buses", "Bis A", "Bus B")</span></span></span></span>
地理索引可以完全删除,因为它存储为 Redis 键,所以DEL
可以使用该命令:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
DEL buses
# Ruby example:
RedisClient.del("buses")</span></span></span></span>
但是,使用DEL
大列表可能不是一个好主意,因为它可能会长时间阻塞 Redis。所以最好总是使用UNLINK
而不是DEL
,即“非阻塞”删除:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
UNLINK buses
# Ruby example:
RedisClient.unlink("buses")</span></span></span></span>
请记住,Redis 有索引过期的机制,如果你没有为索引指定过期日期,那么它永远不会过期并且会吃掉内存。为了防止这种情况发生,您需要使用EXPIRE
命令,传递索引的名称和过期秒数:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
EXPIRE buses 1000
# Ruby example:
RedisClient.expire("buses", 1000)</span></span></span></span>
Redis 使用了半懒惰的过期机制,也就是说索引在没有被读取之前是不会过期的,如果在读取操作过程中发现过期时间已经过去,那么就不返回结果了,对象本身就是从存储中删除。也就是说,在我们请求 Geo Set 之前,它将无限期地存储在内存中。
但是,Redis 有第二级过期 - 它是主动且随机的。它是一个垃圾收集器,随机读取不同的key,当key被读取时,就会出现标准的过期检查机制。
不幸的是,Redis 没有能力直接使索引中的记录过期。这样的功能必须独立开发。
如何通过地理空间数据进行阅读和搜索?
有几种方法可以从索引中读取条目。您可以使用ZRANGE
和ZSCAN
命令开始。这些命令遍历索引中的所有条目。例如,要返回索引中的所有条目:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
ZRANGE buses 0 -1
# Ruby example:
RedisClient.zrange("buses", 0, -1)</span></span></span></span>
对于地理空间数据,有两个命令可以从索引中获取条目的位置。第一个 -GEOPOS
命令返回索引中条目的坐标:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
GEOPOS buses "Bus A"
# Ruby example:
RedisClient.geopos("buses", "Bus A")</span></span></span></span>
第二个命令 -GEOHASH
返回在 geohash 中编码的条目的坐标:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
GEOHASH buses "Bus A"
# Ruby example:
RedisClient.geohash("buses", "Bus A")</span></span></span></span>
要获取索引中两个条目之间的距离,可以使用以下GEODIST
命令:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># Redis example:
GEODIST buses "Bus A" "Bus B"
# Ruby example:
RedisClient.geodist("buses", "Bus A", "Bus B", "km")</span></span></span></span>
该命令的结果将默认以米为单位返回。您可以通过将第四个参数传递给命令来指定所需的测量单位,例如:km 代表公里,m 代表米,mi - 英里,ft - 英尺。
要搜索索引,还可以使用GEORADIUS
and GEORADIUSBYMEMBER
(对于低于 6.2 的 Redis 版本)或GEOSEARCH
(对于低于 6.2 的版本)命令。
GEORADIUS
并GEORADIUSBYMEMBER
接受参数WITHDIST
(显示结果 + 到指定点/记录的距离)和WITHCOORD
(显示结果 + 记录坐标),以及ASC
orDESC
排序选项(按到点的距离排序):
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># 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")</span></span></span></span>
新版本 Redis的GEOSEARCH
命令具有类似的语法并执行相同的操作。命令语法如下所示:
<span style="color:#111111"><span style="background-color:#ffffff"><span style="color:#000000"><span style="background-color:#fbedbb"># 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 </span></span></span></span>
结论
在 Redis 中使用地理空间数据实现位置应用程序的简单性不仅可以轻松处理大量地理空间数据,还可以让您对数据进行一些复杂的处理。例如,查询半径内的条目可以帮助您搜索附近的兴趣点,方法是只为用户提供最接近他们的选择。如果您的应用程序以任何方式使用地理空间数据,请考虑将复杂的计算移至 Redis,这可能会提高您的应用程序的效率。