如何“摇一摇”——Redis实现“附近的人”的方法及其他

1、GEOHash——如何“摇一摇”

1-1、为什么需要GEOHash

现在很多APP都有“摇一摇”、“附近的人”、网约车离我有多远等类似的功能。

那就不可避免需要进行一系列的地理坐标转换为距离的计算。

获取地理坐标不难,只需要用户授权就能轻松获取到很精确的经纬度;

计算距离也不难:两点之间坐标差的平方相加,再开方即可(的markdown太差了,公式也不支持)。

问题在于,用户数量剧增以后(百万、千万、亿……),每次进行全量距离计算本身就变成了一个巨大的负担,大量乘方、开方这样的浮点运算,不管多么强壮的算力都会被压垮。

1-2、什么是GEOHash

GEOHash是解决这一问题的一种算法。

GEOHash的核心思想是将二维的经纬度转换成一维的字符串,这样无论使用何种方式存储用户的位置数据,都可以很方便地建立索引,大大简化计算。

GEOHash有三个主要特点:

  1. 字符串越长,表示的范围越精确。编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右。

  2. 字符串相似的表示距离相近,利用字符串的前缀匹配,可以查询附近的地理位置

  3. GEOHash编码后的字符串,可以反向解码出原来的经纬度。

1-3、GEOHash编码的简易原理

GEOHash的编码过程并不复杂,首先使用分形的思想对经纬度做处理(二维空间转换为分形维),转为二进制字符串;然后偶数位放经度,奇数位放纬度,得到一个新的二进制字符串;最后对该二进制字符串做Base32编码处理即可(或者使用Base64)。

但是“为什么”要这样编码,就涉及到计算机图形学的一些思想(GEOHash应用了Peano曲线),本篇侧重应用,原理的具体说明此处略去。

1-4、GEOHash的边缘问题

GEOHash编码时,实际上就是把所有区域分割成大小相同的矩形块(分得越细,字符串就越长,精度越精确)。

但是不管分割得怎么细,都是一个矩形区域,会出现区域内靠近边缘的某个点,与另一片区域中靠近边缘的点离得更近,但是被误判的情况。

毕竟同一片区域内的hash码是相同的,不管离得多远,都会被判定为离得最近。

解决方法是在查询时,除了使用定位点的GeoHash码进行匹配外,还要使用周围8个区域的GeoHash编码一并匹配。

2、Redis中GEOHash的应用

Redis 从3.2版本开始,追加了GEO数据类型用于存储用户信息,并且提供了各种用于地理计算的方法。

(BTW,对Redis GEO贡献最大的是其实是国内的开发者。也是因为我们对这个功能的需求最大)

下面是相关的全部命令列表:

GEOADD:增加地理位置的坐标(支持批量操作)

时间复杂度O(log(N))

GEOADD key longitude latitude member [longitude latitude member ...]

  • key标识一个地理位置的集合。
  • longitude是经度,latitude是纬度。
  • member是该地理位置的名称(自定义)。
应用举例:

geoadd cities 121.47 31.23 shanghai 

geoadd cities 116.40 39.90 beijing 118.78 32.07 nanjing
GEOPOS:获取地理位置的坐标(支持批量操作)

时间复杂度O(log(N))

GEOPOS key member [member ...]

GEODIST:获取两个地理位置的距离

时间复杂度O(log(N))

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

单位可以指定为以下四种类型:

  • m:米,距离单位默认为米,不传递该参数则单位为米
  • km:公里
  • mi:英里
  • ft:英尺
应用举例:
geodist cities beijing shanghai
GEORADIUS:根据给定地理位置坐标获取指定范围内的地理位置集合

时间复杂度O(N+log(M)),N为指定半径范围内的元素个数,M为要返回的个数

GEORADIUS key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]

  • longitude是经度,latitude是纬度。
  • radius表示范围距离。
  • 距离单位可以为m|km|ft|mi
  • WITHCOORD:传入WITHCOORD参数,则返回结果会带上匹配位置的经纬度。
  • WITHDIST:传入WITHDIST参数,则返回结果会带上匹配位置与给定地理位置的距离。
  • ASC|DESC:默认结果是未排序的,传入ASC为从近到远排序,传入DESC为从远到近排序。
  • WITHHASH:传入WITHHASH参数,则返回结果会带上匹配位置的hash值。
  • COUNT count:传入COUNT参数,可以返回指定数量的结果。
GEORADIUSBYMEMBER:根据给定地理位置获取指定范围内的地理位置集合

时间复杂度O(N+log(M)),N为指定半径范围内的元素个数,M为要返回的个数

GEORADIUSBYMEMBER key member radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]

GEORADIUS 命令的参数是坐标,而本命令的参数是地理位置名称,其他无区别。

应用举例:
georadiusbymember cities shanghai 50 km
GEOHASH:获取地理位置的GEOHash值(支持批量操作)

时间复杂度O(log(N))

GEOHASH key member [member ...]

3、关于Redis中GEO的一些说明

redis使用的GEOHash编码长度为26位,可以精确到0.59m的精度。

Redis中的GEO核心主要是两点:

  1. 使用GEOHash保存地理位置的坐标
  2. 使用有序集合(ZSET)保存地理位置的集合

内部的实际存储使用的是ZSET,进一步说明如下:

  • GEO命令中的key就是ZSET的名字

  • Redis没有提供地理位置的删除命令。可以利用ZSET的ZREM命令进行删除: zrem cities shanghai

  • 执行GEOADD命令时,会先按照标准的GEOHash计算方法计算hash值,然后将地理位置名称(member)作为集合的member,将GEOHash值作为score,执行ZADD命令插入到ZSET中

  • GEORADIUS和GEORADIUSBYMEMBER。内部实现实际上就是先执行ZRANGE命令,然后判断结果是否符合命令中指定的距离即可。另外就是这两个命令已经自行解决了GEOHash的“边缘问题”,会自动关联周围的八片区域,无需额外处理。

4、Redis以外的GEOHash实现

4-1、MySQL

MySQL从 5.7.4 labs 版本开始增加了对于空间索引的支持,通过R树实现。

如果不具备升级MySQL的条件,就需要增加类似 geo_hash 的字段,追加地理位置时手动计算hash码,搜索附近的人时手动解决GEOHash的边缘问题,从算力和复杂度上来说都不能令人满意。

4-2、MongoDB + 2d索引

MongoDB主要是通过它的两种地理空间索引 2dsphere 和 2d来实现地理位置的操作。

两种索引的底层依然是基于Geohash,但与通用的Geohash还有一些不同。

2dsphere 索引仅支持球形表面的几何形状查询。

2d 索引支持平面几何形状和一些球形查询。虽然2d 索引支持某些球形查询,但 2d 索引对这些球形查询时,可能会出错。所以球形查询尽量选择 2dsphere索引。

尽管两种索引的方式不同,但只要坐标跨度不太大,计算出的距离误差几乎可以忽略不计。

对经纬度数据创建2d索引(db.coll.createIndex)以后,只要使用geoNear命令就可以实现附近的人的功能了。

你可能感兴趣的:(如何“摇一摇”——Redis实现“附近的人”的方法及其他)