用Redis实现“附近的人”功能(redis geo附近的人)
mhr18 2024-11-12 11:15 14 浏览 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 本身基于内存的存储特性,在实际使用过程中有非常高的运行效率。
相关推荐
- Spring Boot3 连接 Redis 竟有这么多实用方式
-
各位互联网大厂的后端开发精英们,在日常开发中,想必大家都面临过系统性能优化的挑战。当系统数据量逐渐增大、并发请求不断增多时,如何提升系统的响应速度和稳定性,成为了我们必须攻克的难题。而Redis,这...
- 隧道 ssh -L 命令总结 和 windows端口转发配置
-
摘要:隧道ssh-L命令总结和windows端口转发配置关键词:隧道、ssh-L、端口转发、网络映射整体说明最近在项目中,因为内网的安全密级比较高,只能有一台机器连接内网数据库,推送...
- 火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer
-
火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer综上所述,面试遇到的所有问题,整理成了一份文档,希望大家能够喜欢!!Java面试题分享(Java中高级核心知识全面解析)一、J...
- 「第五期」游服务器一二三面 秋招 米哈游
-
一面下午2点,35分钟golang内存模型golang并发模型golanggc原理过程channel用途,原理redis数据结构,底层实现跳跃表查询插入复杂度进程,线程,协程kill原理除了kil...
- RMQ——支持合并和优先级的消息队列
-
业务背景在一个项目中需要实现一个功能,商品价格发生变化时将商品价格打印在商品主图上面,那么需要在价格发生变动的时候触发合成一张带价格的图片,每一次触发合图时计算价格都是获取当前最新的价格。上游价格变化...
- Redis 中的 zset 为什么要用跳跃表,而不是B+ Tree 呢?
-
Redis中的有序集合使用的是一种叫做跳跃表(SkipList)的数据结构来实现,而不是使用B+Tree。本文将介绍为什么Redis中使用跳跃表来实现有序集合,而不是B+Tree,并且探讨跳跃表...
- 一文让你彻底搞懂 WebSocket 的原理
-
作者:木木匠转发链接:https://juejin.im/post/5c693a4f51882561fb1db0ff一、概述上一篇文章《图文深入http三次握手核心问题【思维导图】》我们分析了简单的一...
- Redis与Java整合的最佳实践
-
Redis与Java整合的最佳实践在这个数字化时代,数据处理速度决定了企业的竞争力。Redis作为一款高性能的内存数据库,以其卓越的速度和丰富的数据结构,成为Java开发者的重要伙伴。本文将带你深入了...
- Docker与Redis:轻松部署和管理你的Redis实例
-
在高速发展的云计算时代,应用程序的部署和管理变得越来越复杂。面对各种操作系统、依赖库和环境差异,开发者常常陷入“在我机器上能跑”的泥潭。然而,容器化技术的兴起,尤其是Docker的普及,彻底改变了这一...
- Java开发中的缓存策略:让程序飞得更快
-
Java开发中的缓存策略:让程序飞得更快缓存是什么?首先,让我们来聊聊什么是缓存。简单来说,缓存是一种存储机制,它将数据保存在更快速的存储介质中,以便后续使用时能够更快地访问。比如,当你打开一个网页时...
- 国庆临近,字节后端开发3+4面,终于拿到秋招第一个offer
-
字节跳动,先面了data部门,3面技术面之后hr说需要实习转正,拒绝,之后另一个部门捞起,四面技术面,已oc分享面经,希望对大家有所帮助,秋招顺利在文末分享了我为金九银十准备的备战资源库,包含了源码笔...
- “快”就一个字!Redis凭什么能让你的APP快到飞起?
-
咱们今天就来聊一个字——“快”!在这个信息爆炸、耐心越来越稀缺的时代,谁不希望自己手机里的APP点一下“嗖”就打开,刷一下“唰”就更新?谁要是敢让咱用户盯着个小圈圈干等,那简直就是在“劝退”!而说到让...
- 双十一秒杀,为何总能抢到?Redis功不可没!
-
一年一度的双十一“剁手节”,那场面,简直比春运抢票还刺激!零点的钟声一敲响,亿万个手指头在屏幕上疯狂戳戳戳,眼睛瞪得像铜铃,就为了抢到那个心心念念的半价商品、限量版宝贝。你有没有发现一个奇怪的现象?明...
- 后端开发必看!为什么说Redis是天然的幂等性?
-
你在做后端开发的时候,有没有遇到过这样的困扰:高并发场景下,同一个操作重复执行多次,导致数据混乱、业务逻辑出错?别担心,很多同行都踩过这个坑。某电商平台就曾因订单创建接口在高并发时不具备幂等性,用户多...
- 开发一个app需要哪些技术和工具
-
APP开发需要一系列技术和工具的支持,以下是对这些技术的清晰归纳和分点表示:一、前端开发技术HTML用于构建页面结构。CSS用于样式设计和布局。JavaScript用于页面交互和逻辑处理。React...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- 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)