Redis3.2增加了GEO模块, 可以实现附件的人
地图的元素位置数据是使用二维的经纬度表示,当两个元素距离不是很远的时候可以使用勾股定理计算元素的距离。但是经纬度坐标的密度不一样,经度360,维度180。勾股定理计算方差的时候再求和时,需要按照一定的系数加权求和。
给一个坐标,计算附近的其他元素。按照距离排序
不能计算所有的元素和目标元素的距离然后再排序,计算量太大了。一般是通过矩阵来限定区域,对区域内的元素进行计算这样可以减少计算量。
可以指定一个半径,一条sql就可以查出来了
select id from positions where x0-r < x < x0+r and y0-r < y < y0+r
为了满足高性能矩阵算法,需要在经纬坐标 加上复合双向索引。优化查询效率。
但是在高并发的情况下,不是一个好的方案。
GEO
常用的算法是GeoHash算法。
GeoHash是将坐标映射到一维的整数,所有的元素都挂载到一条线上面,距离近的坐标映射到一维后点之间的距离也很接近,当要计算附件的元素的时候,直接取一维点附近的元素就可以了。
映射算法的具体实现就是,将地图元素坐标都放置到一个唯一格子中,格子越小位置越精准。然后对格子进行编码,越是靠近的格子编码越接近。
那是如何编码的呢?
简单的方案就是切蛋糕法,将整个蛋糕,二刀下去分为四块,每小块可以标记成00,01,10,11四个二进制整数,然后对每个小块继续二刀法分割,这样每个小小块就可以使用4bit 的二进制整数表示,然后继续分割,块越来越小,二进制整数越来越长,位置精准程度也越来越高。
编码过后,每个地图元素的坐标都是一个整数,通过整数可以还原坐标,整数越长,还原的坐标精准度损失越小。对于找附件的人,这点损失的精准度无大碍。
GeoHash 会继续对这个整数进行base32(09,az,去掉a,i,l,o)位的编码变成一个字符串。
在Redis中,经纬度使用52位整数进行编码,放到zset中,zset的value 是元素的key,score是GeoHash饿52位整数值,zset的score虽然是浮点型,但是存储52位的整数型,可以无损存储。
Geo基本命令
Geo只是普通的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
计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。
距离单位可以是 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
可以获取集合中任意元素的经纬度坐标,可以一次获取多个
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"
获取的经纬度坐标和 geoadd 进去的坐标有轻微的误差,原因是 geohash 对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于[附近的人」这种功能来说,这点误差根本不是事。
获取元素的hash值 geohash
获取元素的经纬度编码字符串,是base32位。
以使用这个编码值去 http://geohash.org/${hash}中进行直接定位,它是 geohash 的标准编码值。
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"
还可以通过坐标值来查找附件的人,参数和georadiusbymember 基本一致,除了将目标元素改成经纬度坐标值
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"
注意
在地图应用中,地图元素会有n条,如果使用redis的geo数据结构,他们都会放大一个zset集合中,在集群环境中,数据可能从一个节点迁移到另一个节点中,当单个key的数据过大的话,对迁移造成较大的影响。集群中,单个key最好不要超过1m,否则迁移会引起卡顿,影响线上服务。
建议geo的数据使用单独的redis进行部署。数据量过大,需要对geo数据进行拆分,按照国家,省,市拆分。