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