Redis计算地理位置距离-GeoHash
Redis 在 3.2 版本以后增加了地理位置 GEO 模块,意味着我们可以使用 Redis 来实现摩拜单车「附近的 Mobike」、美团和饿了么「附近的餐馆」这样的功能了。
地图元素的位置数据使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],纬度正负以赤道为界,北正南负,经度正负以本初子午线 (英国格林尼治天文台) 为 界,东正西负。比如掘金办公室在望京 SOHO,它的经纬度坐标是 (116.48105,39.996794), 都是正数,因为中国位于东北半球。
当两个元素的距离不是很远时,可以直接使用勾股定理就能算得元素之间的距离。我们 平时使用的「附近的人」的功能,元素距离都不是很大,勾股定理算距离足矣。不过需要注 意的是,经纬度坐标的密度不一样 (经度总共 360 度,纬度总共 180 度)
效经度为-180至180度。
有效纬度为-85.05112878至85.05112878度。
勾股定律计算平 方差时之后再求和时,需要按一定的系数比加权求和。
现在,如果要计算「附近的人」,也就是给定一个元素的坐标,然后计算这个坐标附近的其它元素,按照距离进行排序,该如何下手?
如果现在元素的经纬度坐标使用关系数据库 (元素 id, 经度 x, 纬度 y) 存储,你该如何 计算?
GeoHash 算法
Redis 的 GEO 特性将在 Redis 3.2 版本释出, 这个功能可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作
将指定的地理空间项目(纬度,经度,名称)添加到指定的键。数据作为排序集存储到密钥中,使得可以使用GEORADIUS或GEORADIUSBYMEMBER命令使用半径查询稍后检索项目。
注意:没有GEODEL命令,可以使用ZREM来删除元素。地理索引结构只是一个排序集。
zrem company juejin //company相当于一个地理存储空间, juejin是一个地名
Redis GEO实现主要包含了以下两项技术:
1、使用geohash保存地理位置的坐标。
2、使用有序集合(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 个名称和距离单位。
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"
我们可以看到掘金离美团最近,因为它们都在望京。距离单位可以是 m、km、ml、ft, 分别代表米、千米、英里和尺。
获取元素位置(获取经纬度)
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"
geohash的思想是将二维的经纬度转换成一维的字符串,geohash有以下三个特点:
1、字符串越长,表示的范围越精确。编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右。
2、字符串相似的表示距离相近,利用字符串的前缀匹配,可以查询附近的地理位置。这样就实现了快速查询某个坐标附近的地理位置。
3、geohash计算的字符串,可以反向解码出原来的经纬度。
可以打开网站查看地理位置是否正确 http://geohash.org/wx4g52e1ce0
附近的公司
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) "ireaderexit"
2) "0.0000"
3) (integer) 4069886008361398
4) 1) "116.5142020583152771"
2) "39.90540918662494363"
3) 1) "meituan"
2) "11.5748"
3) (integer) 4069887179083478
4) 1) "116.48903220891952515"
2) "40.00766997707732031"
除了 georadiusbymember 指令根据元素查询附近的元素,Redis 还提供了根据坐标值来 查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近 的餐馆」等。它的参数和 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) "ireaderexit"
2) "0.0000"
3) 1) "meituan"
2) "11.5748"
在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合 可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成 较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现 卡顿现象,影响线上服务的正常运行。
所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。
如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按 市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。
命令练习
//geoadd 用于将一个或者多个经纬度、位置名称添加到指定key中
127.0.0.1:6379> geoadd city 116.403963 39.915119 "天安门" (integer) 1 127.0.0.1:6379> geoadd city 116.403414 39.924091 "故宫" 116.024067 40.362639 "长城" (integer) 2 127.0.0.1:6379> type city zset 127.0.0.1:6379> zrange city 0 -1 1) "\xe5\xa4\xa9\xe5\xae\x89\xe9\x97\xa8" 2) "\xe6\x95\x85\xe5\xae\xab" 3) "\xe9\x95\xbf\xe5\x9f\x8e" 127.0.0.1:6379>
//GEOPOS用于从给定的key里返回所有指定名称的位置经纬度,不存在返回nil
127.0.0.1:6379> GEOPOS city 天安门 故宫 1) 1) "116.40396326780319214" 2) "39.91511970338637383" 2) 1) "116.40341609716415405" 2) "39.92409008156928252" 127.0.0.1:6379>
//geohash用于获取一个或者多个位置的hash值 127.0.0.1:6379> GEOHASH city 天安门 故宫 长城 1) "wx4g0f6f2v0" 2) "wx4g0gfqsj0" 3) "wx4t85y1kt0"
//距离单位可以是 m、km、ml、ft, 分别代表米、千米、英里和尺。 127.0.0.1:6379> GEODIST city 天安门 长城 km "59.3390" 127.0.0.1:6379> GEODIST city 天安门 长城 m "59338.9814" 127.0.0.1:6379> GEODIST city 天安门 长城 m [m|km|ft|mi]
127.0.0.1:6379> GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 1) 1) "天安门" 2) "1.2016" 3) 1) "116.40396326780319214" 2) "39.91511970338637383" 2) 1) "故宫" 2) "1.6470" 3) 1) "116.40341609716415405" 2) "39.92409008156928252"
//找出指定范围内的元素,中心点是由给定的位置元素决定 127.0.0.1:6379> GEORADIUSbymember city 天安门 10 km withdist withcoord count 10 1) 1) "天安门" 2) "0.0000" 3) 1) "116.40396326780319214" 2) "39.91511970338637383" 2) 1) "故宫" 2) "0.9988" 3) 1) "116.40341609716415405" 2) "39.92409008156928252"