最近在做上门洗车项目时有个需求:需要根据用户选择的坐标位置和支持的服务范围来查找附近的洗车门店。项目中用到Redis来做缓存,刚好Redis有支持GEO地理位置的功能,用来实现该需求再合适不过了。遂研究了下RedisGEO地理位置如何使用。

概述

Redis3.2版本开始支持geohash算法,地理位置的坐标是以WGS84为标准。WGS84全称World Geodetic System 1984,是为GPS全球定位系统使用而建立的坐标系统。

GEO 命令

RedisGEO目前提供以下6个命令,详情可点击查看

1. GEOADD:增加某个地理位置的坐标。
2. GEOPOS:获取某个地理位置的坐标。
3. GEODIST:获取两个地理位置的距离。
4. GEORADIUS:根据给定地理位置坐标获取指定范围内的地理位置集合。
5. GEORADIUSBYMEMBER:根据给定地理位置获取指定范围内的地理位置集合。
6. GEOHASH:获取某个地理位置的geohash值。

GEOADD

GEOADD命令是用来增加地理位置坐标的,可以批量添加多个地理位置的坐标。命令格式为:

GEOADD key longitude latitude member [longitude latitude member ...]

key标识一个地理位置的集合,longitude是地理位置的经度,latitude是地理位置的纬度,member是该地理位置的名称。

Redis中接受的有效经度范围为-180~180度,有效纬度范围为-85.05112878~85.05112878度(靠近南北极的一小块地方是无法生成索引的)。

GEOPOS

GEOPOS命令是用来获取地理位置坐标的,可以批量获取多个地理位置的坐标。命令格式为:

GEOPOS key member [member ...]

GEODIST

GEODIST命令是用来获取两个地理位置的距离。命令格式为:

GEODIST key member1 member2 [unit]

unit单位可选项为m(米,默认值),km(千米),ft(英尺),mi(英里)。

GEORADIUS

GEORADIUS命令可以根据给定的地理位置坐标获取指定范围内的地理位置集合。命令格式为:

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

key标识一个地理位置的集合,longitude是地理位置的经度,latitude是地理位置的纬度,radius表示范围距离,距离单位可以为m|km|ft|mi,还有一些可选参数:

  • WITHCOORD:同时返回匹配位置的经纬度。
  • WITHDIST:同时返回匹配位置与给定地理位置的距离。
  • WITHHASH:同时返回匹配位置的geohash值。
  • COUNT count:指定返回的结果个数,必须大于0
  • ASC|DESC:默认返回的结果是未排序的,ASC为从近到远排序,DESC为从远到近排序。
  • STORE key:结果存到新的有序集合中,以geohash做为score,该选项与WITHCOORDWITHDISTWITHHASHSTOREDIST选项冲突。
  • STOREDIST key:结果存到新的有序集合中,以与指定位置的距离做为score,该选项与WITHCOORDWITHDISTWITHHASHSTORE选项冲突。

GEORADIUSBYMEMBER

GEORADIUSBYMEMBER命令可以根据给定的地理位置获取指定范围内的地理位置集合。命令格式为:

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

很明显GEORADIUS命令传递的是地理位置坐标,GEORADIUSBYMEMBER命令传递的是地理位置名称。与GEORADIUS相比,使用GEORADIUSBYMEMBER更为方便,但使用GEORADIUS则更为灵活,可以获取任何坐标点范围内的地理位置。

GEOHASH

GEOHASH命令是用来获取地理位置的geohash值,可以批量获取多个地理位置的geohash值。命令格式为:

GEOHASH key member [member ...]

GEORADIUS_RO 和 GEORADIUSBYMEMBER_RO

GEORADIUS_ROGEORADIUSBYMEMBER_RORedis 3.2.10后新引进的两个命令。由于GEORADIUSGEORADIUSBYMEMBER命令存在STORESTOREDIST选项,在Redis中该两个命令被标记为写命令类型。即使在从节点的连接设置了readonly模式下,标记为写命令类型的命令,依然会收到MOVED消息,被转向到相应主节点。所以Redis新增了该两个命令的只读版本,这两个命令除了不支持STORESTOREDIST选项外,其他可选参数与GEORADIUS_ROGEORADIUSBYMEMBER_RO一致。新增的两个只读命令在从节点连接设置readonly模式下可以在从节点执行。

GEO 实现

Redis GEO实现主要包含了以下两项技术:

1. 使用geohash保存地理位置的坐标。
2. 使用有序集合保存地理位置的集合。

使用 geohash 保存地理位置的坐标

使用geohash保存地理位置的坐标的思想是将二维的经纬度转换成一维的字符串hash值。geohash有以下三个特点:

1. 字符串越长,表示的范围越精确。编码长度为 8 时,精度在 19 米左右,而当编码长度为 9 时,精度在 2 米左右。
2. 字符串相似的表示距离相近。利用字符串的前缀匹配,可以快速的查询到附近的地理位置。
3. geohash 计算的字符串,可以反向解码出原来的经纬度。

这三个特性让geohash特别适合表示二维hash值。这篇文章:GeoHash核心原理解析详细的介绍了geohash的原理,想要了解geohash实现的朋友可以参考这篇文章。

使用有序集合保存地理位置的集合

各个命令的实现原理:

  • GEOADD命令增加地理位置时,先计算地理位置坐标的geohash值,然后geohash作为给定的memberscore使用ZADD命令插入到有序集合。
  • GEOPOS命令获取地理位置坐标时,先根据给定的member获取到score,也就是地理位置的geohash值,然后decode得到地理位置的坐标。
  • GEODIST命令获取两个地理位置的距离时,先根据给定的member获得各自的坐标,然后计算两个坐标的距离。
  • GEORADIUSGEORADIUSBYMEMBER使用相同的实现(GEORADIUSBYMEMBER多了一步把地理位置转换成对应的坐标)。先查找该坐标和周围对应8个坐标符合距离要求的地理位置。因为geohash得到的值其实是个格子,并不是点,这样通过计算周围对应8个坐标就能解决边缘问题。由于使用有序集合保存地理位置,在对地列位置基于范围查询,就相当于实现了ZRANGE命令,内部的实现确实与ZRANGE命令一致,只是GEO有些特别的处理,比如获得的某个地理位置,还需要计算该地理位置是否符合给定的距离访问。
  • GEOHASH命令获取地理位置的geohash值时,直接根据给定的member获取到score

细心的读者可能发现,Redis没有实现地理位置的删除命令。不过由于GEO数据是使用有序集合保存的,所以可以使用ZREM命令来删除某个地理位置。

参考文章

redis3.2新功能—GEO地理位置命令介绍

[redis学习笔记]redis中Geo命令介绍