关注不迷路,一起 From Zero To Hero !
前言
相信大家都用过打车软件、或者使用软件搜索过附近的人、附近的店等等,这种都离不开基于位置服务(Location-Based Service,LBS)的应用。此类应用都是基于经纬度来查询附近的目标,Redis GEO就适用于此类场景。
原理介绍
Redis GEO
并不是一种新的数据结构,而是基于Sorted Set
实现的。我们在之前的文章中学过Sorted Set
结构,该结构保存的数据形式是key-score
,即一个元素对应一个分值,默认是根据分值排序的,且可以进行范围查询。但是经纬度是一个数据对,比如(117.25,40.60),那么Redis是如何将经纬度转换成score
值的呢?转换成score
值之后,是如何保证分值相邻的元素距离也相近的呢?这一切就依赖于GeoHash
编码。
经度的范围是[-180,180],纬度的范围是[-90,90],当我们对经纬度进行编码时,先对经度和纬度分别进行GeoHash编码,然后再合并为一个编码值。下面我们就来介绍GeoHash的编码方法。
对于经度或纬度来说,GeoHash会将其编码为一个N为的二进制值,其实就是通过N次的分区得到的,N可以自定义。下面是具体的逻辑:
-
第一次分区:我们把经度范围[-180,180]分为两个区间[-180,0) 和[0,180],简称为左右区间。看当前的经度值落在哪个区间中,如果在左区间,记为一次0,否则记为1,这样我们就得到一位编码值了。
-
第二次分区:假设第一次落在了[0,180]区间内,我们再把该区间分为两个区间[0,90) 和[90,180],然后再根据落在左右区间,得到一个0或者1的编码值。
……
-
重复N次之后,我们就得到了N个编码值。纬度也是一样的逻辑,可以得到N个编码值。
举个具体的例子,给定经纬度[120,40],N=5。
经度120:
分区次数 | 左区间 | 右区间 | 经度120所在区间 | 编码值 |
---|---|---|---|---|
1 | [-180,0) | [0,180] | 右 | 1 |
2 | [0,90) | [90,180] | 右 | 1 |
3 | [90,135) | [135,180] | 左 | 0 |
4 | [90,112.5) | [112.5,135] | 右 | 1 |
5 | [112.5,123.75) | [123.75,135] | 左 | 0 |
纬度40:
分区次数 | 左区间 | 右区间 | 经度120所在区间 | 编码值 |
---|---|---|---|---|
1 | [-90,0) | [0,90] | 右 | 1 |
2 | [0,45) | [45,90] | 左 | 0 |
3 | [0,22.5) | [22.5,45] | 右 | 1 |
4 | [22.5,33.75) | [33.75,45] | 右 | 1 |
5 | [33.75,39.375) | [39.375,45] | 右 | 1 |
分别得到了经度和纬度的N位编码值后,是如何合并为一个编码值的呢?规则就是:最终编码值的长度是2N,其中偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值(从0开始计数,0为偶数),示意图如下:
使用了GeoHash编码后,经纬度[120,40]就被编码成了1110011101,这个值就可以作为key对应的score值。
从上面的过程可以看出,在划分区间的过程中,我们其实是把整个空间划分成了一个一个的小方格。对经度和纬度分别做一次二分区的话,就会得到四个分区。这 4 个分区对应了 4 个方格,每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有方格的编码值映射到一维空间时,相邻方格的 GeoHash 编码值基本也是接近的,如下图所示:
因此我们使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用“搜索附近的人或物”的功能了。
不过,有的编码值虽然在大小上接近,但实际对应的方格却距离比较远。例如,我们用 4 位来做 GeoHash 编码,把经度区间[-180,180]和纬度区间[-90,90]各分成了 4 个分区,一共 16 个分区,对应了 16 个方格。编码值为 0111 和 1000 的两个方格就离得比较远,如下图所示:
所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或 8 个方格。
到这里我们就了解了Redis GEO的原理,通过GeoHash编码,将元素对应的经纬度编码,然后将该元素id作为key,编码值作为score值存入Sorted Set中,最后通过Sorted Set的范围查询就可以完成我们最初的需求。下面我们来看下Redis GEO相关的指令吧!
GEOADD
可用版本:>= 3.2.0
时间复杂度:对每个要添加的元素,时间复杂度为O(log(N)),N为已存在的元素数量
版本变化:6.2.0版本后新增了NX、XX、CH参数
命令格式
|
|
命令描述
- 将指定的空间元素(经度、纬度、元素名)添加到key对应的Sorted Set中,GeoHash编码会将经纬度转化为52位比特值
- 该命令的参数格式是固定的,即(longitude latitude member),经度要在纬度之前
GEOADD
坐标是有限的: 非常接近两极的区域是无法被索引的。坐标被 EPSG:900913 / EPSG:3785 / OSGEO:41001 规范限制, 合法值如下:- 有效的经度介于 -180 度至 180 度之间
- 有效的纬度介于 -85.05112878 度至 85.05112878 度之间
- 当给定的经纬度超出上述合法范围时,会返回error
- Redis GEO 没有删除命令 GEODEL,因为底层使用的是Sorted Set,所以完全可以使用 ZREM 命令删除
可选参数
- XX: 只更新已存在的元素,不添加新元素
- NX: 只添加新元素,不更新已存在元素
- CH: 改变命令返回值的逻辑(CH是change的缩写)。命令默认返回添加新元素的个数,不包含更新已存在的元素;但是如果使用了CH 参数,命令返回被改变的元素数量,包括 添加新元素的数量 + 已存在元素被更新的数量
返回值
默认:返回新增元素的数量
CH参数:被改变的元素数量
示例
|
|
GEOPOS
可用版本:>= 3.2.0
时间复杂度:O(N),N为给定的元素个数
命令格式
|
|
命令描述
- 返回给定元素对应的经纬度
- 使用GEOADD添加的元素,会被GeoHash转化为52位比特值,因此使用GEOPOS取出值并转为经纬度时,可能与添加的经纬度值有少许差异
- 命令接收多个可变参数,返回值始终是数组形式
返回值
数组:存在的元素返回经纬度,不存在的元素返回nil
示例
|
|
GEODIST
可用版本:>= 3.2.0
时间复杂度:O(log(N))
命令格式
|
|
命令描述
- 返回两个给定元素之间的距离
- 距离度量支持如下参数:
- m: 米(默认值)
- km: 千米
- ft: 英尺
- mi:英里
- 在计算距离时会假设地球为完美的球形,在极限情况下最大会造成 0.5% 的误差
- 如果给定的元素中,有元素不存在,返回nil
返回值
字符串:双精度的距离,以字符串形式返回;如果其中一个元素不存在,返回nil
示例
|
|
GEOHASH
可用版本:>= 3.2.0
时间复杂度:对每个元素,时间复杂度为O(log(N)),N为已存在的元素数量
命令格式
|
|
命令描述
- GEOADD命令会将经纬度编码为52bit,该命令返回GeoHash编码转换后的
11
位字符串表示形式,该字符串形式,与 维基百科描述一致,兼容 geohash.org规范 - 使用该URL: http://geohash.org/,可以反向解析出命令返回的11位字符串对应的经纬度,例如http://geohash.org/sqdtr74hy01。页面中显示对应的纬度和经度
返回值
数组:返回给定元素,GeoHash编码值对应的字符串表示
示例
|
|
根据示例返回的字符串“sqdtr74hvb0”,去页面查询,得到的经纬度和添加的经纬度只有很小差异。
GEORADIUS
可用版本:>= 3.2.0
时间复杂度:O(N+log(M)),N为指定范围内元素数量,M为元素数量
版本变化:6.2.0版本开始废弃该命令,考虑使用 GEOSEARCH 和 GEOSEARCHSTORE 命令
命令格式
|
|
命令描述
- 通过给定的经纬度(
longitude
,latitude
)和半径(radius
)得到一个圆形区域,返回在区域内的元素 - 简单来说就是查询指定位置一定距离内的元素,例如查询当前位置5公里内的银行
- 半径的单位有如下几种:
- m: 米
- km: 千米
- ft: 英尺
- mi:英里
可选参数
- 附加信息
- WITHDIST:返回元素的同时,返回与指定经纬度的距离,距离单位与上述给定单位一致
- WITHCOORD:同时返回元素的经纬度信息
- WITHHASH:返回Geohash值
- 命令默认返回无序结果集,可以使用如下两种参数来返回有序数据:
- ASC:距给定经纬度由近及远的顺序
- DESC:距给定经纬度由远及近的顺序
- 命令默认返回所有符合条件的元素,同时也可以通过
COUNT count
指定返回元素的数量
ANY:
- 使用该可选参数,命令在查询时不用得到所有结果,过程中只要得到
count
个元素,直接可以返回了,因此最终距离最近或最远的元素可能不会被包含在结果集中; - 不使用该可选参数,会先得到所有结果,然后进行排序,然后再返回前 count 个。因此如果查询的范围较大,即使返回的元素较少,也会影响服务的性能。
- 命令默认只返回数据,如果想保存结果,可以使用如下两个参数:
-
STORE:保存结果到key对应的Sorted Set中,Store值为GeoHash值
-
STOREDIST:保存结果到key对应的Sorted Set中,Store为距离,单位与给定单位保持一致
注:使用以上两个该参数时,不能使用WITHDIST、WITHCOORD、WITHHASH
返回值
数组:
- 未提供可选参数时:返回元素列表,如[“New York”,“Milan”,“Paris”]
- 提供可选参数时:返回数组的数组,每个子数组代表一个元素信息。每个元素信息的第一个值为name,后续的信息先后顺序为:距离、GeoHash编码、经纬度。例如 GEORADIUS Sicily 15 37 200 km WITHCOORD WITHDIST,即使WITHCOORD在前,但是返回结果中也会在后面的位置:
|
|
示例
|
|
GEORADIUSBYMEMBER
可用版本:>= 3.2.0
时间复杂度:O(N+log(M)),N为指定范围内元素数量,M为元素数量
版本变化:6.2.0版本开始废弃该命令,考虑使用 GEOSEARCH 和 GEOSEARCHSTORE 命令
命令格式
|
|
命令描述
- 该命令与
GEORADIUS
命令基本一样,唯一不同的是:GEORADIUSBYMEMBER
给定的是Sorted Set中的一个元素,而GEORADIUS
给定的是具体经纬度。通过给定元素,其实就可以得到存储的经纬度,进而进行查询。
示例
|
|
GEOSEARCH
可用版本:>= 6.2.0
时间复杂度:O(N+log(M))
命令格式
|
|
命令描述
- 该命令是 GEORADIUS 和 GEORADIUSBYMEMBER 的整合,同时提供了额外功能。整体来说就是返回给定位置一定区域范围内的元素。
必选参数
查询的中心点通过以下必选参数指定:
- FROMMEMBER:指定元素
- FROMLONLAT:指定经纬度
查询的范围形状通过以下必选参数指定:
- BYRADIUS:圆形区域,通过半径radius指定半径
- BYBOX:长方形区域,通过 width 和 height 指定宽和长
可选参数
- 附加信息
- WITHDIST:返回元素的同时,返回与指定经纬度的距离,距离单位与上述给定单位一致
- WITHCOORD:同时返回元素的经纬度信息
- WITHHASH:返回Geohash值
- 命令默认返回无序结果集,可以使用如下两种参数来返回有序数据:
- ASC:距给定经纬度由近及远的顺序
- DESC:距给定经纬度由远及近的顺序
命令默认返回所有符合条件的元素,同时也可以通过 COUNT count
指定返回元素的数量
ANY:
- 使用该可选参数,命令在查询时不用得到所有结果,过程中只要得到
count
个元素,直接可以返回了,因此最终距离最近或最远的元素可能不会被包含在结果集中; - 不使用该可选参数,会先得到所有结果,然后进行排序,然后再返回前 count 个。因此如果查询的范围较大,即使返回的元素较少,也会影响服务的性能。
返回值
数组:
- 未提供可选参数时:返回元素列表,如[“New York”,“Milan”,“Paris”]
- 提供可选参数时:返回数组的数组,其中每个子数组代表一个元素信息。每个子数组的第一个值为name,后续的信息先后顺序为:距离、GeoHash编码、经纬度。例如 GEORADIUS Sicily 15 37 200 km WITHCOORD WITHDIST,即使WITHCOORD在前,但是返回结果中也会在后面的位置:
|
|
示例
|
|
GEOSEARCHSTORE
可用版本:>= 6.2.0
时间复杂度:O(N+log(M))
命令格式
|
|
命令描述
- 该命令与
GEOSEARCH
命令基本一样,唯一不同的是:GEOSEARCH
是直接返回结果,而GEOSEARCHSTORE
可以将结果保存到给定的keydestination
中。 - 命令默认保存代表的地理位置的GeoHash值,指定
STOREDIST
参数后,保存距离信息。
示例
|
|
总结
本文介绍了Redis GEO的基本原理和相关指令:
原理:GeoHash的编码规则
命令:
- GEOADD:添加指定经纬度的元素
- GEOPOS:查询给定元素对应的经纬度
- GEODIST:查询两个给定元素之间的距离
- GEOHASH:返回GeoHash编码后的字符串表示
- GEORADIUS:返回以指定经纬度为圆心,给定半径范围内的元素
- GEORADIUSBYMEMBER:返回以指定元素为圆心,给定半径范围内的元素
- GEOSEARCH:GEORADIUS 和 GEORADIUSBYMEMBER的整合,同时提供了长方形区域范围查询
- GEORADIUSBYMEMBER:与GEOSEARCH功能一致,提供了保存结果功能
更多
微信公众号:CodePlayer