Redis原理及实践之GeoHash

2. 地理位置距离排序算法(GeoHash)

  • GeoHash算法思想
    GeoHash算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离会很接近。当我们想要计算附近的人时,首先将目标位置映射到这条线上,然后在这条一维的线上获取附近的点就ok了。
    那这个映射算法具体是怎样的呢?它将整个地球看成一个二维平面,然后划分成了一系列正方形的方格,就好比围棋棋盘。所有的地图元素坐标都将放置于唯一的方格中。方格越小,坐标越精确。然后对这些方格进行整数编码,越是靠近的方格编码越是接近。那如何编码呢?一个最简单的方案就是切蛋糕法。设想一个正方形的蛋糕摆在你面前,二刀下去均分分成四块小正方形,这四个小正方形可以分别标记为 00,01,10,11 四个二进制整数。然后对每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用 4bit 的二进制整数予以表示。然后继续切下去,正方形就会越来越小,二进制整数也会越来越长,精确度就会越来越高。


    Redis原理及实践之GeoHash_第1张图片
    二分切割法

    编码之后,每个地图元素的坐标都将变成一个整数,通过这个整数可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程度就越小。对于「附近的人」这个功能而言,损失的一点精确度可以忽略不计。

GeoHash算法会对上述编码的整数继续做一次base32编码(0 ~ 9,a ~ z)变成一个字符串。Redis中经纬度使用52位的整数进行编码,放进zset中,zset的value元素是key,score是GeoHash的52位整数值。在使用Redis进行Geo查询时,其内部对应的操作其实只是zset(skiplist)的操作。通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标

总之,Redis中处理这些地理位置坐标点的思想是: 二维平面坐标点 --> 一维整数编码值 --> zset(score为编码值) --> zrangebyrank(获取score相近的元素)、zrangebyscore --> 通过score(整数编码值)反解坐标点 --> 附近点的地理位置坐标。

  • 推荐一篇牛逼的讲解高效多维空间点索引算法文章:https://halfrost.com/go_spatial_search/
  • Redis的Geo指令基本使用
    Redis Geo指令只有6个,使用时务必再次想起,它只是一个普通的zset结构。
    • geoadd : (纬度、经度、名称)三元组
    127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin
    (integer) 1
    127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader
    (integer) 1
    127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan
    (integer) 1
    127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400           xiaomi
    (integer) 2...
    
    • geodist : 计算两个元素之间的距离
    // 例如,计算集合company中juejin和ireader两个元素之间的距离,单位为km
    // 其中,距离单位可以是m、km、ml、ft,分别代表米、千米、英里和尺
    127.0.0.1:6379> geodist company juejin ireader km
    "10.5501"
    127.0.0.1:6379> geodist company juejin meituan km
    "1.3878"
     127.0.0.1:6379> geodist company juejin jd km
     "24.2739"
     127.0.0.1:6379> geodist company juejin xiaomi km
     "12.9606"
     127.0.0.1:6379> geodist company juejin juejin km
      "0.0000"...
    
    • geopos : 获取集合中任意元素的经纬度坐标,可以一次获取多个
    // 获取company集合中juejin元素的经纬度坐标
    127.0.0.1:6379> geopos company juejin
    1) 1) "116.48104995489120483"
       2) "39.99679348858259686"
    127.0.0.1:6379> geopos company ireader
    1) 1) "116.5142020583152771"
       2) "39.90540918662494363"
    127.0.0.1:6379> geopos company juejin ireader  // 同时获取多个元素的经纬度坐标
    1) 1) "116.48104995489120483"
       2) "39.99679348858259686"
    2) 1) "116.5142020583152771"
       2) "39.90540918662494363"...
    
    注意:GeoHash对二维经纬度坐标进行一维映射是有损的,通过映射再还原回的经纬度坐标和原始输入的经纬度坐标存在一定的误差。
    • geohash : 获取元素经纬度坐标经过geohash算法生成的base32编码值
    127.0.0.1:6379> geohash company ireader
    1) "wx4g52e1ce0"
    127.0.0.1:6379> geohash company juejin
    1) "wx4gd94yjn0"
    
    • georadiusbymember : 查询指定元素附近的其它元素
    # 根据元素查找附近的元素
    # 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身
    127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc
    1) "ireader"
    2) "juejin"
    3) "meituan"
    # 范围 20 公里以内最多 3 个元素按距离倒排
    127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc
    1) "jd"
    2) "meituan"
    3) "juejin"
    # 三个可选参数 withcoord withdist withhash 用来携带附加参数
    # withdist 很有用,它可以用来显示距离
    127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist   withhash count 3 asc
    1) 1) "ireader"
       2) "0.0000"
       3) (integer) 4069886008361398
       4) 1) "116.5142020583152771"
          2) "39.90540918662494363"
    2) 1) "juejin"
       2) "10.5501"
       3) (integer) 4069887154388167
       4) 1) "116.48104995489120483"
          2) "39.99679348858259686"
    3) 1) "meituan"
       2) "11.5748"
       3) (integer) 4069887179083478
       4) 1) "116.48903220891952515"
          2) "40.00766997707732031"...
    
    • georadius
    # 根据坐标点查找附近位置的元素
    127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc
    1) 1) "ireader"
       2) "0.0000"
    2) 1) "juejin"
       2) "10.5501"
    3) 1) "meituan"
       2) "11.5748"...
    

Redis Geo使用时的注意事项
在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。
所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。
如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。(注意:zset集合大小,进行合适地切分)

一张图搞懂GeoHash与Z阶曲线的关系:


Redis原理及实践之GeoHash_第2张图片
image.png

GeoHash算法的优缺点:

  • 优点
    • GeoHash利用Z阶曲线进行编码,Z阶曲线可以将二维或更多维的所有点都转换成一阶曲线。
    • 地理位置坐标点通过编码转化成一维值,利用BST、B树、SkipList等,均可进行范围搜索。因此利用GeoHash算法查找邻近点比较快。
  • 缺点
    • 在拥有局部保序性的同时,具有突变性。导致一些邻近点真实并不是距离较近的点。

你可能感兴趣的:(Redis原理及实践之GeoHash)