针对“附近的人”这一位置服务领域的应用场景,常见的可使用 PG、MySQL 和 MongoDB 等多种 DB 的空间索引进行实现。
![91263346f9afee36c235244265a85ae4.png](https://i-blog.csdnimg.cn/blog_migrate/9168038c63c3cb4ea83ecb05636aff28.jpeg)
图片来自 Pexels
而 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