Redis GEO 地理位置的使用与原理解析以及Java实现GEOHash算法

Posted 刘Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis GEO 地理位置的使用与原理解析以及Java实现GEOHash算法相关的知识,希望对你有一定的参考价值。

详细介绍了Redis GEO存储地理位置信息的使用方式以及基本原理,基于Java如何实现GEOHash算法。

Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。

Redis GEO可被用来计算两个经纬度点位之间的物理距离,常见的应用就是“附近的人”的功能,摇一摇附近的人功能,或者外卖中的骑手等距客户多少米的功能,或者周边商家、车辆功能等等,需要计算真实距离的场景都可以使用。

在 Redis 6.2.0的版本中提供了8个相关的操作命令,而且都是很简单的。

1 GEOADD添加坐标

GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]

将指定的一个或者多个地理空间项(经度、纬度、名称)添加到指定的key。数据作为排序集存储在key中,这样就可以使用 GEOSEARCH 和GEORADIUSBYMEMBER命令查询这些项目。

该命令采用标准格式 x,y 的参数,因此必须在纬度之前指定经度。可编入索引的坐标有限制:非常靠近极点的区域不可编入索引。

  1. 有效经度为 -180 到 180 度。
  2. 有效纬度是从 -85.05112878 到 85.05112878 度。

返回新添加到键里面的空间元素数量,不包括那些已经存在但是被更新的元素。

当用户尝试索引指定范围之外的坐标时,该命令将报告错误。这里没有 GEODEL 命令,因为地理索引结构只是一个Sorted Set,因此可以使用 ZREM 删除元素。

如下案例,我们尝试添加几个地理位置:故宫、天安门、日坛公园、天坛公园、北京站(https://jingweidu.bmcx.com/):

127.0.0.1:6379> GEOADD test 116.397128 39.916527 gugong
(integer) 1
127.0.0.1:6379> GEOADD test 116.39798630688475 39.90382054860711 tiananmen
(integer) 1
127.0.0.1:6379> GEOADD test 116.44381989453123 39.915605367786505 ritangongyuan
(integer) 1
127.0.0.1:6379> GEOADD test 116.41077507946775 39.88208903170563 tiantangongyuan
(integer) 1
127.0.0.1:6379> GEOADD test 116.42699707958982 39.90273413640524 beijingzhan
(integer) 1

2 GEOPOS获取坐标

GEOPOS key member [member ...]

返回由key的Sorted set表示的地理空间索引的要给或者多个指定成员的位置(经度、纬度)。

当通过 GEOADD 填充地理空间索引时,坐标被转换为 52 位 geohash,因此返回的坐标可能不完全是用于添加元素的坐标,可能会有小精度错误,也就是说值不一定是插入值。

获取故宫、北京站和日坛公园的地理坐标:

127.0.0.1:6379> GEOPOS test gugong
1) 1) "116.39712899923324585"
   2) "39.91652647362980844"
127.0.0.1:6379> GEOPOS test beijingzhan ritangongyuan
1) 1) "116.4269980788230896"
   2) "39.90273505580184832"
2) 1) "116.44382089376449585"
   2) "39.91560636984896604"

3 GEODIST计算距离

GEODIST key member1 member2 [m|km|ft|mi]  

返回由Sorted set表示的地理空间索引中两个成员之间的距离,这个命令非常有用。

如果缺少一个或两个成员,则该命令返回 NULL。

单位必须是以下之一,默认为m(米),还有km(千米),mi(英里),ft(英尺)。

该命令假设地球是一个完美的球体来计算距离,因此在边缘情况下可能会出现高达 0.5% 的误差。

案例,计算故宫和北京站的距离:

127.0.0.1:6379> GEODIST test gugong beijingzhan
"2974.4056"

结果约2.974公里,差不多(在线经纬度距离计算https://www.hhlink.com/%E7%BB%8F%E7%BA%AC%E5%BA%A6):

4 GEOHASH获取geohash

返回有效的 Geohash 字符串,表示一个或多个元素在表示地理空间索引的Sortedset中的位置(其中元素是使用 GEOADD 添加的)。

通常 Redis 使用 Geohash 技术的变体来表示元素的位置,其中位置使用 52 位无符号二进制整数进行编码。与标准相比,编码也不同,因为在编码和解码过程中使用的初始最小和最大坐标不同。但是,此命令以 Wikipedia 文章(https://en.wikipedia.org/wiki/Geohash)中所述的字符串形式返回标准 Geohash,并与 http://geohash.org/网站兼容。

也就是说,我们获取到某个坐标的hash值之后,就能直接去http://geohash.org/${hash}网站定位它。

该命令返回 11 个字符的 Geohash 字符串,因此与 Redis 内部 52 位表示相比没有精度损失。返回的 Geohashes 具有以下属性:

  1. 它们可以从右边删除字符以缩短,这会使得定位失去精度,但仍会指向同一区域。
  2. 可以在 geohash.org的URL中使用它们,例如http://geohash.org/%3Cgeohash-string>。
  3. 具有相似前缀的字符串在附近,但反之则不然,具有不同前缀的字符串也可能在附近。

我们获取故宫的坐标的hash:

127.0.0.1:6379> GEOHASH test gugong
1) "wx4g0dtf9e0"

去定位一下http://geohash.org/wx4g0dtf96x:

有时候下面的小图加载不出来,点击图中的“google”试试:

确实定位是在故宫。

5 GEORADIUSBYMEMBER成员半径搜索

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]

GEORADIUSBYMEMBER以指定名字的成员的位置用作查询的中心,返回距中心的最大距离(半径)指定的区域的边界内的其他成员。

此命令的常见用例是检索指定点附近不超过给定米数(或其他单位)的地理空间项目。例如,这允许向移动用户推荐附近地点的应用程序,比如附近的餐馆、附近的人等。

半径单位必须是以下之一,默认为m(米),还有km(千米),mi(英里),ft(英尺)。

该命令可选地使用以下参数,用于返回更多的附加信息:

  1. WITHDIST: 还要返回距离指定中心的距离,距离以与指定为命令的半径参数的单位相同的单位返回。
  2. WITHCOORD:还返回匹配项目的经度,纬度坐标。
  3. WITHHASH:还以 52 位无符号二进制数的十进制整数的形式返回项目的原始 geohash 编码的sorted set score,这仅对低级黑客或调试有用,否则一般用户几乎没有兴趣。

命令默认是返回未排序的项目,可以使用以下两个参数调用两种不同的排序方法:

  1. ASC: 相对于中心,从最近到最远对返回的项目进行排序.
  2. DESC: 相对于中心,从最远到最近的顺序对返回的项目进行排序。

默认情况下,返回所有匹配的项目。可以使用 COUNT N参数将结果限制为前 N 个匹配项。

当添加ANY 参数时,该命令将在找到足够的匹配项后立即返回,因此结果可能不是最接近要求的结果,但另一方面,将显著降低服务器的工作量,这个命令对于一个很大的范围的搜索来说对于减轻服务器压力非常有用。

默认情况下,该命令将项目返回给客户端,可以使用以下参数之一存储结果:

  1. STORE: 将项目存储在填充了其地理空间信息的Sorted set中.
  2. STOREDIST: 将项目存储在一个Sorted set中,该集合以它们与中心的距离作为浮点数填充,在半径中指定的单位相同。

返回值:

  1. 没有指定任何 WITH 选项,该命令只返回一个线性数组,如 [“New York”,“Milan”,“Paris”]
  2. 如果指定了 WITHCOORD、WITHDIST 或 WITHHASH 选项,则该命令返回一个数组的数组,其中每个子数组代表一个项目。
  3. 子数组最多包含四项元素:元素名、与中心的距离的浮点数,单位与参数指定的单位相同、geohash 整数、两个元素的数组(经度和纬度)。

由于 GEORADIUS 和 GEORADIUSBYMEMBER 具有 STORE 和 STOREDIST 选项,因此它们在技术上被标记为 Redis 命令表中的写入命令。因此,Redis 集群的从服务器接到这两命令时,会将它们重定向到主服务器。

从 Redis 3.2.10 和 Redis 4.0.0 开始,Reids分别提供了可在从服务器中使用的只读命令GEORADIUS_RO和GEORADIUSBYMEMBER_RO来代替GEORADIUS和GEORADIUSBYMEMBER,这两个只读命令与原始命令完全相同,但拒绝 STORE 和 STOREDIST 选项,可以安全的在从服务器中使用。

5.1 使用案例

返回距离故宫不超过3km的最近的一个点位(这里的COUNT 后面的数字是2,因为ASC不会排除自身):

127.0.0.1:6379> GEORADIUSBYMEMBER test gugong 3 km COUNT 2 ASC WITHDIST
1) 1) "gugong"
   2) "0.0000"
2) 1) "tiananmen"
   2) "1.4152"

可以看到,3km以内的距离故宫最近的点位是天安门,它们距离约1415m:

再返回距离故宫不超过3km的最远的一个点位:

127.0.0.1:6379> GEORADIUSBYMEMBER test gugong 3 km COUNT 1 DESC WITHDIST
1) 1) "beijingzhan"
   2) "2.9744"

可以看到,3km以内的距离故宫最远的点位是北京站,它们距离约2977m。

返回距离故宫不超过3km的最近的3个点位,展示全部附加内容:

127.0.0.1:6379> GEORADIUSBYMEMBER test gugong 3 km COUNT 4 ASC WITHCOORD WITHDIST WITHHASH
1) 1) "gugong"
   2) "0.0000"
   3) (integer) 4069885548668386
   4) 1) "116.39712899923324585"
      2) "39.91652647362980844"
2) 1) "tiananmen"
   2) "1.4152"
   3) (integer) 4069885361351847
   4) 1) "116.39798730611801147"
      2) "39.9038199164580405"
3) 1) "beijingzhan"
   2) "2.9744"
   3) (integer) 4069885468202423
   4) 1) "116.4269980788230896"
      2) "39.90273505580184832"

返回距离故宫不超过5km的最近的3个点位,展示全部附加内容:

127.0.0.1:6379> GEORADIUSBYMEMBER test gugong 5 km COUNT 4 ASC WITHCOORD WITHDIST WITHHASH
1) 1) "gugong"
   2) "0.0000"
   3) (integer) 4069885548668386
   4) 1) "116.39712899923324585"
      2) "39.91652647362980844"
2) 1) "tiananmen"
   2) "1.4152"
   3) (integer) 4069885361351847
   4) 1) "116.39798730611801147"
      2) "39.9038199164580405"
3) 1) "beijingzhan"
   2) "2.9744"
   3) (integer) 4069885468202423
   4) 1) "116.4269980788230896"
      2) "39.90273505580184832"
4) 1) "ritangongyuan"
   2) "3.9846"
   3) (integer) 4069885683167475
   4) 1) "116.44382089376449585"
      2) "39.91560636984896604"

6 GEORADIUS坐标半径搜索

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]

该命令与 GEORADIUSBYMEMBER完全相同,唯一不同的是,它不以Sorted Set表示的地理空间索引中已存在的成员的名称作为要查询的区域的中心,而是以指定的经纬度值作为查询区域中心。

在使用GEORADIUS时,只需要将点位的名字改为经纬度值即可。

127.0.0.1:6379> GEOPOS test gugong
1) 1) "116.39712899923324585"
   2) "39.91652647362980844"
127.0.0.1:6379> GEORADIUS test 116.39712899923324585 39.91652647362980844 3 km COUNT 2 ASC WITHDIST
1) 1) "gugong"
   2) "0.0000"
2) 1) "tiananmen"
   2) "1.4152"

在 Redis 6.2.0的版本中,GEORADIUS 命令系列被视为已弃用,在新代码中优先选择 GEOSEARCH 和 GEOSEARCHSTORE。

7 GEOSEARCH指定区域搜索

GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]

Redis 6.2 新增的命令。返回使用 GEOADD填充的地理空间信息的Sorted set的成员,这些成员位于给定形状指定的区域的边界内。

该命令扩展了 GEORADIUS 命令,因此除了在圆形区域内搜索外,它还支持在矩形区域内搜索。应使用此命令代替已弃用的 GEORADIUS 和 GEORADIUSBYMEMBER 命令。

查询的中心点由以下强制性参数之一提供:

  1. FROMMEMBER: 使用给定在的现有Sorted set中的成员的位置。
  2. FROMLONLAT: 使用给定的经度和纬度确定的位置。

这两个参数使得GEOSEARCH完全替代了GEORADIUS 和 GEORADIUSBYMEMBER 命令。

查询的形状由以下强制性参数之一提供:

  1. BYRADIUS: 与 GEORADIUS 类似,根据给定的radius半径在圆形区域内搜索。
  2. BYBOX: 在轴对齐的矩形内搜索,由height长和width确定长和宽。

该命令可选地使用以下参数,用于返回更多的附加信息:WITHDIST、WITHCOORD、WITHHASH。其含义与在GEORADIUS和GEORADIUSBYMEMBER命令中的使用一致。

命令默认是返回未排序的项目。可以使用以下两个参数调用两种不同的排序方法:ASC、DESC。其含义与在GEORADIUS和GEORADIUSBYMEMBER命令中的使用一致。

其他命令参数,如COUNT N、ANY、STORE、STOREDIST等的含义与在GEORADIUS和GEORADIUSBYMEMBER命令中的使用一致。

返回值的格式与在GEORADIUS和GEORADIUSBYMEMBER命令中的使用一致。

相比于GEORADIUS和GEORADIUSBYMEMBER,该命令没有提供STORESTOREDIST选项,因此是一个真正的只读命令,可以在从服务器中直接使用。

我们搜索以故宫为中心,半径为3km的圆形范围内的目标:

127.0.0.1:6379> GEOSEARCH test FROMMEMBER gugong BYRADIUS 3 km ASC COUNT 5 WITHDIST
1) 1) "gugong"
   2) "0.0000"
2) 1) "tiananmen"
   2) "1.4152"
3) 1) "beijingzhan"
   2) "2.9744"

我们搜索以故宫为中心,长宽为3km的矩形范围内的目标:

127.0.0.1:6379> GEOSEARCH test FROMMEMBER gugong BYBOX 3 3 km ASC COUNT 5 WITHDIST
1) 1) "gugong"
   2) "0.0000"
2) 1) "tiananmen"
   2) "1.4152"

很明显,选择不同的形状,将可能返回不同的结果。

8 GEOSEARCHSTORE搜索并存储

GEOSEARCHSTORE destination source [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [STOREDIST]

Redis 6.2 新增的命令。此命令类似于 GEOSEARCH,但只是将结果存储在目标key中,此命令代替现已弃用的 GEORADIUS 和 GEORADIUSBYMEMBER,这是一个写命令。

默认情况下,它将结果与其地理空间信息一起存储在目标Sorted set中,score是geohash的52 位无符号二进制数的十进制形式。使用 STOREDIST 选项时,该命令将项目存储在一个Sorted set集中,该Sorted set中填充了它们与圆或框中心的距离浮点数作为score,单位是指定的半径单位。

如下案例,我们将以故宫为中心,半径为3km的所有坐标存储在一个新的test2的Sorted set集合中,score为geohash的整数形式:

127.0.0.1:6379> GEOSEARCHSTORE test2 test FROMMEMBER gugong BYRADIUS 3 km ASC
(integer) 3
127.0.0.1:6379> ZRANGE test2 0 -1 WITHSCORES
1) "tiananmen"
2) "4069885361351847"
3) "beijingzhan"
4) "4069885468202423"
5) "gugong"
6) "4069885548668386"

如果我们加上STOREDIST选项,那么新的Sorted set中的score就是距离,其单位与GEOSEARCHSTORE中指定的单位相同:

127.0.0.1:6379> GEOSEARCHSTORE test2 test FROMMEMBER gugong BYRADIUS 3 km ASC STOREDIST
(integer) 3
127.0.0.1:6379> ZRANGE test2 0 -1 WITHSCORES
1) "gugong"
2) "0"
3) "tiananmen"
4) "1.4151991392787893"
5) "beijingzhan"
6) "2.9744056471131772"

9 Redis GEO的原理

经纬度是经度与纬度的合称组成一个坐标系统,称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,经度范围是东经180到西经180,纬度范围是南纬90到北纬90,我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。

经纬度能够标示地球上除了南北极点的任何一个位置,因为南北极是一个点,所有经线交汇于极点,极点的经度可以表示为0度到180度之间(东经)和0度到-180度之间(西经)的任意值。

9.1 GeoHash算法的简介

GeoHash是一种地址编码方法,他能够把二维的空间经纬度数据编码成一个字符串。Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。

GeoHash算法分为三步,我们以故宫的坐标(116.397128, 39.916527)为例子进行讲解。

第一步:将经度和纬度使用极限逼近算法转换为二进制数,这一步也采用了二分法的思想,以经度116.397128为例子:

  1. 将区间[-180,180]进行二分得到[-180,0),[0,180]两个区间,设左区间的位为0,右边区间的位为1,116.397128属于右区间,那么转换为二进制数的第1位为1。
  2. 将右区间[0,180]进行二分得到[0,90),[90,180]两个区间,同理,116.397128属于右区间,那么转换为二进制数的第2位为1。
  3. 将右区间[90,180]进行二分得到[90,135),[135,180]两个区间,同理,116.397128属于左区间,那么转换为二进制数的第3位为0。
  4. 将左区间[90,135)进行二分得到[90,112.5],[112.5,135)两个区间,同理,116.397128属于右区间,那么转换为二进制数的第4位为1。
  5. 将右区间[112.5,135)进行二分得到[112.5,123.75),[123.75,135)两个区间,同理,116.397128属于左区间,那么转换为二进制数的第5位为0。
  6. 将左区间[112.5,123.75)进行二分得到[112.5,118.125),[118.125,123.75)两个区间,同理,116.397128属于左区间,那么转换为二进制数的第6位为0。

这样不断地进行二分下去,二分的次数越多,所得到的区间也就越逼近于真实的经度或者纬度值,最终得到的GeoHash定位也就越准确,范围也就越小。

在 Redis 中,经纬度使用 52 位的无符号整数进行整体编码。经过26次的二分法,最终得到的经度的26位的二进制数就是:11010010110001010111001101,得到的纬度的26位的二进制数就是10111000110001010010100111。

第二步:将经度和纬度的二进制编码从头开始一位一位的合并,经度占奇数位,纬度占偶数位,最终得到的一个52位的无符号数:1110011101001000111100000011001100101110010010110111。

第三步,对得到的合并二进制编码进行Base32编码(编码表字符集有32个字符,09,az,去掉 a/i/l/o四个容易混淆的字母,编码时以每五个位进行一次编码,不够的填充0),让它变成一个真正的字符串,最终的结果就是“wx4g0dtf9e3”,这就是我们获取到的故宫的geihash值。可以看到该值与Redis GEO计算的值“wx4g0dtf9e0”有些许区别,因为各种GEO的实现稍有不同,但是最终的定位位置都会是在故宫的。

我们打开http://geohash.org/wx4g0dtf9e3打开,点击google,结果如下:


确实定位在故宫!

我们再换一个日坛公园的地址(116.44381989453123, 39.915605367786505),计算得到的geoHash为wx4g1drv382,和Redis GEO的计算结果“wx4g1drv380”差不多,同样http://geohash.org/wx4g1drv382也能够定位到日坛公园:


经过GeoHash算法,二维的经纬度点位就能够变成一个非常好保存且保密性比较好的字符串的形式,Redis GEO同样采用了上面这种GeoHash算法,并且将最终的数据存入一个Sorted set集合中,集合的每一个元素就是存入的坐标的名字(比如gugong、ritangongyuan),每个元素的score则是经纬度的52位的无符号geoHash编码的10进制整数形式。

9.2 GeoHash算法的Java实现

下面是GeoHash算法的简单Java实现。

/**
 * @author lx
 */
public class GEOHash {
    static final String ONE = "1";
    static final String ZERO = "0";
    static final String TWO = "2";
    static final String LONGITUDE_MAX = "180";
    static final String LONGITUDE_MIN = "-180";
    static final String LATITUDE_MAX = "90";
    static final String LATITUDE_MIN = "-90";
    static final int BIN_NUM = 26;
    static final int MERGE_BIN_NUM = 26 << 1;

    /**
     * Base32编码的字符串池
     */
    private final static char[] BASE_32_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
            'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};

    /**
     * 与十进制值的对应关系
     */
    final static HashMap<Character, Integer> BASE_32_LOOKUP = new HashMap<Character, Integer>();


    static {
        int i = 0;
        for (char c : BASE_32_CHARS) {
            BASE_32_LOOKUP.put(c, i++);
        }
    }

    public static void main(String[] args) {
        String longitude = getBin("116.44381989453123", true);
        System.out.println("longitudebin: " + longitude);
        String latitude = getBin("39.915605367786505", false);
        System.out.println("latitudebin: " + latitude);
        String merge = merge(longitude, latitude);
        System.out.println("mergebin: " + merge);
        String geoHash = base32Encode(merge);
        System.out.println("geohash: " + geoHash);
        double[] doubles = base32Decode(geoHash);
        System.out.println("longitude: " + doubles[0] + ",  latitude: " + doubles[1]);
    }

    /**
     * 经纬度的二进制编码
     */
    private static String getBin(String longitudeOrLatitude, boolean isLongitudeOrLatitude) {
        String min, max;
        if (isLongitudeOrLatitude) {
            max = LONGITUDE_MAX;
            min = LONGITUDE_MIN;
        } else {
            max = LATITUDE_MAX;
            min = LATITUDE_MIN;
        }
        BigDecimal num = new BigDecimal(longitudeOrLatitude);
        BigDecimal two = new BigDecimal(TWO), x, y, z;
        StringBuilder stringBuilder = new StringBuilder();
        if (num.compareTo(new BigDecimal(ZERO)) >= 0) {
            stringBuilder.append(ONE);
            x = new BigDecimal(ZERO);
            y = new BigDecimal(max);
        } else {
            stringBuilder.append(ZERO);
            x = new BigDecimal(min);
            y = new BigDecimal(ZERO);
        }
        z = x.add(y).divide(two);
        for (int i = 1; i < 26; i++) {
            if (num.compareTo(z) >= 0) {
                stringBuilder.append(ONE);
                x = z;
                z = x.add(y).divide(two);
            } else {
                stringBuilder.append(ZERO);
                y = z;
                z = x.add(y).divide(two);
            }
        }
        return stringBuilder.toString();
    }

    /**
     * 合并经纬度的二进制编码
     */
    private static String merge(String longitude, String latitude) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < BIN_NUM; i++) {
            stringBuilder.append(longitude.Redis 的 GEO 特性将在 Redis 3.2 版本释出

redis geo 地理位置系应用战案例

Redis Geo HyperLogLog类型介绍

Redis学习笔记—地理信息定位(GEO)

redis GEO数据类型

Redis Geo: Redis新增位置查询功能