用Redis实现“附近的人”功能(redis geo附近的人)
mhr18 2024-11-12 11:15 19 浏览 0 评论
针对“附近的人”这一位置服务领域的应用场景,常见的可使用 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。
由于 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,如:
源码分析
PS:此段源码较长,看不下去的可直接看中文注释,或直接跳到小结部分。
上文代码中最核心的步骤有两个,一是“计算中心点范围”,二是“对中心点及其周围 8 个 GEOHASH 网格区域进行查找”。
对应的是如下两个函数:
- geohashGetAreasByRadiusWGS84
- membersOfAllNeighbors
我们依次来看:
①计算中心点范围
// geohash_helper.c
②对中心点及其周围 8 个 GEOHASH 网格区域进行查找
// geo.c
算法小结
抛开众多可选参数不谈,简单总结下 GEORADIUS 命令是怎么利用 GEOHASH 获取目标位置对象的:
- 参数提取和校验。
- 利用中心点和输入半径计算待查区域范围。这个范围参数包括满足条件的最高的 GEOHASH 网格等级(精度)以及对应的能够覆盖目标区域的九宫格位置(后续会有详细说明)。
- 对九宫格进行遍历,根据每个 GEOHASH 网格的范围框选出位置对象。进一步找出与中心点距离小于输入半径的对象,进行返回。
直接描述不太好理解,我们通过如下两张图再对算法进行简单的演示:
令左图的中心为搜索中心,绿色圆形区域为目标区域,所有点为待搜索的位置对象,红色点则为满足条件的位置对象。
在实际搜索时,首先会根据搜索半径计算 GEOHASH 网格等级(即右图中网格大小等级),并确定九宫格位置(即红色九宫格位置信息)。
再依次查找计算九宫格中的点(蓝点和红点)与中心点的距离,最终筛选出距离范围内的点(红点)。
算法分析
为什么要用这种算法策略进行查询,或者说这种策略的优势在哪,让我们以问答的方式进行分析说明。
①为什么要找到满足条件的最高的 GEOHASH 网格等级?为什么用九宫格?
这其实是一个问题,本质上是对所有的元素对象进行了一次初步筛选。在多层 GEOHASH 网格中,每个低等级的 GEOHASH 网格都是由 4 个高一级的网格拼接而成(如图)。
换句话说,GEOHASH 网格等级越高,所覆盖的地理位置范围就越小。当我们根据输入半径和中心点位置计算出的能够覆盖目标区域的最高等级的九宫格(网格)时,就已经对九宫格外的元素进行了筛除。
这里之所以使用九宫格,而不用单个网格,主要原因还是为了避免边界情况,尽可能缩小查询区域范围。
试想以 0 经纬度为中心,就算查 1 米范围,单个网格覆盖的话也得查整个地球区域。而向四周八个方向扩展一圈可有效避免这个问题。
②如何通过 GEOHASH 网格的范围框选出元素对象?效率如何?
首先在每个 GEOHASH 网格中的 GEOHASH 值都是连续的,有固定范围。所以只要找出有序集合中,处在该范围的位置对象即可。
以下是有序集合的跳表数据结构:
其拥有类似二叉查找树的查询效率,操作平均时间复杂性为 O(log(N))。且最底层的所有元素都以链表的形式按序排列。
所以在查询时,只要找到集合中处在目标 GEOHASH 网格中的第一个值,后续依次对比即可,不用多次查找。
九宫格不能一起查,要一个个遍历的原因也在于九宫格各网格对应的 GEOHASH 值不具有连续性。只有连续了,查询效率才会高,不然要多做许多距离运算。
综上,我们从源码角度解析了 Redis Geo 模块中 “增(GEOADD)” 和 “查(GEORADIUS)” 的详细过程。
并可推算出 Redis 中 GEORADIUS 查找附近的人功能,时间复杂度为:O(N+log(M))。
其中 N 为指定半径范围内的位置元素数量,而 M 则是被九宫格圈住计算距离的元素的数量。
结合 Redis 本身基于内存的存储特性,在实际使用过程中有非常高的运行效率。
相关推荐
- Java培训机构,你选对了吗?(java培训机构官网)
-
如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...
- 产品经理MacBook软件清单-20个实用软件
-
三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...
- RAD Studio(Delphi) 本月隆重推出新的版本12.3
-
#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...
- 图解Java垃圾回收机制,写得非常好
-
什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...
- Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷
-
1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...
- 半虚拟化如何提高服务器性能(虚拟化 半虚拟化)
-
半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...
- HashMap底层实现原理以及线程安全实现
-
HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...
- long和double类型操作的非原子性探究
-
前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...
- 数据库DELETE 语句,还保存原有的磁盘空间
-
MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...
- 线程池—ThreadPoolExecutor详解(线程池实战)
-
一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...
- navicat如何使用orcale(详细步骤)
-
前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...
- 你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开
-
各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...
- 大数据正当时,理解这几个术语很重要
-
目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...
- 秒懂列式数据库和行式数据库(列式数据库的特点)
-
行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...
- AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4
-
AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle 空为0 (51)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)