Geo的基本操作
redis地理位置信息geo的基本操作和使用咱们之前已经聊过,可以看看这篇文章 微信附近的人,用redis也能实现?
GEO的底层结构
和zset的相似之处
首先我们不妨大胆来猜一猜GEO的底层结构是什么样的?首先一个key里面得保存各个member和经纬度,而且经纬度还必须得能够排序,所以我们发现这个结构其实和redis的zset结构其实挺像的,唯一的区别可能在于zset只有一个score,而GEO有经度和纬度,所以我们只需要解决能用一个score来保存经度和纬度就可以解决问题了。其实redis的确也是这么做的,而且GEO的底层其实就是在zset的结果上做了一层封装,所以按照严格意义上讲GEO并不是redis的一种新的数据类型。
GEO的hash编码方式
为了能高效地对经纬度进行比较,Redis 采用了业界广泛使用的 GeoHash 编码方法,这 个方法的基本原理就是“二分区间,区间编码”。
当我们要对一组经纬度进行 GeoHash 编码时,我们要先对经度和纬度分别编码,然后再 把经纬度各自的编码组合成一个最终编码。
首先,我们来看下经度和纬度的单独编码过程。我们以经纬度 116.37,39.86为例 首先看经度 116.37
- 第一次二分区操作,把经度区间[-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 的编码过程。
我们刚刚计算的经纬度(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 的权重分数了。最后根据上述得到的二进制值,以5位为一组,进行base32编码
最后获得的结果就是一组经纬度的geohash值。
地理位置二维转一维
上文讲了GeoHash的计算步骤,仅仅说明是什么而没有说明为什么?为什么分别给经度和维度编码?为什么需要将经纬度两串编码交叉组合成一串编码?本节试图回答这一问题。
如下图所示,我们将二进制编码的结果填写到空间中,当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线,当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子块也形成Z曲线,这种类型的曲线被称为Peano空间填充曲线。
这种类型的空间填充曲线的优点是将二维空间转换成一维曲线(事实上是分形维),对大部分而言,编码相似的距离也相近, 但Peano空间填充曲线最大的缺点就是突变性,有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大。
所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或 8 个方格。