在处理地理空间数据时,经常会遇到需要计算两个点之间的距离、在地球表面查找特定半径内的点等复杂问题。由于地球是一个不规则的椭球体,这些计算变得相当复杂。本文将探讨如何利用Redis来简化这些计算过程。
Geohash是一种将坐标表示为字符串的系统。它使用Base32编码将纬度和经度转换为字符串。例如,圣彼得堡宫殿广场的Geohash看起来像这样:udtscze2chgq。Geohash的长度可变,代表了不同的定位精度,即Geohash越短,它所代表的坐标精度就越低。换句话说,较短的Geohash将代表相同的地理位置,但精度较低。可以在尝试编码坐标。
Redis使用有序集合(ZSET)作为底层数据结构来实现地理空间数据的存储,但同时提供了实时编码和解码位置数据以及新的API。这意味着使用内置命令进行索引、搜索和按特定位置排序可以非常轻松地完成,只需几行代码:
要向Redis存储中添加新的列表(或现有列表中的新元素),可以使用GEOADD命令。以下是Redis命令和Ruby客户端操作Redis的示例:
# Redis示例:
GEOADD "buses" -74.00020246342898 40.717855101298305 "Bus A"
# Ruby示例:
RedisClient.geoadd("buses", -74.00020246342898, 40.717855101298305, "Bus A")
这些命令将向名为"buses"的Geo Set添加公交车"Bus A"的位置坐标。如果Redis中尚未存储此名称的Geo Set,则会创建一个。如果列表中已存在相同名称("Bus A")的条目,则不会添加新条目。也就是说,"Bus A"是一个唯一标识符。
也可以使用单个GEOADD调用来一次添加多个记录,这有助于减少网络和数据库负载。记录ID必须是唯一的:
# Redis示例:
GEOADD "buses" -74.00020246342898 40.717855101298305 "Bus A" -73.99472237472686 40.725856700515855 "Bus B"
# Ruby示例:
RedisClient.geoadd("buses", -74.00020246342898, 40.717855101298305, "Bus A",
-73.99472237472686, 40.725856700515855, "Bus B")
相同的命令也用于更新记录的索引。如果GEOADD被调用时Geo Set中已有条目,Redis将简单地更新这些条目的数据,例如,当公交车A开始移动时,其位置可以被更新:
# Redis示例:
GEOADD "buses" -76.99265963484487 38.87275545298483 "Bus A"
# Ruby示例:
RedisClient.geoadd("buses", -76.99265963484487, 38.87275545298483, "Bus A")
除了添加和更新,当然也可以从未索引中删除条目。ZREM命令用于从Redis的Geo Set中删除条目。ZREM接受要删除记录的索引名称和要删除的记录ID:
# Redis示例:
ZREM buses "Bus A" "Bus B"
# Ruby示例:
RedisClient.zrem("buses", "Bus A", "Bus B")
可以完全删除地理索引,由于它作为Redis键存储,可以使用DEL命令:
# Redis示例:
DEL buses
# Ruby示例:
RedisClient.del("buses")
但是,对于大列表使用DEL可能不是一个好主意,因为它可能会长时间阻塞Redis。因此,最好总是使用UNLINK而不是DEL,即'非阻塞'删除:
# Redis示例:
UNLINK buses
# Ruby示例:
RedisClient.unlink("buses")
请记住,Redis有一个索引过期机制,如果不为索引指定过期日期,那么它将永远不会过期,并且会占用内存。为了防止这种情况发生,需要使用EXPIRE命令,传递索引的名称和过期秒数:
# Redis示例:
EXPIRE buses 1000
# Ruby示例:
RedisClient.expire("buses", 1000)
Redis使用半延迟过期机制,这意味着索引在未被读取之前不会被过期,如果在读取操作期间发现过期时间已过,则结果不会被返回,对象本身将从存储中删除。也就是说,直到请求Geo Set,它将无限期地存储在内存中。
有几种方法可以从索引中读取条目。可以使用ZRANGE和ZSCAN命令开始。这些命令遍历索引中的所有条目。例如,返回索引中的所有条目:
# Redis示例:
ZRANGE buses 0 -1
# Ruby示例:
RedisClient.zrange("buses", 0, -1)
对于地理空间数据,有两个命令可以从索引中获取条目的位置。第一个是GEOPOS命令,返回索引中条目的坐标:
# Redis示例:
GEOPOS buses "Bus A"
# Ruby示例:
RedisClient.geopos("buses", "Bus A")
第二个命令GEOHASH返回条目编码为geohash的坐标:
# Redis示例:
GEOHASH buses "Bus A"
# Ruby示例:
RedisClient.geohash("buses", "Bus A")
要获取索引中两个条目之间的距离,可以使用GEODIST命令:
# Redis示例:
GEODIST buses "Bus A" "Bus B"
# Ruby示例:
RedisClient.geodist("buses", "Bus A", "Bus B", "km")
命令的结果默认以米为单位返回。可以通过将第四个参数传递给命令来指定所需的测量单位,例如:km表示公里,m表示米,mi表示英里,ft表示英尺。
要搜索索引,也使用GEORADIUS和GEORADIUSBYMEMBER(对于Redis版本小于6.2)或GEOSEARCH(对于版本大于6.2)命令。GEORADIUS和GEORADIUSBYMEMBER接受WITHDIST(显示结果+指定点/记录的距离)和WITHCOORD(显示结果+记录坐标)参数,以及ASC或DESC排序选项(按距离点排序):
# Redis示例:
GEORADIUS buses -73 40 200 km WITHDIST
# 返回:
1) 1) "Bus A"
2) "190.4424"
2) 1) "Bus B"
2) "56.4413"
GEORADIUS buses -73 40 200 km WITHCOORD
# 返回:
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
# 返回:
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"
GEORADIUSBYMEMBER buses "Bus A" 100 km
# 返回:
1) "Bus B"
# Ruby示例:
RedisClient.georadiusbymember("buses", "Bus A", 100, "km")
对于新版本的Redis,GEOSEARCH命令具有类似的语法并执行相同的操作。命令语法如下:
# Redis示例:
GEOSEARCH buses FROMMEMBER "Bus A" BYRADIUS 100 km ASC WITHCOORD WITHDIST WITHHASH
# 返回所有距离Bus A 100km范围内的条目,包括坐标、距离和geohashes
GEOSEARCH buses FROMLONLAT -74.00020246342898 40.717855101298305 BYRADIUS 200 mi DESC COUNT 2
# 返回最多2个条目,从最远到最近排序,距离中心200英里,给定坐标