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

125 阅读7分钟

处理地理空间数据 是出了名的困难,因为经纬度是浮点数,应该是非常精确的。此外,经纬度似乎可以用网格来表示,但事实上,它们不能,原因很简单,因为地球不是平的,而数学是一门复杂的科学。

例如,为了确定球体上两点之间的大圆距离,根据它们的经纬度,使用了哈维辛公式,它看起来像这样。

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

在这篇文章中,我们将看看 Redis如何帮助我们在处理地理空间数据时尽量减少计算量。

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

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

Geohash是一个 以字符串表示坐标的系统。Geohashing使用Base32编码将纬度和经度转换为字符串。例如,圣彼得堡皇宫广场的坐标的地理哈希值看起来是这样的:udtscze2chgq。一个可变的地理哈希长度代表一个可变的位置精度;换句话说,地理哈希越短,它所代表的坐标就越不精确。也就是说,较短的geohash将代表相同的地理位置,但精度较低。你可以尝试用geohash编码坐标。

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

Redis使用排序列表(ZSET)作为底层数据结构来实现地理空间数据的存储,但对位置数据进行即时编码和解码,并采用新的API。这意味着索引、搜索和按特定位置排序可以用很少的几行代码和最小的努力使用内置命令扔进Redis。GEOADDGEODISTGEORADIUSGEORADIUSBYMEMBERGEOSEARCH)。

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")    

这些命令向名为 "公共汽车 "的地理集添加公共汽车 "A "的位置坐标。如果Redis中还没有存储这个名称的地理集,它将被创建。只有当同名的条目("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")

同一命令用于更新记录的索引。如果在调用GEOADD时,Geo Set中已有条目,Redis只需更新这些条目的数据;只要巴士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")

请记住,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返回在地理哈希中编码的条目的坐标。

# 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(对于Redis版本小于6.2)或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,它可能会提高你的应用程序的效率。