Redis计算地理位置距离-GeoHash

时间:2023-03-09 03:14:50
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度。

勾股定律计算平 方差时之后再求和时,需要按一定的系数比加权求和。

现在,如果要计算「附近的人」,也就是给定一个元素的坐标,然后计算这个坐标附近的其它元素,按照距离进行排序,该如何下手?

Redis计算地理位置距离-GeoHash

如果现在元素的经纬度坐标使用关系数据库 (元素 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:> geoadd company 116.48105 39.996794 juejin (integer)
127.0.0.1:> geoadd company 116.514203 39.905409 ireader (integer)
127.0.0.1:> geoadd company 116.489033 40.007669 meituan
(integer)
127.0.0.1:> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi (integer)

计算距离

geodist 指令可以用来计算两个元素之间的距离,携带集合名称、 个名称和距离单位。
127.0.0.1:> geodist company juejin ireader km "10.5501"
127.0.0.1:> geodist company juejin meituan km "1.3878"
127.0.0.1:> geodist company juejin jd km "24.2739"
127.0.0.1:> geodist company juejin xiaomi km "12.9606"
127.0.0.1:> geodist company juejin juejin km "0.0000"

我们可以看到掘金离美团最近,因为它们都在望京。距离单位可以是 m、km、ml、ft, 分别代表米、千米、英里和尺。

获取元素位置

127.0.0.1:> geopos company juejin
) ) "116.48104995489120483"
) "39.99679348858259686"
127.0.0.1:> geopos company ireader
) ) "116.5142020583152771"
) "39.90540918662494363"
127.0.0.1:> geopos company juejin ireader
) ) "116.48104995489120483"
) "39.99679348858259686"
) ) "116.5142020583152771"
) "39.90540918662494363"

我们观察到获取的经纬度坐标和 geoadd 进去的坐标有轻微的误差,原因是 geohash 对 二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于「附 近的人」这种功能来说,这点误差根本不是事。

获取元素的 hash 值

geohash 可以获取元素的经纬度编码字符串,上面已经提到,它是 base32 编码。 你可 以使用这个编码值去 http://geohash.org/${hash}中进行直接定位,它是 geohash 的标准编码 值。

127.0.0.1:> geohash company ireader
) "wx4g52e1ce0"
127.0.0.1:> geohash company juejin
) "wx4gd94yjn0"

geohash的思想是将二维的经纬度转换成一维的字符串,geohash有以下三个特点:
1、字符串越长,表示的范围越精确。编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右。
2、字符串相似的表示距离相近,利用字符串的前缀匹配,可以查询附近的地理位置。这样就实现了快速查询某个坐标附近的地理位置。
3、geohash计算的字符串,可以反向解码出原来的经纬度。

可以打开网站查看地理位置是否正确 http://geohash.org/wx4g52e1ce0

附近的公司

georadiusbymember 指令是最为关键的指令,它可以用来查询指定元素附近的其它元 素,它的参数非常复杂。

# 范围  公里以内最多  个元素按距离正排,它不会排除自身
127.0.0.1:> georadiusbymember company ireader km count asc
) "ireader"
) "juejin"
) "meituan"
# 范围 公里以内最多 个元素按距离倒排
127.0.0.1:> georadiusbymember company ireader km count desc
) "jd"
) "meituan"
) "juejin"
# 三个可选参数 withcoord withdist withhash 用来携带附加参数
# withdist 很有用,它可以用来显示距离
127.0.0.1:> georadiusbymember company ireader km withcoord withdist withhash count asc
) ) "ireader"
) "0.0000"
) (integer)
) ) "116.5142020583152771"
) "39.90540918662494363"
) ) "ireaderexit"
) "0.0000"
) (integer)
) ) "116.5142020583152771"
) "39.90540918662494363"
) ) "meituan"
) "11.5748"
) (integer)
) ) "116.48903220891952515"
) "40.00766997707732031"

除了 georadiusbymember 指令根据元素查询附近的元素,Redis 还提供了根据坐标值来 查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近 的餐馆」等。它的参数和 georadiusbymember 基本一致,除了将目标元素改成经纬度坐标 值。

127.0.0.1:> georadius company 116.514202 39.905409  km withdist count  asc
) ) "ireader"
) "0.0000"
) ) "ireaderexit"
) "0.0000"
) ) "meituan"
) "11.5748"

在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合 可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成 较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现 卡顿现象,影响线上服务的正常运行。

所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。

如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按 市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。

不错的文章

https://blog.****.net/weixin_36135773/article/details/78800215