Redis GeoHash 核心原理解析,你学废了吗?

作者 | SoWhat1412

来源 | SoWhat1412(ID:sowhat9094)

头图 |  CSDN 下载自东方IC

引言

小麦同学是个吃货+技术宅,平日里就喜欢拿着手机地图点点按按来查询一些好玩的东西。某一天到北海公园游玩,肚肚饿了,于是乎打开手机地图,搜索北海公园附近的餐馆,并选了其中一家用餐。

饱暖思yin欲的小麦饭后思考「地图后台如何根据自己所在位置查询来查询附近餐馆的呢」?苦思冥想了半天,小麦想出了个方法:计算所在位置P与北京所有餐馆的距离,然后返回距离<=1000米的餐馆。小得意了一会儿,小麦发现北京的餐馆何其多啊,这样计算不得了,于是想了,既然知道经纬度了,那它应该知道自己在西城区,那应该计算所在位置P与西城区所有餐馆的距离啊,机机运用了递归的思想,想到了西城区也很多餐馆啊,应该计算所在位置P与所在街道所有餐馆的距离,这样计算量又小了,效率也提升了。

小麦的计算思想很朴素,就是通过「过滤」的方法来减小参与计算的餐馆数目,从某种角度上讲,机机在使用索引技术。

一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL、oracle、PostgreSQL等)都在使用B树。B树索引本质上是对索引字段进行排序,然后通过类似「二分查找」的方法进行快速查找,即它要求索引的字段是可排序的,一般而言,可排序的是一维字段,比如时间、年龄、薪水等等。但是对于空间上的一个点(二维,包括经度和纬度),如何排序呢?又如何索引呢?解决的方法很多,下文介绍一种方法来解决这一问题。

思想:如果能通过某种方法将二维的点数据转换成一维的数据,那样不就可以继续使用B树索引了嘛。那这种方法真的存在嘛,答案是肯定的。目前很火的 GeoHash 算法就是运用了上述思想,下面我们就开始 GeoHash 之旅吧。

感性认识

先来两个干货,在线查看 GPS 某个区域的 GeoHash 值。

1. http://geohash.gofreerange.com/[1]

2. http://www.geohash.cn/[2]

更好用些

通俗说

GeoHash 将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash 字符串,分别是 WX4ER,WX4G2、WX4G3 等等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的 GeoHash 字符串都是 WX4ER,所以可以把 WX4ER 当作 key,把该区域的餐馆信息当作 value 来进行缓存,而如果不使用 GeoHash 的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。

字符串越长,表示的范围越精确。如图所示,5位的编码能表示10平方千米范围的矩形区域,而6位编码能表示更精细的区域(约0.34平方千米)

字符串相似的表示距离相近(特殊情况后文阐述),这样可以利用字符串的前缀匹配来查询附近的 POI 信息。如下两个图所示,第一个在城区,第二个在郊区,城区的 GeoHash 字符串之间比较相似,郊区的字符串之间也比较相似,而城区和郊区的 GeoHash 字符串相似程度要低些。

通过上面的介绍我们知道了 GeoHash 就是一种将经纬度转换成字符串的方法,并且使得在大部分情况下,字符串前缀匹配越多的距离越近,回到我们的案例,根据所在位置查询来查询附近餐馆时,只需要将所在位置经纬度转换成 GeoHash 字符串,并与各个餐馆的 GeoHash 字符串进行前缀匹配,匹配越多的距离越近。

GeoHash算法的步骤

下面以北海公园附近随便一个位置为例介绍GeoHash算法的计算步骤,先用百度 GPS反定位系统查找看下经纬度。

纬度=116.395371,经度=39.931957。

1、根据经纬度计算GeoHash二进制编码

地球纬度区间是[-90,90], 北海公园的纬度是39.928167,可以通过下面算法对纬度39.928167进行逼近编码:

  1. 区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1;

  2. 接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0;

  3. 递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167;

  4. 如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会产生一个序列1011100,序列的长度跟给定的区间划分次数有关。

39.928167 根据纬度算编码

bitminmidmax
1-90.0000.00090.000
00.00045.00090.000
10.00022.50045.000
122.50033.75045.000
133.75039.37545.000
039.37542.18845.000
039.37540.781542.188
039.37540.0782540.7815
139.37539.72662540.07825
139.72662539.902437540.07825

同理,地球经度区间是[-180,180],可以对经度116.389550进行编码。根据经度算编码

bitminmidmax
1-1800.000180
10.00090180
090135180
190112.5135
0112.5123.75135
0112.5118.125123.75
1112.5115.3125118.125
0115.3125116.71875118.125
1115.3125116.015625116.71875
1116.015625116.3671875116.71875

2、组码

通过上述计算,纬度产生的编码为10111 00011,经度产生的编码为11010 01011。偶数位放经度,奇数位放纬度,把2串编码组合生成新串:11100 11101 00100 01111。最后使用用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,首先将11100 11101 00100 01111转成十进制,对应着28、29、4、15,十进制对应的编码就是wx4g。同理,将编码转换成经纬度的解码算法与之相反,具体不再赘述。

GeoHash Base32编码长度与精度

可以看出,当geohash base32编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右,编码长度需要根据数据情况进行选择。

经纬度距离换算

在纬度相等的情况下:
  • 经度每隔0.00001度,距离相差约1米;

  • 每隔0.0001度,距离相差约10米;

  • 每隔0.001度,距离相差约100米;

  • 每隔0.01度,距离相差约1000米;

  • 每隔0.1度,距离相差约10000米。

在经度相等的情况下:
  • 纬度每隔0.00001度,距离相差约1.1米;

  • 每隔0.0001度,距离相差约11米;

  • 每隔0.001度,距离相差约111米;

  • 每隔0.01度,距离相差约1113米;

  • 每隔0.1度,距离相差约11132米。


GeoHash算法

上文讲了GeoHash的计算步骤,仅仅说明是什么而没有说明为什么?为什么分别给经度和维度编码?为什么需要将经纬度两串编码交叉组合成一串编码?本节试图回答这一问题。

如下图所示,我们将二进制编码的结果填写到空间中,当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线,当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为 Peano 空间填充曲线。

这种类型的空间填充曲线的优点是将二维空间转换成一维曲线(事实上是分形维),对大部分而言,编码相似的距离也相近, 但Peano空间填充曲线最大的缺点就是突变性,有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大。  

除 Peano 空间填充曲线外,还有很多空间填充曲线,如图所示,其中效果公认较好是 Hilbert 空间填充曲线,相较于 Peano 曲线而言,Hilbert 曲线没有较大的突变。为什么 GeoHash 不选择 Hilbert 空间填充曲线呢?可能是 Peano曲线思路以及计算上比较简单吧,事实上,Peano 曲线就是一种四叉树线性编码方式。

使用注意点

1. 临界问题

由于GeoHash是将区域划分为一个个规则矩形,并对每个矩形进行编码,这样在查询附近POI信息时会导致以下问题,比如红色的点是我们的位置,绿色的两个点分别是附近的两个餐馆,但是在查询的时候会发现距离较远餐馆的GeoHash编码与我们一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。这个问题往往产生在边界处

解决的思路很简单,我们查询时,除了使用定位点的GeoHash编码进行匹配外,还使用周围8个区域的GeoHash编码,这样可以避免这个问题。

2. 注意点

我们已经知道现有的 GeoHash 算法使用的是 Peano 空间填充曲线,这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近餐馆时候,首先筛选GeoHash编码「相似的POI(point of interest)点」,然后进行实际距离计算。

3. 使用心得

GeoHash 只是空间索引的一种方式,特别适合点数据,而对「线、面数据采用R树索引」更有优势(可为什么需要空间索引)。

GeoHash值可以区分精度,位数越多,精度越高,表达的地理位置越精细;如一位的GeoHash值把地球划分为32个矩形,8位的geohash值把地球划分为32^8个小矩形

适合根据某个经纬度坐标position计算出GeoHash值,然后和数据库中精度更高的GeoHash值做前缀比较

空间索引

常见问题:如何根据自己所在位置查询来查询附近50米的POI(point of interest,比如商家、景点等)呢(图1a)?

每个POI都有经纬度信息,用图1b的SQL语句在mySQL中建立了POI_spatial的表,其中lat和lng两个字段来代表纬度经度。为后续分析方便起见,我人造了40万个POI数据。

方法一:暴力方法

该方法的思路很直接:计算位置与所有POI的距离,并保留距离小于50米的POI。

插句题外话,计算经纬度之间的距离不能像求欧式距离那样平方开根号,因为地球是个不规整的球体(图2a),普通计算适合都是默认按最简单的完美球体假设,两点之间的距离函数应该如图2b所示。

该方法的复杂度为:40万*距离函数。我们将球体距离函数写为mysql存储过程distance,之后我们执行查询操作(图3),发现花费了4.66秒。

方法二:矩形过滤方法

该方法采用逐步细化的方式,一般分为两部:

  1. 先用矩形框过滤(图4a),判断一个点在矩形框内很简单,只要进行两次判断(LtMin<lat<LtMax; LnMin<lng<LnMax),落在矩形框内的POI个数为n(n<<40万);

  2. 用球面距离公式计算位置与矩形框内n个POI的距离(图4b),并保留距离小于50米的POI

矩形过滤方法的复杂度:40万矩形过滤函数 + n距离函数(n<<40万)。

 根据这个思路我们执行SQl查询(图5)(注:经度或纬度每隔0.001度,距离相差约100米,由此推算出矩形左下角和右上角坐标),发现过滤后正好剩下两个POI。

此查询花费了0.36秒,相比于方法一查询时间大大降低,但是对于一次查询来说还是很长。时间长的原因在于遍历了40万次。

方法三:B树对经度或纬度建立索引

方法二耗时的原因在于执行了遍历操作,为了不进行遍历,我们自然想到了索引。我们对纬度进行了B树索引。

alter table poi_spatial add index latindex(lat);
alter table poi_spatial add index lngindex(lng);

此方法包括三个步骤:

  1. 通过B树快速找到某纬度范围的POI(图6a),个数为m(m<40万),复杂度为Log(40万)*过滤函数;

  2. 在步骤a过滤得到的m个POI中查找某经度范围的POI(图6b),个数为n(n<m),复杂度为m*过滤函数;

  3. 用球面距离公式计算位置与步骤b得到的n个POI的距离(图6c),并保留距离小于50米的POI

执行SQL查询(图7),发现时间已经大大降低,从方法2的0.36秒下降到0.01秒

B树能索引空间数据吗?

这时候有人会说了:方法三效果如此好,能够满足我们附近POI查询问题啊,看来B树用来索引空间数据也是可以的嘛!那么B树真的能够索引空间数据吗?

  1. 只能对经度或纬度索引(一维索引),与期望的不符 我们期待的是快速找出落在某一空间范围的POI(如矩形)(图8a),而不是快速找出落在某纬度或经度范围的POI(图8b),想象一下,我要查询北京某区的POI,但是B树索引不仅给我找出了北京的,还有与北京同一维度的天津、大同、甚至国外城市的POI,当数据量很大时,效率很低。

  2. 当数据是多维,比如三维(x,y,z),B树怎么索引?比如z可能是高程值,也可能是时间。有人会说B树其实可以对「多个字段进行索引」,但这时需要指定优先级,形成一个组合字段,而空间数据在各个维度方向上不存在优先级,我们不能说纬度比经度更重要,也不能说纬度比高程更重要。

  3. 当空间数据不是点,而是线(道路、地铁、河流等),面(行政区边界、建筑物等),B树怎么索引?对于面来说,它由一系列首尾相连的经纬度坐标点组成,一个面可能有成百上千个坐标,这时数据库怎么存储,B树怎么索引,这些都是问题。

既然传统的索引不能很好的索引空间数据,我们自然需要一种方法能对空间数据进行索引,即空间索引

实战

  1. SpringBoot + Redis 实现geo操作。

  2. 调用Java三方依赖判断两点距离

  3. 判断[3] 一个IP坐标是否在中国地图内,核心思想就是看点到线上的交点看是否在右边。具体看参考文档实战代码。

Reference

[1]http://geohash.gofreerange.com/

[2]http://www.geohash.cn/
[3]判断: 

https://www.geeksforgeeks.org/how-to-check-if-a-given-point-lies-inside-a-polygon/

[4]Java实现GPS范围查找: 

https://blog.csdn.net/pavel101/article/details/83585431
[5]浙大大佬通俗说GPS: 

https://www.cnblogs.com/LBSer/p/3403933.html
[6]SpringBoot+RedisGeo实战: 

https://download.csdn.net/download/qq_31821675/12630443
[7]Neo4j自带距离计算: 

http://we-yun.com/apoc/index34.html#_calculating_distance_between_locations


更多精彩推荐
☞华科出身,师从贾佳亚,从鹅厂到创业,90后如何登上胡润U30?
☞C++之父访谈录:我也没想到 C ++ 会这么成功!
☞腾讯 AI 医学进展破解“秃头”难题,登 Nature 子刊!
☞倪光南、求伯君“出山”:爱解 Bug、无惧“35岁魔咒”、编码之路痛并快乐!

☞饿了么技术往事
☞给大忙人们看的 Java NIO 极简教程
点分享点点赞点在看
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值