GEORADIUS
和 GEORADIUSBYMEMBER
这样的命令可以在之后通过位置查询取得这些元素。注意:
当用户尝试输入一个超出范围的经度或者纬度时, GEOADD 命令将返回一个错误
GEOADD key longitude latitude member [longitude latitude member ...]
longitude、latitude、member分别是该地理位置的经度、纬度、成员
127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing
(integer) 1
127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing
(integer) 0
geoadd可以同时添加多个地理位置信息。
127.0.0.1:6379> geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding
(integer) 4
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEOPOS Sicily Palermo Catania NonExisting
1) 1) "13.361389338970184"
2) "38.115556395496299"
2) 1) "15.087267458438873"
--- 2) "37.50266842333162"
3-) (nil)
1-27.0.0.1:6379> geopos cities:locations tianjin
1) 1) "117.12000042200088501"
- 2) "39.0800000535766543"
返回两个给定位置之间的距离。
如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit 必须是以下单位的其中一个:
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。
GEODIST key member1 member2 [m|km|ft|mi]
最后一个距离单位参数说明:
127.0.0.1:6379> geodist cities:locations beijing tianjin km
"89.2061"
redis> GEODIST Sicily Foo Bar
(nil)
georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
范围可以使用以下其中一个单位:
在给定以下可选项时, 命令会返回额外的信息:
命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT
选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。
GEORADIUS 命令返回一个数组, 具体来说:
WITHCOORD 、 WITHDIST 、 WITHHASH
等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:
由两个元素组成的坐标,分别为经度和纬度。
举个例子,GEORADIUS Sicily 15 37 200 km withcoord withdist
这样的命令返回的每个子数组都是类似以下格式的:
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEORADIUS Sicily 15 37 200 km WITHDIST
1) 1) "Palermo"
2) "190.4424"
2) 1) "Catania"
2) "56.4413"
redis> GEORADIUS Sicily 15 37 200 km WITHCOORD
1) 1) "Palermo"
2) 1) "13.361389338970184"
2) "38.115556395496299"
2) 1) "Catania"
2) 1) "15.087267458438873"
2) "37.50266842333162"
redis> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD
1) 1) "Palermo"
2) "190.4424"
3) 1) "13.361389338970184"
2) "38.115556395496299"
2) 1) "Catania"
2) "56.4413"
3) 1) "15.087267458438873"
2) "37.50266842333162"
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
一个数组, 数组中的每个项表示一个范围之内的位置元素。
127.0.0.1:6379> georadiusbymember cities:locations beijing 100 km #距离北京100km内的城市
1) "beijing"
2) "tianjin"
geohash key member [member ... ]
Redis使用geohash将二维经纬度转换为一维字符串
geohash有如下特点:
Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"
zrem key member
GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所有可以借用zrem实现对地理位置信息的删除。
127.0.0.1:6379> zrem cities:locations tangshan
(integer) 1
一般来说,在设计一个数据类型的底层结构时,我们首先要知道,要处理的数据有什么访问特定。所以,我们要先搞清楚位置信息是怎么被存取的。
以叫车服务为例,来分析下 LBS 应用中经纬度的存取特点。
可以看到,一辆车(或一个用户)对应一组经纬度,并且随着车(或用户)的位置移动,相应的经纬度也会变化。
这种数据记录模式属于一个 key(例如车 ID)对应一个 value(一组经纬度)。当有很多车辆信息要保存时,就需要有一个集合来保存一系列的 key 和 value。 Hash集合类型可以快速存取一系列的key和value,正好可以用来记录一系列车辆ID和经纬度的对应关系。所以,我们可以把不同车辆的ID和它们对应的经纬度信息存在Hash集合中。如下图
但是,除了记录经纬度信息,我们还需要根据用户的经纬度信息在车辆的Hash集合中进行范围查询。一旦设计到范围查询,就意味着集合中的元素需要有序,但是Hash类型的元素是无需的,所以不能用hash
那能不能用sorted set呢? Sorted Set 类型也支持一个 key 对应一个 value 的记录模式,其中,key 就是 SortedSet 中的元素,而 value 则是元素的权重分数。更重要的是,Sorted Set 可以根据元素的权重分数排序,支持范围查询。这就能满足 LBS 服务中查找相邻位置的需求了。
实际上,GEO类型的底层数据结构就是用sorted Set来实现的。用 Sorted Set 来保存车辆的经纬度信息时,Sorted Set 的元素是车辆 ID,元素的权重分数是经纬度信息,如下图所示:
这时问题来了,Sorted Set 元素的权重分数是一个浮点数(float 类型),而一组经纬度包含的是经度和纬度两个值,是没法直接保存为一个浮点数的,那具体该怎么进行保存呢?
这就要用到 GEO 类型中的 GeoHash 编码了。
GeoHash 编码方法的基本原理是“二分区间,区间编码”
当我们要对一组经纬度进行GeoHash编码时,我们先对经度和维度分别编码,然后再把经纬度各自的编码组成一个最终编码。
首先,我们来看下经度和纬度的单独编码过程。
对于一个地理位置信息来说,它的经度范围是[-180,190]。GeoHash编码对把一个经纬度编码成一个N位的二进制值,我们来对经度范围[-180,180]做 N 次的二分区操作,其中 N可以自定义。
在进行第一次二分区时,经度范围[-180,180]会被分成两个子区间:[-180,0) 和[0,180](左、右分区)。此时,我们可以查看一下要编码的经度值落在了左分区还是右分区。如果是落在左分区,我们就用 0 表示;如果落在右分区,就用 1 表示。这样一来,每做完一次二分区,我们就可以得到 1 位编码值。
然后,我们再对经度值所属的分区再做一次二分区,同时再次查看经度值落在了二分区后的左分区还是右分区,按照刚才的规则再做 1 位编码。当做完 N 次的二分区后,经度值就可以用一个 N bit 的数来表示了。
举个例子,假设我们要编码的经度值是 116.37,我们用 5 位编码值(也就是 N=5,做 5次分区)。
当一组经纬度值都编完码后,我们再把它们的各自编码值组合在一起,组合的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始。
我们刚刚计算的经纬度(116.37,39.86)的各自编码值是 11010 和 10111,组合之后,第 0 位是经度的第 0 位 1,第 1 位是纬度的第 0 位 1,第 2 位是经度的第 1 位 1,第 3位是纬度的第 1 位 0,以此类推,就能得到最终编码值 1110011101,如下图所示:
用了 GeoHash 编码后,原来无法用一个权重分数表示的一组经纬度(116.37,39.86)就可以用 1110011101 这一个值来表示,就可以保存为 Sorted Set 的权重分数了。
当然,使用 GeoHash 编码后,我们相当于把整个地理空间划分成了一个个方格,每个方格对应了 GeoHash 中的一个分区。
举个例子。我们把经度区间[-180,180]做一次二分区,把纬度区间[-90,90]做一次二分区,就会得到 4 个分区。我们来看下它们的经度和纬度范围以及对应的 GeoHash 组合编码。
这 4 个分区对应了 4 个方格,每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有方格的编码值映射到一维空间时,相邻方格的 GeoHash 编码值基本也是接近的,如下图所示:
所以,我们使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用“搜索附近的人或物”的功能了。
不过,有的编码值虽然在大小上接近,但实际对应的方格却距离比较远。例如,我们用 4 位来做 GeoHash 编码,把经度区间[-180,180]和纬度区间[-90,90]各分成了 4 个分区,一共 16 个分区,对应了 16 个方格。编码值为 0111 和 1000 的两个方格就离得比较远,如下图所示:
所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或8 个方格。
总结:GEO 类型是把经纬度所在的区间编码作为 Sorted Set 中
元素的权重分数,把和经纬度相关的车辆 ID 作为 Sorted Set 中元素本身的值保存下来,这样相邻经纬度的查询就可以通过编码值的大小范围查询来实现了。