GeoHash
Redis 3.2版本后增加了地理位置模块,所以可以通过Redis来实现附近的XX这样的功能了。
用数据库来算附近的人
地图元素的位置数据使用二维经纬度表示,经度范围(-180, 180],纬度范围(-90, 90],纬度正负以赤道为界,北正南负,经度正负以本初子午线为界,东正西负。
当两个元素距离不是很远时,可直接用勾股定理算距离,但要注意的是经纬度坐标的密度不一样,需要按一定系数比加权求勾股定理。
如果要计算附近的人,也就是给定一个元素的坐标,然后计算其周围的元素,并按照距离排序,该如何做呢?
首先,不可能通过遍历来计算所有的元素和目标元素的距离,然后再进行排序,这样计算量太大,性能指标肯定无法满足。
常规做法是通过矩形区域来限定元素的数量,然后对区域内的元素进行全量距离计算再排序,这样可以减少计算量。
如何划分矩形区域呢?指定一个半径r,用一条SQL圈出来,如果对筛选出的结果不满意,增加或减少r。
select id from positions where x0-r < x < x0+r and y0-r < y < y0+r;
为了满足高性能的矩形区域算法,数据表需要再经纬度坐标加上双向复合索引(x,y),这样可以最大优化查询性能,但数据库查询性能毕竟有限,在高并发场合并不是一个很好的方案。
GeoHash算法
业界通用的地理位置距离排序算法是GeoHash算法,Redis也使用该算法。
GeoHash算法将二维的经纬度数据映射到一维整数,这样所有元素都映射到一条线上,距离靠近的二维坐标映射到一维后点之间的距离也会很近,要计算附近的人时,获取目标点附近的点就行了。
那么这个映射算法是怎样的呢?
它将整个地球看成一个二维平面,并划分成一系列的正方形方格,所有地图元素坐标都放在唯一的方格中,方格越小,坐标越精确。然后对这些方格进行整数编码,这个编码方法是base32,越靠近的方格,其整数编码也越接近。
在Redis内,经纬度使用52位的整数进行编码,放进zset中,zset的value是元素的key,score是GeoHash的52位整数。zset的score虽然是浮点数,但可以无损存储52位整数。
在查询时,通过zset的score排序就可以得到坐标附近的其他元素,将score还原成坐标值即可获得原始坐标。
Redis的Geo指令基本使用
Redis提供的Geo指令只有6个,必须注意的一点是,它就是一个普通的zset结构。
增加
geoadd [setname] [经度] [纬度] [名称]
,后面的三元组可以有多个,返回整数。
删除可通过zrem key member
来操作,Geo并没有提供单独的方法。
距离
geodist [setname] [名称1] [名称2] [单位]
,单位可以是km、m、ml、ft(千米、米、英里、尺),返回字串。
获取元素位置
geopos [setname] [名称]
,可以获取多个元素位置,返回列表,坐标会有轻微误差。
获取元素的hash值
geohash [setname] [名称]
,获取元素的经纬度编码字符串,base32编码,可以在http://geohash.org/${hash}中直接定位。
附近的公司
georadiusbymember
,该命令参数复杂。
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc # 20km以内最多3个元素按距离正向排序,不排除自身
1) "ireader"
2) "juejin"
3) "meituan"
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc # 20km以内最多3个元素按距离反向排序,排除自身
1) "jd"
2) "meituan"
3) "juejin"
另有三个可选参数,withcoord、withdist、withhash,其中withdist用于显示距离,即返回列表中包括距离值。
根据坐标查询附近的元素
georadius
,用法同georadiusbymember
,将名称更换为经纬度即可。
注意事项
使用Geo算法,他们将被放在一个zset中,在集群环境中会导致单个key数据过大,对集群迁移工作造成较大影响,集群环境中单个key对应数据量不应超过1M,否则会导致迁移卡顿,影响线上服务。
建议Geo的数据使用单独的Redis实例部署,不要使用集群环境,如果数据量太大,可对Geo的数据按国家、省、市、区拆分。