Redis Geohash指令与位置服务应用

目录

0.Redis 命令

增加

距离

获取元素位置

获取元素的 hash 值

附近的公司

 1.为什么要GeoHash?

2 Redis GEO API

2.1 增加地理位置信息

2.2 获取地理位置信息

2.3 获取两个地理位置的距离

2.4 获取指定位置范围的地理信息位置集合

2.5 获取指定元素范围的地理信息位置集合

3 Redis GEO实现附近XXX功能

4 Redis GEO背后的原理

4.1 存储结构

4.2 geohash原理分析

4.2.1 如何将三维变二维?

4.2.2 如何将二维变一维?

4.2.3 如何将一维表示成二进制码存储

5.结束

6.参考文章


 

0.Redis 命令

Redis 提供的 Geo 指令只有 6 个,它只是一个普通的 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,
分别代表米、千米、英里和尺。

获取元素位置

geopos 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。
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"

获取元素的 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"

附近的公司

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) "juejin"
2) "10.5501"
3) (integer) 4069887154388167
4) 1) "116.48104995489120483"
2) "39.99679348858259686"
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) "juejin"
2) "10.5501"
3) 1) "meituan"
2) "11.5748"

 1.为什么要GeoHash?

Redis Geohash指令与位置服务应用_第1张图片

移动互联网已融入到我们生活中的方方面面。

我们平时找商家、找房子、找车都可以通过各种App来完成。作为:man:‍:computer:‍的笔者职业习惯性地思考这些功能是如何实现的呢?

例如寻找附近3公里范围内的出租车的需求,最直观的想法就是去 数据库 里面查表筛选出距离用户小于3公里的车辆,将数据返回给客户端。

 

这种方法有一个很严重的问题,需要对整张表里面的每一项都计算一次相对距离太耗时了。既然整张表数据量比较大那么我们能不能缩小扫描的范围呢?那么就会想到是否可以按业务特点缩小扫描范围比如只扫描用户当前位置所在城市的车辆,按照这个思路扩展开来发现数据量还是很大而且不能解决当用户处于两个城市的边界时的问题。

如何快速地索引数据是解决这个问题的关键,在浏览Redis API的时候发现其可以直接实现附近的XXX功能,下文中将介绍如何以 Redis 实现此类功能并深入分析其背后的实现原理。

2 Redis GEO API

2.1 增加地理位置信息

geo add key longitude latitude member [longitude latitude member ...]
复制代码

eg:

向cars:locations中增加车辆编号为1以及车辆编号为2的位置信息。

127.0.0.1:6379> geoadd cars:locations 120.346111 31.556381 1 120.375821 31.560368 2 
复制代码

2.2 获取地理位置信息

eg:

获取车辆编号为1的车辆位置信息

127.0.0.1:6379> geopos cars:locations 1
1) 1) "120.34611314535140991"
   2) "31.55637987511895659"
复制代码

2.3 获取两个地理位置的距离

eg:

获取编号为1的车辆与编号为2的车辆之间的距离

127.0.0.1:6379> geodist cars:locations 1 2 km
"2.8504"
复制代码

2.4 获取指定位置范围的地理信息位置集合

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
复制代码

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

eg:

以经度120.375821纬度31.556381为中心查找5公里范围内的车辆

127.0.0.1:6379> GEORADIUS cars:locations 120.375821 31.556381 5 km WITHCOORD WITHDIST WITHHASH  ASC COUNT 100
1) 1) "2"
   2) "0.4433"
   3) (integer) 4054421167795118
   4) 1) "120.37582129240036011"
      2) "31.5603669915025975"
2) 1) "1"
   2) "2.8157"
   3) (integer) 4054421060663027
   4) 1) "120.34611314535140991"
      2) "31.55637987511895659"
复制代码

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

范围可以使用以下其中一个单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。

  • WITHCOORD : 将位置元素的经度和维度也一并返回。

  • WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。 命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

  • ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。

  • 在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。

2.5 获取指定元素范围的地理信息位置集合

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
复制代码

eg:

以编号为1的车辆为中心查找5公里范围内的车辆 GEORA

127.0.0.1:6379> GEORADIUSBYMEMBER cars:locations 2 5 km WITHCOORD WITHDIST WITHHASH  ASC COUNT 100
1) 1) "2"
   2) "0.0000"
   3) (integer) 4054421167795118
   4) 1) "120.37582129240036011"
      2) "31.5603669915025975"
2) 1) "1"
   2) "2.8157"
   3) (integer) 4054421060663027
   4) 1) "120.34611314535140991"
      2) "31.55637987511895659"
复制代码

相关可选参数同2.4中一致。

3 Redis GEO实现附近XXX功能

研究完Redis GEO API后可以发现只要在Redis客户端调用

2.4 获取指定位置范围的地理信息位置集合

API 即可实现相关需求。so easy!!!

4 Redis GEO背后的原理

4.1 存储结构

Redis 在存储数据不同数据类型的数据时都有对应的编码方式。 Redis GEO是采用哪种编码方式进行存储的呢?

在翻阅Redis GEO API时发现其并没有删除指令,因为其底层是使用 zset 进行实现的。 我们可以使用zrem 进行数据的删除。

再尝试用zset的查询指令,查询上文中添加的GEO信息

127.0.0.1:6379> ZRANGE cars:locations  0 -1 WITHSCORES
1) "1"
2) "4054421060663027"
3) "2"
4) "4054421167795118"
复制代码

发现车辆编号为1的位置信息为4054421060663027;车辆编号为2的位置信息为4054421167795118。 再回顾一下zset增加成员的指令

ZADD key score member [[score member] [score member] ...]
复制代码

至此可以推断出Redis GEO 添加经、纬度位置信息的指令的过程是

ZADD cars:locations 4054421060663027 1
复制代码

4054421060663027为对经纬度进行编码后的值。使用4054421060663027作为score 可以快速实现对经纬度的索引。

查看相关文档发现Redis使用了geohash对经纬度信息进行的编码。

4.2 geohash原理分析

关于geohash的核心原理,这篇文章分析的很好GeoHash核心原理解析

总结下来就是

  • 如何唯一表示地球上的一块空间?
  • 如何将地球切分成大小近似的区块,并支持不同粒度的表示?

为了解决上述两个问题,我们需要三个步骤。

  1. 第一步,将三维地球变成二维;
  2. 第二步,将二维再转成一维;
  3. 最后一步,将一维表示成二进制码存储。

4.2.1 如何将三维变二维?

地球纬度区间是[-90,90],经度区间是[-180,180]。 将它展开想象长一个矩形为

Redis Geohash指令与位置服务应用_第2张图片

4.2.2 如何将二维变一维?

通过刚才的方法,我们能够将地球的表面转换成二维空间的平面。那接下来要将二维转变成一维。如果切割二维空间,可以切割出很多正方形。如何表示这个正方形呢?最简单的方法是在平面上进行遍历。每遍历到一个点,就给它标注一个值,比如00、01、10、11,随着二进制数字增加,相当于遍历面上不同的位置。

Redis Geohash指令与位置服务应用_第3张图片

当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线。

如何表示不同的粒度?

当我们递归的将各个块分解成更小的子块时可以标识更小的空间范围(如上图二中所示),如从0000开始到1111结束编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为Peano空间填充曲线。

4.2.3 如何将一维表示成二进制码存储

Geohash 也有几种编码形式,常见的有2种,base 32 和 base 36。 会将落到网格中的二进制数据编码成字符串

5.结束

分析完Redis GEO的实现原理后不难发现其背后核心是geohash,使用geohash将二维的经纬度数据编码成一维数据,再使用B树索引快速查找出需要的数据。

上述使用Redis GEO 实现附近的人,附近的车辆,附近的商家此类功能时只能通过半径进行查找。

Q:如果需求是我要查找附近5公里内所有商家中有卖咖啡的呢?

A:当然我们可以在应用层对Reids 查询出的所有数据进行过滤。

Q:当Redis返回数据量、用户请求量比较大时是非常吃内存资源的,是否有更优解?业内的数据库实现中是否已经有了更好的解决方案?

A:带着这样的疑问我查找了相关资料发现geohash其实是空间索引的一种实现,我们经常使用的 MySql 、 MongoDB 都有空间索引的实现。

  1. MongoDB

mongo中的GeoJSON对象有点、线、多边形、多条线段、多点、多个多边形。支持 包含、相交、临近的查询,同时支持多条件查询。( 感觉非常强大的样子真是换一个解决方案可能会有质的收益 )

  1. MySql

MySql InnoDB 在5.7.4 labs版本中才添加对空间索引的支持,它们都是通过 R 树来实现空间索引。

MySql的升级成本是很高的,理解了geohash原理后我们可以在MySql表中新增geohash字段,通过B数的二分查找法快速定位数据。

6.参考文章

Redis GEO & 实现原理深度分析

Redis之GeoHash

你可能感兴趣的:(Memcache/Redis,LBS位置服务)