当前位置:首页 > 公众号精选 > 架构师社区
[导读]作者简介万汨,饿了么资深开发工程师。iOS,Go,Java均有涉猎。目前主攻大数据开发。喜欢骑行、爬山。前言:针对“附近的人”这一位置服务领域的应用场景,常见的可使用PG、MySQL和MongoDB等多种DB的空间索引进行实现。而Redis另辟蹊径,结合其有序队列zset以及ge...

作者简介

万汨,饿了么资深开发工程师。iOS,Go,Java均有涉猎。目前主攻大数据开发。喜欢骑行、爬山。

前言:针对“附近的人”这一位置服务领域的应用场景,常见的可使用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

使用方式

GEOADD key longitude latitude member [longitude latitude member ...]
将给定的位置对象(纬度、经度、名字)添加到指定的key。

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

成功插入后的返回值:

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

源码分析

/* GEOADD key long lat name [long2 lat2 name2 ... longN latN nameN] */
void geoaddCommand(client *c) {

//参数校验
    /* Check arguments number for sanity. */
    if ((c->argc - 2) % 3 != 0) {
        /* Need an odd number of arguments if we got this far... */
        addReplyError(c, "syntax error. Try GEOADD key [x1] [y1] [name1] "
                         "[x2] [y2] [name2] ... ");
        return;
    }

//参数提取Redis
    int elements = (c->argc - 2) / 3;
    int argc = 2 elements*2/* ZADD key score ele ... */
    robj **argv = zcalloc(argc*sizeof(robj*));
    argv[0] = createRawStringObject("zadd",4);
    argv[1] = c->argv[1]; /* key */
    incrRefCount(argv[1]);

//参数遍历 转换
    /* Create the argument vector to call ZADD in order to add all
     * the score,value pairs to the requested zset, where score is actually
     * an encoded version of lat,long. */

    int i;
    for (i = 0; i < elements; i ) {
        double xy[2];

    //提取经纬度
        if (extractLongLatOrReply(c, (c->argv 2) (i*3),xy) == C_ERR) {
            for (i = 0; i < argc; i )
                if (argv[i]) decrRefCount(argv[i]);
            zfree(argv);
            return;
        }
    
    //将经纬度转换为52位的geohash作为分值 
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭