你必须非常努力,才能看起来毫不费力!
微信搜索公众号[ 漫漫Coding路 ],一起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参数
命令格式
GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]
命令描述
- 将指定的空间元素(经度、纬度、元素名)添加到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参数:被改变的元素数量
示例
127.0.0.1:6379> geoadd city 116.41667 39.91667 beijing 121.43333 34.50000 shanghai 117.20000 39.13333 tianjin
(integer) 3
127.0.0.1:6379> geoadd city NX 116.41667 39.91667 beijing
(integer) 0
GEOPOS
可用版本:>= 3.2.0
时间复杂度:O(N),N为给定的元素个数
命令格式
GEOPOS key member [member ...]
命令描述
- 返回给定元素对应的经纬度
- 使用GEOADD添加的元素,会被GeoHash转化为52位比特值,因此使用GEOPOS取出值并转为经纬度时,可能与添加的经纬度值有少许差异
- 命令接收多个可变参数,返回值始终是数组形式
返回值
数组:存在的元素返回经纬度,不存在的元素返回nil
示例
127.0.0.1:6379> geoadd city 116.41667 39.91667 beijing 121.43333 34.50000 shanghai 117.20000 39.13333 tianjin
(integer) 3
127.0.0.1:6379> geopos city beijing nanjing
1) 1) "116.41667157411575317"
2) "39.91667095273589183"
2) (nil)
GEODIST
可用版本:>= 3.2.0
时间复杂度:O(log(N))
命令格式
GEODIST key member1 member2 [m|km|ft|mi]
命令描述
- 返回两个给定元素之间的距离
- 距离度量支持如下参数:
- m: 米(默认值)
- km: 千米
- ft: 英尺
- mi:英里
- 在计算距离时会假设地球为完美的球形,在极限情况下最大会造成 0.5% 的误差
- 如果给定的元素中,有元素不存在,返回nil
返回值
字符串:双精度的距离,以字符串形式返回;如果其中一个元素不存在,返回nil
示例
127.0.0.1:6379> geoadd city 116.41667 39.91667 beijing 121.43333 34.50000 shanghai 117.20000 39.13333 tianjin
(integer) 3
127.0.0.1:6379> geodist city beijing shanghai
"748346.9287"
127.0.0.1:6379> geodist city beijing shanghai km
"748.3469"
GEOHASH
可用版本:>= 3.2.0
时间复杂度:对每个元素,时间复杂度为O(log(N)),N为已存在的元素数量
命令格式
GEOHASH key member [member ...]
命令描述
- GEOADD命令会将经纬度编码为52bit,该命令返回GeoHash编码转换后的
11
位字符串表示形式,该字符串形式,与 维基百科描述一致,兼容 geohash.org规范 - 使用该URL: http://geohash.org/,可以反向解析出命令返回的11位字符串对应的经纬度,例如http://geohash.org/sqdtr74hy01。页面中显示对应的纬度和经度
返回值
数组:返回给定元素,GeoHash编码值对应的字符串表示
示例
127.0.0.1:6379> geoadd city 15.08723 37.50265 test
(integer) 1
127.0.0.1:6379> geohash city test
1) "sqdtr74hvb0"
根据示例返回的字符串“sqdtr74hvb0”,去页面查询,得到的经纬度和添加的经纬度只有很小差异。
GEORADIUS
可用版本:>= 3.2.0
时间复杂度:O(N+log(M)),N为指定范围内元素数量,M为元素数量
版本变化:6.2.0版本开始废弃该命令,考虑使用 GEOSEARCH 和 GEOSEARCHSTORE 命令
命令格式
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
命令描述
- 通过给定的经纬度(
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在前,但是返回结果中也会在后面的位置:
["Palermo","190.4424",["13.361389338970184","38.115556395496299"]]
示例
127.0.0.1:6379> geoadd city 116.408 39.904 beijing
(integer) 1
127.0.0.1:6379> geoadd city 116.298 39.959 haidian
(integer) 1
127.0.0.1:6379> geoadd city 116.443 39.922 chaoyang
(integer) 1
127.0.0.1:6379> geoadd city 121.445 31.213 shanghai
(integer) 1
127.0.0.1:6379> geoadd city 121.23 31.07 minhang
(integer) 1
127.0.0.1:6379> geoadd city 117.246 39.117 tianjin
(integer) 1
# withdist
127.0.0.1:6379> GEORADIUS city 116 40 100 km withdist
1) 1) "haidian"
2) "25.8046"
2) 1) "beijing"
2) "36.3898"
3) 1) "chaoyang"
2) "38.7507"
# withdist withhash withcoord
127.0.0.1:6379> GEORADIUS city 116 40 100 km withdist withhash withcoord
1) 1) "haidian"
2) "25.8046"
3) (integer) 4069880423958265
4) 1) "116.29799991846084595"
2) "39.95900079609685207"
2) 1) "beijing"
2) "36.3898"
3) (integer) 4069885369376452
4) 1) "116.40800267457962036"
2) "39.90399988166036138"
3) 1) "chaoyang"
2) "38.7507"
3) (integer) 4069885695970869
4) 1) "116.44300013780593872"
2) "39.92199893661282317"
# DESC
127.0.0.1:6379> GEORADIUS city 116 40 100 km withdist DESC
1) 1) "chaoyang"
2) "38.7507"
2) 1) "beijing"
2) "36.3898"
3) 1) "haidian"
2) "25.8046"
# 提供any参数,会随机返回两个元素,并排序,可以看到结果中没有chaoyang
127.0.0.1:6379> GEORADIUS city 116 40 100 km withdist COUNT 2 any desc
1) 1) "beijing"
2) "36.3898"
2) 1) "haidian"
2) "25.8046"
# 不使用any参数,会返回排序后的前两个元素
127.0.0.1:6379> GEORADIUS city 116 40 100 km withdist COUNT 2 desc
1) 1) "chaoyang"
2) "38.7507"
2) 1) "beijing"
2) "36.3898"
# 使用store,默认保存地理位置信息GeoHash
127.0.0.1:6379> GEORADIUS city 116 40 100 km store city_res
(integer) 3
127.0.0.1:6379> zrange city_res 0 -1 withscores
1) "haidian"
2) "4069880423958265"
3) "beijing"
4) "4069885369376452"
5) "chaoyang"
6) "4069885695970869"
# storedist,保存距离
127.0.0.1:6379> GEORADIUS city 116 40 100 km storedist city_res
(integer) 3
127.0.0.1:6379> zrange city_res 0 -1 withscores
1) "haidian"
2) "25.80461312279574"
3) "beijing"
4) "36.389782386698734"
5) "chaoyang"
6) "38.750694603755257"
GEORADIUSBYMEMBER
可用版本:>= 3.2.0
时间复杂度:O(N+log(M)),N为指定范围内元素数量,M为元素数量
版本变化:6.2.0版本开始废弃该命令,考虑使用 GEOSEARCH 和 GEOSEARCHSTORE 命令
命令格式
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
命令描述
- 该命令与
GEORADIUS
命令基本一样,唯一不同的是:GEORADIUSBYMEMBER
给定的是Sorted Set中的一个元素,而GEORADIUS
给定的是具体经纬度。通过给定元素,其实就可以得到存储的经纬度,进而进行查询。
示例
127.0.0.1:6379> geoadd city 116.408 39.904 beijing 116.298 39.959 haidian 116.443 39.922 chaoyang 121.445 31.213 shanghai 121.23 31.07 minhang 117.246 39.117 tianjin
(integer) 6
127.0.0.1:6379> GEORADIUSBYMEMBER city beijing 500 km withdist asc
1) 1) "beijing"
2) "0.0000"
2) 1) "chaoyang"
2) "3.5948"
3) 1) "haidian"
2) "11.2004"
4) 1) "tianjin"
2) "113.2837"
GEOSEARCH
可用版本:>= 6.2.0
时间复杂度:O(N+log(M))
命令格式
GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
命令描述
- 该命令是 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在前,但是返回结果中也会在后面的位置:
["Palermo","190.4424",["13.361389338970184","38.115556395496299"]]
示例
127.0.0.1:6379> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
127.0.0.1:6379> GEOADD Sicily 12.758489 38.788135 "edge1" 17.241510 38.788135 "edge2"
(integer) 2
127.0.0.1:6379> GEOSEARCH Sicily FROMLONLAT 15 37 BYRADIUS 200 km ASC
1) "Catania"
2) "Palermo"
127.0.0.1:6379> GEOSEARCH Sicily FROMLONLAT 15 37 BYBOX 400 400 km ASC WITHCOORD WITHDIST
1) 1) "Catania"
2) "56.4413"
3) 1) "15.08726745843887329"
2) "37.50266842333162032"
2) 1) "Palermo"
2) "190.4424"
3) 1) "13.36138933897018433"
2) "38.11555639549629859"
3) 1) "edge2"
2) "279.7403"
3) 1) "17.24151045083999634"
2) "38.78813451624225195"
4) 1) "edge1"
2) "279.7405"
3) 1) "12.7584877610206604"
2) "38.78813451624225195"
127.0.0.1:6379>
GEOSEARCHSTORE
可用版本:>= 6.2.0
时间复杂度:O(N+log(M))
命令格式
GEOSEARCHSTORE destination source [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [STOREDIST]
命令描述
- 该命令与
GEOSEARCH
命令基本一样,唯一不同的是:GEOSEARCH
是直接返回结果,而GEOSEARCHSTORE
可以将结果保存到给定的keydestination
中。 - 命令默认保存代表的地理位置的GeoHash值,指定
STOREDIST
参数后,保存距离信息。
示例
127.0.0.1:6379> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
127.0.0.1:6379> GEOADD Sicily 12.758489 38.788135 "edge1" 17.241510 38.788135 "edge2"
(integer) 2
# 查询,只返回结果
127.0.0.1:6379> GEOSEARCH Sicily FROMLONLAT 15 37 BYRADIUS 200 km ASC withhash
1) 1) "Catania"
2) (integer) 3479447370796909
2) 1) "Palermo"
2) (integer) 3479099956230698
# 将查询结果保存到 Sicily_des 中,默认保存geohash
127.0.0.1:6379> GEOSEARCHSTORE Sicily_des Sicily FROMLONLAT 15 37 BYRADIUS 200 km ASC
(integer) 2
# 遍历 Sicily_des
127.0.0.1:6379> zrange Sicily_des 0 -1 withscores
1) "Palermo"
2) "3479099956230698"
3) "Catania"
4) "3479447370796909"
# 保存距离信息
127.0.0.1:6379> GEOSEARCHSTORE Sicily_des Sicily FROMLONLAT 15 37 BYRADIUS 200 km ASC STOREDIST
(integer) 2
127.0.0.1:6379> zrange Sicily_des 0 -1 withscores
1) "Catania"
2) "56.441257870158204"
3) "Palermo"
4) "190.44242984775784"
总结
本文介绍了Redis GEO的基本原理和相关指令:
原理:GeoHash的编码规则
命令:
- GEOADD:添加指定经纬度的元素
- GEOPOS:查询给定元素对应的经纬度
- GEODIST:查询两个给定元素之间的距离
- GEOHASH:返回GeoHash编码后的字符串表示
- GEORADIUS:返回以指定经纬度为圆心,给定半径范围内的元素
- GEORADIUSBYMEMBER:返回以指定元素为圆心,给定半径范围内的元素
- GEOSEARCH:GEORADIUS 和 GEORADIUSBYMEMBER的整合,同时提供了长方形区域范围查询
- GEORADIUSBYMEMBER:与GEOSEARCH功能一致,提供了保存结果功能
更多
个人博客: https://lifelmy.github.io/
微信公众号:漫漫Coding路