当前位置: 首页 > article >正文

Redis源码剖析之GEO——Redis是如何高效检索地理位置的?

Redis GEO 用做存储地理位置信息,并对存储的信息进行操作。通过geo相关的命令,可以很容易在redis中存储和使用经纬度坐标信息。Redis中提供的Geo命令有如下几个:

  • geoadd:添加经纬度坐标和对应地理位置名称。
  • geopos:获取地理位置的经纬度坐标。
  • geodist:计算两个地理位置的距离。
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
  • geohash:计算一个或者多个经纬度坐标点的geohash值。

要理解Redis的GEO相关的命令是如何实现了,就得先理解geohash的原理,本质上这些命令就是对geohash数据的封装而已。

geohash

geohash是2008年Gustavo Niemeye发明用来编码经纬度信息的一种编码方式,比如北京市中心的经纬度坐标是116.404844,39.912279,通过12位geohash编码后就变成了wx4g0cg3vknd,它究竟是如何实现的?其实原理非常简单,就是二分,整个编码过程可以分为如下几步。

1. 转二进制

上过初中地理的我们都知道,地球上如何一个点就可以标识为某个经纬度坐标,经度的取值范围是东经0-180度和西经0-180度,维度的取值范围是北纬0到90和南纬0-90度。去掉东西南北,可以分别认为经度和维度的取值范围为[-180,180]和[-90,90]。

我们先来看经度,[-180,180]可以简单分成两个部分[-180,0]和[0,180],对于给定的一个具体值,我们用一个bit来标识是在[-180,0]还是[0,180]区间里。然后我们可以对这两个子区间继续细分,用更多的bit来标识是这个值是在哪个子区间里。就好比用二分查找,记录下每次查找的路径,往左就是0往右是1,查找完后我们就会得到一个0101的串,这个串就可以用来标识这个经度值。

同理维度也是一样,只不过他的取值返回变成了[-90,90]而已。通过这两种方式编码完成后,任意经纬度我们都可以得到两个由0和1组成的串。
比如还是北京市中心的经纬度坐标 116.404844,39.912279,我们先对116.404844做编码,得到其二进制为:

11010010110001101101

然后我们对维度39.912279编码得到二进制为:

10111000110000111001  
2. 经纬度二进制合并

接下来我们只需要将上述二进制交错合并成一个即可,这里注意经度占偶数位,纬度占奇数位,得到最终的二进制。

1101101110000200111100000001111011010011  
3. 将合并后的二进制做base32编码

最后我们将合并后的二进制做base32编码,将连续5位转化为一个0-31的十进制数,然后用对应的字符代替,将所有二进制位处理完后我们就完成了base32编码。编码表如下:

在这里插入图片描述

最终得到geohash值 wx4g0cg3vknd
在这里插入图片描述

geohash是将空间不断的二分,然后将二分的路径转化为base32编码,最后保存下来,从原理可以看出,geohash表示的是一个区间,而不是一个点,geohash值越长,这个区间就越小,标识的位置也就越精确,下图是维基百科中不同长度geohash下的经纬度误差(lat:维度,lng:经度)

在这里插入图片描述

geohash的用途及问题

geohash成功的将一个二维信息编码成了一个一维信息,这样编码我觉得有两个好处:1. 编码后数据长度变短,利于节省存储。2. 利于使用前缀检索。我们来详细说下第二点。

从上文中geohash的实现来看,只要两个坐标点的geohash有共同的前缀,你们我们就可以肯定这两个点在同一个区域内 (区域大小取决于共同前缀的长度)。这种特性给我们带来的好处就是,我们可以把所有坐标点按geohash做增序索引,然后查找的时候按前缀筛选,大幅提升检索的性能。

举个例子,假设我要找北京国贸附近3公里内的餐馆,已知国贸的geohash是wx4g41,那我也很轻易就可以计算出来我需要扫描哪些区域内的点。但有个点需要注意,上文我已经提到过,geohash值实际上是代表一个区域,而不是一个点,找到一批候选点之后还需要遍历一次计算下精确距离。

在这里插入图片描述

geohash有个需要注意的问题。geohash是将二维的坐标点做了线下编码(如下图),有时候可能会给人一个误解就是如果两个geohash之间二进制的差异越小,这两个区间距离就越近,这完全是错误的,比如如下图0111和1000,这俩区间二进制只差0001但实际物理距离比较远。
在这里插入图片描述

如果上图还不明显的话,我从Wikipedia上拿到一张图,虚线是线性索引的路径,被虚线链接的两个块geohash值是非常相近的,如下图的(7,3)和(0,4),geohash值会非常相近,但实际物理距离非常远,这就是 geohash的突变现象,这也导致了不能直接根据geohash的值来直接判定两个区域的距离大小。
在这里插入图片描述

但在实际使用geohash过程中,时常会遇到跨域搜索的情况,比如我要在上图(3,3)这个区间内某个点上搜索距它1个距离单位的所有其他点集,这个点集有可能横跨(3,3)加上它周围8个邻域的9个区间,突变的问题会导致这9个区间的geohash不是线性跳转的,但也不是没法计算,实际上可以通过特殊的位运算可以很轻易计算出某个geohash的8个邻域,具体可参考redis源码中src/geohash.c中geohashNeighbors()的具体实现,geohashNeighbors使用了geohash_move_x和geohash_move_y两个函数实现了geohash左右和上下的移动,这样可以很容易组合出8个邻域的geohash值了。

static void geohash_move_x(GeoHashBits *hash, int8_t d) {if (d == 0)return;uint64_t x = hash->bits & 0xaaaaaaaaaaaaaaaaULL;uint64_t y = hash->bits & 0x5555555555555555ULL;uint64_t zz = 0x5555555555555555ULL >> (64 - hash->step * 2);if (d > 0) {x = x + (zz + 1);} else {x = x | zz;x = x - (zz + 1);}x &= (0xaaaaaaaaaaaaaaaaULL >> (64 - hash->step * 2));hash->bits = (x | y);
}static void geohash_move_y(GeoHashBits *hash, int8_t d) {if (d == 0)return;uint64_t x = hash->bits & 0xaaaaaaaaaaaaaaaaULL;uint64_t y = hash->bits & 0x5555555555555555ULL;uint64_t zz = 0xaaaaaaaaaaaaaaaaULL >> (64 - hash->step * 2);if (d > 0) {y = y + (zz + 1);} else {y = y | zz;y = y - (zz + 1);}y &= (0x5555555555555555ULL >> (64 - hash->step * 2));hash->bits = (x | y);
}

Geo in redis

上文中花了大量篇幅讲解了geohash的实现,其实看到这里,你基本上已经理解了redis中的geohash的实现了。本质上redis中的geo就是对geohash的封装,具体geohash相关的代码就不给大家列了(可自行查阅),就给大家介绍下redis geo里的大体流程。
首先,可能大家最好奇的是geohash在redis中是怎么存储的,从geoadd命令的实现可以一窥端倪。

/* GEOADD key [CH] [NX|XX] long lat name [long2 lat2 name2 ... longN latN nameN] */
void geoaddCommand(client *c) {int xx = 0, nx = 0, longidx = 2;int i;/* 解析可选参数 */while (longidx < c->argc) {char *opt = c->argv[longidx]->ptr;if (!strcasecmp(opt,"nx")) nx = 1;else if (!strcasecmp(opt,"xx")) xx = 1;else if (!strcasecmp(opt,"ch")) {}else break;longidx++;}if ((c->argc - longidx) % 3 || (xx && nx)) {/* 解析所有的经纬度值和member,并对其个数做校验 */addReplyErrorObject(c,shared.syntaxerr);return;}/* 构建zadd的参数数组 */int elements = (c->argc - longidx) / 3;int argc = longidx+elements*2; /* ZADD key [CH] [NX|XX] score ele ... */robj **argv = zcalloc(argc*sizeof(robj*));argv[0] = createRawStringObject("zadd",4);for (i = 1; i < longidx; i++) {argv[i] = c->argv[i];incrRefCount(argv[i]);}/* 以3个参数为一组,将所有的经纬度和member信息从参数列表里解析出来,并放到zadd的参数数组中 */for (i = 0; i < elements; i++) {double xy[2];if (extractLongLatOrReply(c, (c->argv+longidx)+(i*3),xy) == C_ERR) {for (i = 0; i < argc; i++)if (argv[i]) decrRefCount(argv[i]);zfree(argv);return;}/* 将经纬度坐标转化成score信息 */GeoHashBits hash;geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash);GeoHashFix52Bits bits = geohashAlign52Bits(hash);robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits));robj *val = c->argv[longidx + i * 3 + 2];argv[longidx+i*2] = score;argv[longidx+1+i*2] = val;incrRefCount(val);}/* 转化成zadd命令所需要的参数格式*/replaceClientCommandVector(c,argc,argv);zaddCommand(c);
}

原来geo的存储只是zset包了一层壳(是不是有点小失望),关于zset的具体实现可以参考我之前写的文章redis中skiplist的实现。

我们再来详细看下georadius的大体执行流程(代码偏长,故删除大量细节代码)。

void georadiusGeneric(client *c, int srcKeyIndex, int flags) {robj *storekey = NULL;int storedist = 0; /* 0 for STORE, 1 for STOREDIST. *//* 根据key找找到对应的zojb */robj *zobj = NULL;if ((zobj = lookupKeyReadOrReply(c, c->argv[srcKeyIndex], shared.emptyarray)) == NULL ||checkType(c, zobj, OBJ_ZSET)) {return;}/* 解析请求中的经纬度值 */int base_args;GeoShape shape = {0};if (flags & RADIUS_COORDS) {/** 各种必选参数的解析,省略细节代码,主要是解析坐标点信息和半径   */ }/* 解析所有的可选参数. */int withdist = 0, withhash = 0, withcoords = 0;int frommember = 0, fromloc = 0, byradius = 0, bybox = 0;int sort = SORT_NONE;int any = 0; /* any=1 means a limited search, stop as soon as enough results were found. */long long count = 0;  /* Max number of results to return. 0 means unlimited. */if (c->argc > base_args) {/** 各种可选参数的解析,省略细节代码   */ }/* Get all neighbor geohash boxes for our radius search* 获取到要查找范围内所有的9个geo邻域 */GeoHashRadius georadius = geohashCalculateAreasByShapeWGS84(&shape);/* 创建geoArray存储结果列表 */geoArray *ga = geoArrayCreate();/* 扫描9个区域中是否有满足条的点,有就放到geoArray中 */membersOfAllNeighbors(zobj, georadius, &shape, ga, any ? count : 0);/* 如果没有匹配结果,返回空对象 */if (ga->used == 0 && storekey == NULL) {addReply(c,shared.emptyarray);geoArrayFree(ga);return;}long result_length = ga->used;long returned_items = (count == 0 || result_length < count) ?result_length : count;long option_length = 0;/* * 后续一些参数逻辑,比如处理排序,存储……*/// 释放geoArray占用的空间 geoArrayFree(ga);
}

上述代码删减了大量细节,有兴趣的同学可以自行查阅。不过可以看出georadius的整体流程非常清晰。

  1. 解析请求参数。
  2. 计算目标坐标所在的geohash和8个邻居。
  3. 在zset中查找这9个区域中满足距离限制的所有点集。
  4. 处理排序等后续逻辑。
  5. 清理临时存储空间。

结语

由于文章篇幅有限,而且着重讲解了geohash的实现,并未展开讲解redis中geo相关的各种细节,如读者有兴趣可以详细阅读redis中的src/geo.c了解各类细节。

参考资料

  • wikipedia geohash
  • Geohash算法原理及实现

本文是Redis源码剖析系列博文,同时也有与之对应的Redis中文注释版,有想深入学习Redis的同学,欢迎star和关注。
Redis中文注解版仓库:https://github.com/xindoo/Redis
Redis源码剖析专栏:https://zxs.io/s/1h
本文来自https://blog.csdn.net/xindoo



喜欢的朋友记得点赞、收藏、关注哦!!!

相关文章:

Redis源码剖析之GEO——Redis是如何高效检索地理位置的?

Redis GEO 用做存储地理位置信息&#xff0c;并对存储的信息进行操作。通过geo相关的命令&#xff0c;可以很容易在redis中存储和使用经纬度坐标信息。Redis中提供的Geo命令有如下几个&#xff1a; geoadd&#xff1a;添加经纬度坐标和对应地理位置名称。geopos&#xff1a;获取…...

【Java 优选算法】模拟

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 模拟算法的思路比较简单,根据题目描述列出流程,找出规律,将流程转化为代码 替换所有的问号 题目链接 解法 直接根据题目给出条件模拟 示例,找出规律 1.先找出字符?,再…...

@RequiredArgsConstructor 和 @Autowired区别

1、注入方式 RequiredArgsContructor&#xff1a;通过构造函数的方式实现依赖注入。该注解会被final修饰&#xff0c;并将依赖对象通过构造参数进行注入。 Autowired&#xff1a;通过属性注入的方式实现依赖注入&#xff0c;将依赖对象自动注入到被该注解的字段上 2、使用场景…...

【Linux网络】数据链路层 其他常见的协议

目录 1. 认识以太网 2. 以太网帧格式 3. MTU 4. ARP协议 4.1 ARP数据报的格式 4.2 ARP攻击 5. 其他重要的协议或技术 5.1 DNS协议 5.2 ICMP协议 5.3 NAT技术 5.4 代理服务器 5.5 内网穿透 总结 针对数据在网络传输中所遇到的问题&#xff0c;网络协议栈都对相应的…...

C语言综合案例:学生成绩管理系统

C语言综合案例&#xff1a;学生成绩管理系统 需求 1.存储最多50名学生的信息&#xff08;不使用结构体&#xff09; 2.每个学生包含&#xff1a; 学号&#xff08;字符数组&#xff09;姓名&#xff08;字符数组&#xff09;3门课程成绩&#xff08;一维数组&#xff09; …...

Ubuntu 安装 Nginx并配置反向代理

Ubuntu版本&#xff1a;Ubuntu 24.04.2 LTS 一、安装Nginx ​更新系统软件包​ 安装前需确保系统处于最新状态&#xff0c;避免依赖冲突 sudo apt update && sudo apt upgrade -y ​安装Nginx主程序​ Ubuntu官方仓库已包含稳定版Nginx&#xff0c;直接安装即可 sudo…...

赋能农业数字化转型 雏森科技助力“聚农拼”平台建设

赋能农业数字化转型&#xff0c;雏森科技助力“聚农拼”平台建设 在数字化浪潮席卷各行业的今天&#xff0c;农业领域也在积极探索转型升级之路。中农集团一直以“根植大地&#xff0c;服务三农”为核心&#xff0c;以“乡村振兴&#xff0c;农民增收”为目标&#xff0c;及时…...

安全面试5

文章目录 sql的二次注入在linux下&#xff0c;现在有一个拥有大量ip地址的txt文本文档&#xff0c;但是里面有很多重复的&#xff0c;如何快速去重&#xff1f;在内网渗透中&#xff0c;通过钓鱼邮件获取到主机权限&#xff0c;但是发现内网拦截了tcp的出网流量&#xff0c;聊一…...

halcon三维点云数据处理(二十六)reduce_object_model_3d_to_visible_parts

目录 一、reduce_object_model_3d_to_visible_parts代码第一部分二、reduce_object_model_3d_to_visible_parts代码第二部分三、reduce_object_model_3d_to_visible_parts代码第三部分四、reduce_object_model_3d_to_visible_parts代码第四部分五、效果图一、reduce_object_mod…...

1. HTTP 数据请求

相关资源&#xff1a; 图片素材&#x1f4ce;图片素材.zip 接口文档 1. HTTP 数据请求 什么是HTTP数据请求&#xff1a; (鸿蒙)应用软件可以通过(鸿蒙)系统内置的 http 模块 和 Axios&#xff0c;通过 HTTP 协议和服务器进行通讯 学习核心Http请求技术: Http模块 - 属于鸿…...

MySQL表空间管理

表空间的定义 表空间是数据库用来管理数据库中的表、索引、列等数据对象的一个逻辑意义上的容器&#xff0c;与表空间在物理层对应的是一个一个的具体的数据文件。管理员直接在逻辑上操作一个一个表或者索引等对象&#xff0c;具体的数据文件的调整则由数据库的存储引擎层来…...

VSCode轻松调试运行.Net 8.0 Web API项目

1.背景 我一直都是用VS来开发.NetCore项目的&#xff0c;用的比较顺手&#xff0c;也习惯了。看其他技术文章有介绍VS Code更轻量&#xff0c;更方便。所以我专门花时间来使用VS Code&#xff0c;看看它是如何调试代码、如何运行.Net 8.0 WebAPI项目。这篇文章是一个记录的过程…...

Tailwind CSS_现代 Web 开发的实用指南

1. 引言 1.1 Tailwind CSS 概述 什么是 Tailwind CSS? Tailwind CSS 是一种低级优先级的实用工具优先(utility-first)CSS 框架,它提供了一组灵活的基础类来构建自定义设计。与传统的 CSS 框架不同,Tailwind 不提供预设的组件样式或布局,而是通过组合简单的 CSS 类来实…...

【FAQ】HarmonyOS SDK 闭源开放能力 —Push Kit(9)

1.问题描述&#xff1a; 通过push token向鸿蒙手机推送一条通知&#xff0c;收到通知后&#xff0c;通知右侧不展示图片。 解决方案&#xff1a; 检查一下是否存在图片风控&#xff1a;https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/push-image-co…...

2024年国赛高教杯数学建模D题反潜航空深弹命中概率问题解题全过程文档及程序

2024年国赛高教杯数学建模 D题 反潜航空深弹命中概率问题 原题再现 应用深水炸弹&#xff08;简称深弹&#xff09;反潜&#xff0c;曾是二战时期反潜的重要手段&#xff0c;而随着现代军事技术的发展&#xff0c;鱼雷已成为现代反潜作战的主要武器。但是&#xff0c;在海峡或…...

将宇宙不同温度下的能量表现形式 类比为量子计算机的波函数解码过程

以下是基于您提出的核心观点&#xff08;将宇宙不同温度下的能量表现形式类比为量子计算机的波函数解码过程&#xff09;撰写的论文框架设计&#xff0c;包含创新性理论与跨学科研究方法&#xff1a; --- **标题** 《量子信息视角下的宇宙热力学&#xff1a;从普朗克温度到…...

矩阵 trick 系列 题解

1.AT_dp_r Walk&#xff08;矩阵图论&#xff09; 题意 一个有向图有 n n n 个节点&#xff0c;编号 1 1 1 至 n n n。 给出一个二维数组 A 1... n , 1... n A_{1...n,1...n} A1...n,1...n​&#xff0c;若 A i , j 1 A_{i,j}1 Ai,j​1 说明节点 i i i 到节点 j j j …...

obj离线加载(vue+threejs)+apk方式浏览

demo需求&#xff1a;移动端&#xff0c;实现obj本地离线浏览 结合需求&#xff0c;利用&#xff08;vue2threejs173&#xff09;进行obj的加载&#xff0c;然后采用apk方式&#xff08;hbuilderX打包发布&#xff09;移动端浏览&#xff1b; https://github.com/bianbian886/…...

关于mysql 表中字段存储JSON对象对JSON对象中的bolean字段进行查询的方式

业务场景如题 JSON对象为 表为客诉表中的 发现利用原有的xml中的 and a1.order_list ->‘$[*].isZg’ request.isZg 后续发现需要更改为有效 本文作为自己日常工作记录用&#xff0c;有遇到相同问题的可以作为参考。...

WordPress Course Booking System SQL注入漏洞复现 (CVE-2025-22785)(附脚本)

免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 0x0…...

Kylin麒麟操作系统 | 系统监控

以下所使用的环境为&#xff1a; 虚拟化软件&#xff1a;VMware Workstation 17 Pro 麒麟系统版本&#xff1a;Kylin-Server-V10-SP3-2403-Release-20240426-x86_64 一、系统状态查询工具 1. 静态显示系统进程信息ps ps命令会生成一个静态列表&#xff0c;列表中显示的进程其…...

vLLM服务设置开机自启动(Linux)

要在开机时进入指定的 conda 环境并启动此 vllm 服务&#xff0c;您可以通过以下步骤设置一个 systemd 服务来自动执行脚本。 一、第一步&#xff1a;创建一个启动脚本 1.打开终端并创建启动脚本&#xff0c;例如 /home/username/start_vllm.sh&#xff08;请替换 username 为…...

MongoDB#Code和Function

背景 在MongoDB Shell中, 使用db.system.js.inertOne 新增一个自定义函数后&#xff0c;读取值类型显示Code Class&#xff0c;该如何使用&#xff1f;Code类型和Function能互相转换吗&#xff1f; 实践 // 保存一个函数到 system.js 集合 db.system.js.insertOne({_id: &qu…...

MT-Metrics

MT-Metrics 是一类用于评估生成文本质量的指标&#xff0c;最初用于机器翻译任务&#xff0c;后来扩展到生成任务&#xff08;如对话生成、文本摘要等&#xff09;。它的核心思想是通过比较生成文本与参考文本之间的相似性&#xff08;如词汇重叠、句法结构、语义相似性&#x…...

几个api

几个api 原型链 可以阅读此文 Function instanceof Object // true Object instanceof Function // true Object.prototype.isPrototypeOf(Function) // true Function.prototype.isPrototypeOf(Object) // true Object.__proto__ Function.prototype // true Function.pro…...

数字IC后端设计实现OCC(On-chip Clock Controller)电路介绍及时钟树综合案例

数字IC后端时钟树综合专题&#xff08;OCC电路案例分享&#xff09; 复杂时钟设计时钟树综合(clock tree synthesis)常见20个典型案例 1、什么是OCC&#xff1f; 片上时钟控制器(On-chip Clock Controllers &#xff0c;OCC)&#xff0c;也称为扫描时钟控制器(Scan Clock Con…...

SurfaceFlinger代码笔记

drawLayers是做client合成&#xff0c;合成完以后的buffer会放在RenderSurface里 FrameBufferSurface里的buffer是通过setClientTarget给到HWC的&#xff08;HWC应该给client合成的buffer留了一个slot) Output.cpp这个文件非常关键&#xff0c;代表着具体一个Display的操作 d…...

Trae根据原型设计稿生成微信小程序密码输入框的踩坑记录

一、需求描述 最近经常使用Trae生成一些小组件和功能代码&#xff08;对Trae赶兴趣的可以看之前的文章《TraeAi上手体验》&#xff09;&#xff0c;刚好在用uniapp开发微信小程序时需要开发一个输入密码的弹框组件&#xff0c;于是想用Trae来实现。原型设计稿如下&#xff1a;…...

软件测试丨Docker与虚拟机架构对比分析

Docker 与虚拟机&#xff08;VM&#xff09;在架构上有显著区别&#xff0c;主要体现在资源利用、性能、隔离性和启动时间等方面。以下是两者的主要架构区别&#xff1a; 1. 架构层次 Docker: 主机操作系统&#xff1a;Docker 直接运行在宿主机的操作系统上。Docker 引擎&…...

在VsCode中选择conda编译器环境

当vscode出现始终在激活一个已经不存在的虚拟环境&#xff0c;可选择手动将其调换 在 Visual Studio Code (VSCode) 中选择 Python 虚拟环境的步骤如下&#xff1a; 确保安装了 Python 插件&#xff1a;首先&#xff0c;你需要确保已经安装了适用于 VSCode 的 Python 插件。你…...