Redis的GEO
Redis 在 3.2 版本以后增加了地理位置 GEO 模块,意味着我们可以使用 Redis 来实现摩拜单车「附近的 Mobike」、美团和饿了么「附近的餐馆」这样的功能了。
用数据库来算附近的人
地图元素的位置数据使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90,90],纬度正负以赤道为界,北正南负,经度正负以本初子午线 (英国格林尼治天文台) 为
界,东正西负。比如掘金办公室在望京 SOHO,它的经纬度坐标是 (116.48105,39.996794),都是正数,因为中国位于东北半球。
当两个元素的距离不是很远时,可以直接使用勾股定理就能算得元素之间的距离。我们平时使用的「附近的人」的功能,元素距离都不是很大,勾股定理算距离足矣。不过需要注
意的是,经纬度坐标的密度不一样 (经度总共 360 度,纬度总共 180 度),勾股定律计算平方差时之后再求和时,需要按一定的系数比加权求和。
现在,如果要计算「附近的人」,也就是给定一个元素的坐标,然后计算这个坐标附近的其它元素,按照距离进行排序,该如何下手?
如果现在元素的经纬度坐标使用关系数据库 (元素 id, 经度 x, 纬度 y) 存储,你该如何计算?首先,你不可能通过遍历来计算所有的元素和目标元素的距离然后再进行排序,这个计
算量太大了,性能指标肯定无法满足。一般的方法都是通过矩形区域来限定元素的数量,然后对区域内的元素进行全量距离计算再排序。这样可以明显减少计算量。如何划分矩形区域
呢?可以指定一个半径 r,使用一条 SQL 就可以圈出来。当用户对筛出来的结果不满意,那就扩大半径继续筛选。
select id from positions where x0-r < x < x0+r and y0-r < y < y0+r
为了满足高性能的矩形区域算法,数据表需要在经纬度坐标加上双向复合索引 (x, y),这样可以最大优化查询性能。
但是数据库查询性能毕竟有限,如果「附近的人」查询请求非常多,在高并发场合,这可能并不是一个很好的方案。
GeoHash 算法
业界比较通用的地理位置距离排序算法是 GeoHash 算法,Redis 也使用 GeoHash 算法。GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一
条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。
那这个映射算法具体是怎样的呢?它将整个地球看成一个二维平面,然后划分成了一系列正方形的方格,就好比围棋棋盘。所有的地图元素坐标都将放置于唯一的方格中。方格越
小,坐标越精确。然后对这些方格进行整数编码,越是靠近的方格编码越是接近。那如何编码呢?一个最简单的方案就是切蛋糕法。设想一个正方形的蛋糕摆在你面前,二刀下去均分
分成四块小正方形,这四个小正方形可以分别标记为 00,01,10,11 四个二进制整数。然后对每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用 4bit 的二进制整数
予以表示。然后继续切下去,正方形就会越来越小,二进制整数也会越来越长,精确度就会越来越高。
上面的例子中使用的是二刀法,真实算法中还会有很多其它刀法,最终编码出来的整数数字也都不一样。
编码之后,每个地图元素的坐标都将变成一个整数,通过这个整数可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程度就越小。对于「附近的人」这个功能而言,损
失的一点精确度可以忽略不计。
GeoHash 算法会继续对这个整数做一次 base32 编码 (0-9,a-z 去掉 a,i,l,o 四个字母) 变成一个字符串。在 Redis 里面,经纬度使用 52 位的整数进行编码,放进了 zset 里面,zset
的 value 是元素的 key,score 是 GeoHash 的 52 位整数值。zset 的 score 虽然是浮点数,但是对于 52 位的整数值,它可以无损存储。
在使用 Redis 进行 Geo 查询时,我们要时刻想到它的内部结构实际上只是一个zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一
些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标。
Redis 的 Geo 指令基本使用
命令演示如下:
root@d4cad7fb69c2:/data# redis-cli 127.0.0.1:6379> GEOADD geos 1 1 a (integer) 1 127.0.0.1:6379> GEOADD geos 2 2 b (integer) 1 127.0.0.1:6379> GEOPOS geos a 1) 1) "0.99999994039535522" 2) "0.99999945914297683" 127.0.0.1:6379> GEODIST geos a b "157270.0561" 127.0.0.1:6379> GEODIST geos a b m "157270.0561" 127.0.0.1:6379> GEODIST geos a b km "157.2701" 127.0.0.1:6379> geoadd key longitude latitude member [longitude latitude member 127.0.0.1:6379> geoadd key longitude latitude member [longitude latitude member 127.0.0.1:6379> geoadd key longitude latitude member [longitude latitude member 127.0.0.1:6379> geoadd key longitude latitude member [longitude latitude member 127.0.0.1:6379> geoadd key longitude latitude member [longitude latitude member 127.0.0.1:6379> geoadd geos 1 1 1,1 (integer) 1 127.0.0.1:6379> geoadd geos 1 2 1,2 (integer) 1 127.0.0.1:6379> geoadd geos 1 3 1,3 (integer) 1 127.0.0.1:6379> geoadd geos 2 3 2,3 (integer) 1 127.0.0.1:6379> geoadd geos 2 2 2,2 (integer) 1 127.0.0.1:6379> geoadd geos 2 1 2,1 (integer) 1 127.0.0.1:6379> geoadd geos 3 1 3,1 (integer) 1 127.0.0.1:6379> geoadd geos 3 2 3,2 (integer) 1 127.0.0.1:6379> geoadd geos 3 3 3,3 (integer) 1 127.0.0.1:6379> geoadd geos 5 5 5,5 (integer) 1127.0.0.1:6379> GEORADIUSBYMEMBER geos 2,2 180 km 1) "1,1" 2) "a" 3) "2,1" 4) "1,2" 5) "2,2" 6) "b" 7) "3,1" 8) "3,2" 9) "1,3" 10) "2,3" 11) "3,3" 127.0.0.1:6379> GEORADIUSBYMEMBER geos 2,2 120 km 1) "1,2" 2) "2,2" 3) "b" 4) "2,3" 5) "2,1" 6) "3,2" 127.0.0.1:6379> GEORADIUS geos 1.5 1.5 100km (error) ERR wrong number of arguments for 'georadius' command 127.0.0.1:6379> GEORADIUS geos 1.5 1.5 100 km 1) "1,2" 2) "2,2" 3) "b" 4) "1,1" 5) "a" 6) "2,1" 127.0.0.1:6379> geohash geos 3,3 1) "s0d1h60s300" 127.0.0.1:6379> GEORADIUSBYMEMBER geos 2,2 120 km count 3 desc 1) "2,1" 2) "2,3" 3) "1,2" 127.0.0.1:6379>