Redis 是怎么实现 “附近的人” 的?

640?wx_fmt=jpeg

针对“附近的人”这一位置服务领域的应用场景,常见的可以使用PG、 MySQL和MongoDB等多种DB的空间索引进行实现。

而Redis另辟蹊径,结合其有序队列zset以及geohash编码,实现了空间搜索功能,且拥有极高的运行效率。

本文将从源码角度对其算法原理进行解析,并推算查询时间复杂度。

要提供完整的“附近的人”服务,最基本的是要实现“增”、“删”、“查”的功能。 如下将分别进行介绍,其中会重点对查询功能进行解析。

操做命令

自Redis 3.2开始,Redis基于geohash和有序集合提供了地理位置相关功能。Redis Geo模块包含了如下6个命令:

  • GEOADD: 将给定的位置对象(纬度、经度、名字)添加到指定的key;
  • GEOPOS: 从key里面返回全部给定位置对象的位置(经度和纬度);
  • GEODIST: 返回两个给定位置之间的距离;
  • GEOHASH: 返回一个或多个位置对象的Geohash表示;
  • GEORADIUS: 以给定的经纬度为中心,返回目标集合中与中心的距离不超过给定最大距离的全部位置对象;
  • GEORADIUSBYMEMBER: 以给定的位置对象为中心,返回与其距离不超过给定最大距离的全部位置对象。

其中,组合使用GEOADD和GEORADIUS可实现“附近的人”中“增”和“查”的基本功能。

要实现微信中“附近的人”功能,可直接使用GEORADIUSBYMEMBER命令。其中“给定的位置对象”即为用户本人,搜索的对象为其余用户。

不过本质上,GEORADIUSBYMEMBER = GEOPOS + GEORADIUS,即先查找用户位置再经过该位置搜索附近知足位置相互距离条件的其余用户对象。

如下会从源码角度入手对GEOADD和GEORADIUS命令进行分析,剖析其算法原理。
Redis geo操做中只包含了“增”和“查”的操做,并无专门的“删除”命令。主要是由于Redis内部使用有序集合(zset)保存位置对象,可用zrem进行删除。

在Redis源码geo.c的文件注释中,只说明了该文件为GEOADD、GEORADIUS和GEORADIUSBYMEMBER的实现文件(其实在也实现了另三个命令)。从侧面看出其余三个命令为辅助命令。


GEOADD


使用方式


 
 

将给定的位置对象(纬度、经度、名字)添加到指定的key。

其中,key为集合名称,member为该经纬度所对应的对象。在实际运用中,当所需存储的对象数量过多时,可经过设置多key(如一个省一个key)的方式对对象集合变相作sharding,避免单集合数量过多。

成功插入后的返回值:

 
 

其中N为成功插入的个数。

源码分析

 
 
 
 
经过源码分析能够看出Redis内部使用有序集合(zset)保存位置对象,有序集合中每一个元素都是一个带位置的对象,元素的score值为其经纬度对应的52位的geohash值。

double类型精度为52位;
geohash是以base32的方式编码,52bits最高可存储10位geohash值,对应地理区域大小为0.6*0.6米的格子。换句话说经Redis geo转换过的位置理论上会有约0.3*1.414=0.424米的偏差。


算法小结

简单总结下GEOADD命令都干了啥:
一、参数提取和校验;
二、将入参经纬度转换为52位的geohash值(score);
三、调用ZADD命令将member及其对应的score存入集合key中。


GEORADIUS

使用方式


 
  
 
 
 
 

范围单位:m | km | ft | mi --> 米 | 公里 | 英尺 | 英里

额外参数:

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

- WITHCOORD:将位置对象的经度和维度也一并返回。

- WITHHASH:以 52 位有符号整数的形式,返回位置对象通过原始 geohash 编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中的做用并不大。

- ASC|DESC:从近到远返回位置对象元素 | 从远到近返回位置对象元素。- COUNT count:选取前N个匹配位置对象元素。(不设置则返回全部元素) 

- STORE key:将返回结果的地理位置信息保存到指定key。- STORedisT key:将返回结果离中心点的距离保存到指定key。
关注微信公众号:Java技术栈,在后台回复:redis,能够获取我整理的 N 篇最新 Redis 教程,都是干货。
因为 STORE 和 STORedisT 两个选项的存在,GEORADIUS 和 GEORADIUSBYMEMBER 命令在技术上会被标记为写入命令,从而只会查询(写入)主实例,QPS太高时容易形成主实例读写压力过大。

为解决这个问题,在 Redis 3.2.10 和 Redis 4.0.0 中,分别新增了 GEORADIUS_RO 和 GEORADIUSBYMEMBER_RO两个只读命令。

不过,在实际开发中笔者发现 在java package
Redis.clients.jedis.params.geo  的 GeoRadiusParam 参数类中并不包含 STORE 和 STORedisT 两个参数选项,在调用georadius时是否真的只查询了主实例,仍是进行了只读封装。感兴趣的朋友能够本身研究下。

成功查询后的返回值:

不带WITH限定,返回一个member list,如:
 
 
带WITH限定,member list中每一个member也是一个嵌套list,如:

 
 
 
 

源码分析

此段源码较长,看不下去的可直接看中文注释,或直接跳到小结部分
 
 
 
 

对应的是 geohashGetAreasByRadiusWGS84 membersOfAllNeighbors 两个函数。

咱们依次来看:
  • 计算中心点范围:

// geohash_helper.c

 
 
 
 
  • 对中心点及其周围8个geohash网格区域进行查找:

// geo.c

 
 
 
 

算法小结

抛开众多可选参数不谈,简单总结下GEORADIUS命令是怎么利用geohash获取目标位置对象的:

一、参数提取和校验;

二、利用中心点和输入半径计算待查区域范围。这个范围参数包括知足条件的最高的geohash网格等级(精度) 以及 对应的可以覆盖目标区域的九宫格位置;(后续会有详细说明)

三、对九宫格进行遍历,根据每一个geohash网格的范围框选出位置对象。进一步找出与中心点距离小于输入半径的对象,进行返回。

直接描述不太好理解,咱们经过以下两张图在对算法进行简单的演示:

640?wx_fmt=jpeg
640?wx_fmt=jpeg

令左图的中心为搜索中心,绿色圆形区域为目标区域,全部点为待搜索的位置对象,红色点则为知足条件的位置对象。

在实际搜索时,首先会根据搜索半径计算geohash网格等级(即右图中网格大小等级),并肯定九宫格位置(即红色九宫格位置信息);再依次查找计算九宫格中的点(蓝点和红点)与中心点的距离,最终筛选出距离范围内的点(红点)。

算法分析

为何要用这种算法策略进行查询,或者说这种策略的优点在哪,让咱们以问答的方式进行分析说明。


这实际上是一个问题,本质上是对全部的元素对象进行了一次初步筛选。 在多层geohash网格中,每一个低等级的geohash网格都是由4个高一级的网格拼接而成(如图)。

640?wx_fmt=jpeg

换句话说,geohash网格等级越高,所覆盖的地理位置范围就越小。当咱们根据输入半径和中心点位置计算出的可以覆盖目标区域的最高等级的九宫格(网格)时,就已经对九宫格外的元素进行了筛除。

这里之因此使用九宫格,而不用单个网格,主要缘由仍是为了不边界状况,尽量缩小查询区域范围。试想以0经纬度为中心,就算查1米范围,单个网格覆盖的话也得查整个地球区域。而向四周八个方向扩展一圈可有效避免这个问题。

如何经过geohash网格的范围框选出元素对象?效率如何?

首先在每一个geohash网格中的geohash值都是连续的,有固定范围。因此只要找出有序集合中,处在该范围的位置对象便可。如下是有序集合的跳表数据结构:

640?wx_fmt=jpeg

其拥有相似二叉查找树的查询效率,操做平均时间复杂性为O(log(N))。且最底层的全部元素都以链表的形式按序排列。

因此在查询时,只要找到集合中处在目标geohash网格中的第一个值,后续依次对比便可,不用屡次查找。 

九宫格不能一块儿查,要一个个遍历的缘由也在于九宫格各网格对应的geohash值不具备连续性。只有连续了,查询效率才会高,否则要多作许多距离运算。

综上,咱们从源码角度解析了Redis Geo模块中 “增(GEOADD)” 和 “查(GEORADIUS)” 的详细过程。并可推算出Redis中GEORADIUS查找附近的人功能,时间复杂度为:O(N+log(M))

其中N为指定半径范围内的位置元素数量,而M则是被九宫格圈住计算距离的元素的数量。结合Redis自己基于内存的存储特性,在实际使用过程当中有很是高的运行效率。

来源:饿了么物流团队javascript

https://juejin.im/post/5da40462f265da5baf410a11java

- END -
推荐阅读:

关注 Java技术栈 公众号在后台回复: redis ,可获取一份栈长整理的最新 Redis 教程,都是干货。

640

点击「阅读原文」和栈长学更多~redis

相关文章
相关标签/搜索