[TOC]
简介
这时问题来了,Sorted Set 元素的权重分数是一个浮点数(float 类型),而一组经纬度包
含的是经度和纬度两个值,是没法直接保存为一个浮点数的,那具体该怎么进行保存呢?
这就要用到 GEO 类型中的 GeoHash 编码了。
GeoHash 的编码方法
为了能高效地对经纬度进行比较,Redis 采用了业界广泛使用的 GeoHash 编码方法,这
个方法的基本原理就是“二分区间,区间编码”。
当我们要对一组经纬度进行 GeoHash 编码时,我们要先对经度和纬度分别编码,然后再
把经纬度各自的编码组合成一个最终编码。
首先,我们来看下经度和纬度的单独编码过程。
对于一个地理位置信息来说,它的经度范围是[-180,180]。GeoHash 编码会把一个经度值
编码成一个 N 位的二进制值,我们来对经度范围[-180,180]做 N 次的二分区操作,其中 N
可以自定义。
位编码。当做完 N 次的二分区后,经度值就
可以用一个 N bit 的数来表示了。
举个例子,假设我们要编码的经度值是 116.37,我们用 5 位编码值(也就是 N=5,做 5
次分区)。
我们先做第一次二分区操作,把经度区间[-180,180]分成了左分区[-180,0) 和右分区
[0,180],此时,经度值 116.37 是属于右分区[0,180],所以,我们用 1 表示第一次二分区
后的编码值。
接下来,我们做第二次二分区:把经度值 116.37 所属的[0,180]区间,分成[0,90) 和[90,
180]。此时,经度值 116.37 还是属于右分区[90,180],所以,第二次分区后的编码值仍然
为 1。等到第三次对[90,180]进行二分区,经度值 116.37 落在了分区后的左分区[90, 135)
中,所以,第三次分区后的编码值就是 0。
按照这种方法,做完 5 次分区后,我们把经度值 116.37 定位在[112.5, 123.75]这个区
间,并且得到了经度值的 5 位编码值,即 11010。这个编码过程如下表所示:
对纬度的编码方式,和对经度的一样,只是纬度的范围是[-90,90],下面这张表显示了对
纬度值 39.86 的编码过程。
当一组经纬度值都编完码后,我们再把它们的各自编码值组合在一起,组合的规则是:最
终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位
从 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 应用“搜索附近的人或物”的功能了。
分区一:[-180,0) 和[-90,0),编码 00;
分区二:[-180,0) 和[0,90],编码 01;
分区三:[0,180]和[-90,0),编码 10;
分区四:[0,180]和[0,90],编码 11。
不过,我要提醒你一句,有的编码值虽然在大小上接近,但实际对应的方格却距离比较
远。例如,我们用 4 位来做 GeoHash 编码,把经度区间[-180,180]和纬度区间[-90,90]各
分成了 4 个分区,一共 16 个分区,对应了 16 个方格。编码值为 0111 和 1000 的两个方
格就离得比较远,如下图所示:
所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或
8 个方格。
好了,到这里,我们就知道了,GEO 类型是把经纬度所在的区间编码作为 Sorted Set 中
元素的权重分数,把和经纬度相关的车辆 ID 作为 Sorted Set 中元素本身的值保存下来,
这样相邻经纬度的查询就可以通过编码值的大小范围查询来实现了。接下来,我们再来聊
聊具体如何操作 GEO 类型。
一、CEO概述
- Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信 息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需 要实现这些功能的开发者来说是一大福音
- GEO功能是Redis的另一位作者Matt Stancliff借鉴NoSQL数据库Ardb实现的,Ardb的作者来自中国,它提供了优秀的GEO功能
二、增加地理位置信息(geoadd)
1 | geoadd key longitude latitude member [longitude latitude member ...] |
- 参数如下:
- longitude:地址位置的经度
- latitude:地址位置的纬度
- member:成员
- 相关注意事项:
- geoadd一次可以添加多个地理位置信息
- geoadd添加成功返回1
- 如果member已经存在,那么该命令返回0,此时代表更新member的值
- 例如:下面添加5个城市的经纬度
三、获取地理信息位置(geopos)
1 | geodist key member1 member2 [unit] |
- 该命令用来获取两个地址位置的距离
- unit参数代表返货结果的单位,包含以下4种:
- m(meters)代表米
- km(kilometers)代表公里
- mi(miles)代表英里
- ft(feet)代表尺
- 例如:下面计算天津到北京的距离,以公里为单位
四、获取指定位置范围内的地理信息位置集合(georadius、georadiusbymember)
1 | georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] |
- 两个命令作用相同,都是以一个地理位置为中心算出指定半径内的其他地理信息位置
- 不同的是:
- georadius命令的中心位置给出了具体的经纬度
- georadiusbymember只需给出成员即可
- 其中radiusm|km|ft|mi是必需参数,指定了半径(带单位),其他可选参数意义如下:
- withcoord:返回结果中包含经纬度
- withdist:返回结果中包含离中心节点位置的距离
- withhash:返回结果中包含geohash,有关geohash后面介绍
- COUNT count:指定返回结果的数量
- asc|desc:返回结果按照离中心节点的距离做升序或者降序
- store key:将返回结果的地理位置信息保存到指定键
- storedist key:将返回结果离中心节点的距离保存到指定键。
- 例如:下面计算5个城市中,距离北京150公里以内的城市
五、获取geohash
1 | geohash key member [member ...] |
- Redis使用geohash(https://en.wikipedia.org/wiki/Geohash)**将二维经纬度转换为一维字符串**
- 例如:下面操作会返回beijing的geohash值
- geohash有如下特点:
- GEO的数据类型为zset(见下图),Redis将所有地理位置信息的geohash存放在zset中
- 字符串越长,表示的位置更精确,下图给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右
- 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令
- geohash编码和经纬度是可以相互转换的
- Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令
六、删除地理位置信息(zrem)
1 | zrem key member |
- GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除
- 例如,下面将cities:locations中的所有地理位置信息删除