Redis奇幻之旅(二)4. GeoHash

4. GeoHash

​ 在日常生活中我们已经习惯了“附近的人”、“附近的饭店”、“附近的商场”等等功能的使用,但是我们似乎很少去思考这个功能是怎么实现的呢?

​ 假如我们有两个点的经纬度(a, b)和(c, d)要算他们之间的距离可以用勾股定理直接得出答案,只要这两个人不是隔得很远,那么将地球当成是平面的似乎也没什么不妥。于是我们将每个人的经纬度数据都存到了mysql中。这时,如果让我们去计算“附近的人”,我们先想到的是将自己的经纬度和库里所有的经纬度进行对比然后按距离排序。但是这种遍历式的操作肯定是不能过性能评审的。于是我们继续思考,既然我们只需要附近的这部分数据,那是不是可以直接定出经纬度的范围,然后将范围内的数据进行排序即可。假设我们有个半径r,那么这个范围应该就被限制在(a-r, b+r)、(a+r, b+r)、(a-r, b-r)、(a+r, b-r)这四个坐标的范围之内。于是我们查询sql中就会添加上where条件,这时候我们要算距离的数据就大大减少了。但是如果这个请求量级比较大,数据库的性能可能就跟不上了。

4.1 GeoHash算法

​ 为了更好的解决类似“附近的人”这种需求,人们提出了GeoHash算法。GeoHash算法使用二分切割法,将二维的经纬度数据映射到一维的整数,这样所有的元素都将挂载到一条线上,地球纬度区间是[-90,90],经度区间是[-180,180]。 将它展开想象长一个矩形,在这个矩形上进行遍历。每遍历到一个点,就给它标注一个值,比如00、01、10、11,随着二进制数字增加,相当于遍历面上不同的位置。

​ 当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线。当我们递归的将各个块分解成更小的子块时可以标识更小的空间范围(如上图二中所示),如从0000开始到1111结束编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为Peano空间填充曲线。对大部分而言,编码相似的距离也相近,但Peano空间填充曲线最大的缺点就是突变性,有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大。

​ GeoHash算法通过将节点数据编码转换成一维数据,(base 32 和 base 36) 会将落到网格中的二进制数据编码成字符串,再快速查找出需要的数据。

​ 举个例子:我们有一个坐标的经纬度,经过一系列的二分切割获取到经纬度产生的编码。假设纬度产生的编码为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。同理,将编码转换成经纬度的解码算法与之相反。

4.2 Redis的GeoHash

​ Redis在3.2版本之后就加入了Geo模块,源码在src/geo中。当我们往Redis中加入经度和纬度,Redis会通过Geohash算法将经纬度映射成一个一维52位的整数,然后使用zset结构来存储数据,zset的value是元素的key,score是这个52位的整数。

​ 由于Redis的GeoHash底层是拿zset存储的,所以GeoHash的数据也可以用zset的命令来操作。需要注意的是,当我们在使用GeoHash的时候极容易产生大key,因为我们一个zset中放入了大量的经纬度数据。在日常开发的时候,我们可以在业务层面提前拆分一下数据,好让zset的数据不至于过大。

4.3 Geo相关命令

  • geoadd

geoadd key longitude latitude member [longitude latitude member ...]

用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中

  • geopos

    geopos key member [member ...]

    用于从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。

  • geodist

    geodist key member1 member2 [m|km|ft|mi]

    用于返回两个给定位置之间的距离。

    最后一个距离单位参数说明:

    m :米,默认单位。

    km :千米。

    mi :英里。

    ft :英尺。

  • georadius、georadiusbymember

    georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
    georadiusbymember key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

    georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

    georadiusbymember 和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 georadiusbymember 的中心点是由给定的位置元素决定的, 而不是使用经度和纬度来决定中心点。

    m :米,默认单位。
    km :千米。
    mi :英里。
    ft :英尺。
    WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
    WITHCOORD: 将位置元素的经度和维度也一并返回。
    WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
    COUNT 限定返回的记录数。
    ASC: 查找结果根据距离从近到远排序。
    DESC: 查找结果根据从远到近排序。

  • geohash

    geohash key member [member ...]

    用于获取一个或多个位置元素的 geohash 值。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容