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

Redis核心技术与实战【学习笔记】 - 7.Redis GEO类型 - 面向 LBS 应用的数据类型

前言

前面,介绍了 Redis 的 5 大基本数据类型:String、List、Hash、Set、Sorted Set,它们可以满足绝大多数的数据存储需求,但是在面对海里数据统计时,它们的内存开销很大。所以对于一些特殊的场景,它们是无法支持的。所以,Redis 还提供了 3 种扩展数据类型,分别是 Bitmap、HyperLogLog、GEO。今天再介绍下 GEO。


1.面向 LBS 应用的 GEO 数据类型

日常生活中,“附近停车场”、打车软件的叫车,这些都离不开基于位置信息服务(LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,且要能查询相邻的经纬度范围,Redis 的 GEO 就非常适合应用在 LBS 服务的场景中。

1.1 GEO 底层结构

在设计一个数据类型的底层结构时,首先要知道,要处理的数据有什么访问特点。所以,需要先搞清楚位置信息到底是怎么存取的。

以叫车服务为例,来分析下 LBS 应用中经纬度的存取特点:

  1. 每辆网约车都有一个编号(如 1001),网约车需要将自己的经纬度信息(如 117.273521 , 39.884737)发给叫车应用。
  2. 用户在叫车的时候,叫车应用会根据用户的经纬度信息( 117.273521 , 39.884740)查找用户附件的车辆,并进行匹配。
  3. 等把位置相近的用户和车辆匹配上以后,叫车应用就会根据车辆的编号,获取车辆的信息,并返回给用户。

可以看到,一辆车(或一个用户)对应一组经纬度,并且随着车(或用户)的位置移动,相应的经纬度也会变化。

这种数据属于一个 key (例如车辆 ID) 对应一个 value(一组经纬度)。当有很多车辆信息需要保存时,就需要有一个集合来保存一系列的 key 和 value。Hash 集合类型可以快速存取一系列 key 和 value,正好可以记录一系列车辆 ID 和经纬度的对应关系,如下所示:
在这里插入图片描述
此外,Hash 类型的 HSET 操作,可以快速的更新车辆变化的经纬度信息。

目前看来,Hash 类型是一个不错的选择。但是,一个 LBS 应用除了记录经纬度信息外,还需要根据用户经纬度信息在车辆的 Hash 集合中进行范围查找。一旦涉及到范围查询,就意味着集合中的数据是有序的,但 Hash 类型是无需的,不能满足要求。

Sorted Set 类型也支持一个 key 对应一个 value 的记录模式,其中,key 就是 Sorted Set 中的元素,而 value 则是元素的权重分数。此外,Sorted Set 可以根据元素的权重分数排序,支持范围查询。这就能满足 LBS 服务中查找相邻位置的需求了。

而 GEO 类型的底层数据结构就是用 Sorted Set 来实现的。咱们还是接着叫车应用例子,用 Sorted Set 来保存车辆的经纬度信息时,Sorted Set 的元素是车辆 ID,元素的权重分数是经纬度信息,如下图所示:
在这里插入图片描述
此时问题是,Sorted Set 元素的权重分数是一个浮点数(float 类型),而一组经纬度包含的精度和纬度两个值,是没法直接保存为一个浮点数。这就要用到 GEO 类型中的 GeoHash 编码了。

1.2 GeoHash 编码方法

Redis 采用了 GeoHash 编码方法,这个方法的基本原理就是“二分区间,区间编码”。当我们要对一组经纬度进行 GeoHash 编码时,要先对经度和纬度分别编码,然后把经纬度各自的编码组合成一个最终编码。

首先,看下经度和纬度的单独编码过程。

  1. 对于一个地理位置来说,经度范围是 [-180, 180]。
  2. GeoHash 编码会把一个经度值编码成一个 N 位的二进制值,我们来对经度范围 [-180, 180] 做 N 次的二分区操作,其中 N 可以自定义。
  3. 在进行第一次区分时,经度范围 [-180, 180] 会被分成两个子区间:[-180, 0) 和 [0, 180]。此时,看一下编码的经度值是落在了左分区还是右分区。 如果是落在做分区,我们就用 0 表示;如果落在右分区,就用 1 表示。这样一来,每做完一次二分区,我们就可以得到 1 位编码值。
  4. 再对经度值所属的分区,再做一次二分区,同时再次查看经度落在了新二分区的做分区还是右分区,按照刚才的规则再做 1 位编码。当做完 N次的二分区后,经度值就可以用一个 N bit 的数来表示了。

举个例子,假设我们要编码的经度值是 117.273521 ,我们用 5 位编码值(也就是 N = 5,做 5 次分区)。
5. 先做第一次二分区操作,把经度区间 [-180, 180] 分成两个子区间:[-180, 0) 和 [0, 180],此时,经度值 117.273521 是属于右分区 [0, 180],所以,我们用 1 表示第一次二分区后的编码值。
6. 再做第二次二分区:把经度值 117.273521 所属分区 [0, 180] 区间,分成 [0, 90) 和 [90, 180]。此时经度值 117.273521 还是属于右分区 [90, 180],编码值仍为 1。
7. 第三次二分区,经度值 117.273521 落在了左分区 [90, 135) 中,所以,第三次分区后的编码值就是 0。
8. 第四次二分区,经度值 117.273521 落在了右分区 [112.5, 135]中,所以第四次编码值就是 1。
9. 第五次二分区,经度值 117.273521 落在了左分区 [112.5, 123.75) 中,所以第五次次编码值就是 0。

最终,做完 5 次分区后,我们把经度值 117.273521 的 GeoHash 编码值为 11010

对维度的编码方式,和对经度一样,只是经度的范围是 [-90, 90],GeoHash 编码的值为 10111,下标展示了对纬度值 39.884737 的编码过程。

分区次数最小维度值二分区中间值最大维度值维度39.884737所在区间维度的GeoHash编码
第一次-90090[0, 90]1
第二次04590[0, 45)0
第三次022.545[22.5, 45]1
第四次22.533.7545[33.75, 45]1
第五次33.7539.37545[39.375, 45)1

我们再把一组经纬度值都编完码后,再把它们组合在一起,组合的规则是:

  • 最终编码值的偶数位上依次是经度的编码值
  • 奇数位上依次是纬度的编码值
  • 其中偶数位从 0 开始,奇数位从 1 开始。

我们把刚刚计算的经纬度(117.273521, 39.884737)的各自编码值 11010 和 10111 ,组合之后:

  • 第 0 位 :经度的第 0 位 ,为 1
  • 第 1 位:纬度度的第 0 位,为 1
  • 第 2 位:经度的第 1 位,为 1
  • 依次类推,就能得到最终的编码值: 1 1 1 0 0 1 1 1 0 1(加粗的为经度编码值,其他是纬度编码值)

用了 GeoHash 编码后,原本无法表示权重的经纬度,就可以用 1110011101 这个值来表示,就可以保存为 Sorted Set 的权重分数了。

其实,使用 GeoHash 编码后,就相当于把整个地理空间划分成一个个放个,每个放个对应了一个 GeoHash 中的一个分区。举个例子。 我们把经度区间 [-180,180] 做一次二分区,把维度区间 [-90,90] 做一次二分区间,就会得到 4 个分区。我们看看经度和纬度范围及对应的 GeoHash 组合编码:

  • 分区一:[-180, 0) 和 [-90, 0),编码 00
  • 分区二:[-180, 0) 和 [0, 90),编码 01
  • 分区三:[0, 180) 和 [-90, 0),编码 10
  • 分区三:[0, 180) 和 [0, 90),编码 11

这 4 个分区对应了 4 个方格,每个方格覆盖了一定范围内的经纬度,分区越多,每个方格能覆盖到的地理位置就越小,也就越精准。我们把所有方格的编码值映射到一维空间内,相邻方格的 GeoHash 编码值基本也是接近的,如下所示:
在这里插入图片描述
所以,我们使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用搜索附件人或物的功能了。

不过,需要注意的是,有的编码值虽然在大小上接近,但实际对应的方格确距离比较远。例如,我们用 4 魏来做 GeoHash 编码,把经度区间 [-180,180] 和纬度区间 [-90,90] 分成了 4 个分区,一共 16 个分区。编码值为 0111 和 1000 的两个方格就离的比较远,如下所示:
在这里插入图片描述
所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或 8 个方格。

好了,现在我们知道 GEO 类型是把经纬度所在区间编码作为 Sorted Set 中元素的权重分数,把和经纬度相关的车辆 ID 作为 Sorted Set 中元素本身的值保存下来,这样相邻经纬度的查询就可以通过编码值的大小范围来实现了。

1.3 如何操作 GEO 类型

在使用 GEO 类型的时,我们经常会用到两个命令,分别是 GETADD 和 GEORADIUS。

  • GETADD:把一组经纬度和相对应的一个 ID 记录到 GEO 类型集合中;
  • GEORADIUS:会根据输入的经纬度未知,查找这个纬度为中心的一定范围内的其他元素。

假设车辆 ID 是 1001,经纬度位置是 (117.273521, 39.884737),我们可以用一个 GEO 集合保存所有车辆的经纬度,集合 key 是 cars:locations。执行下面的这个命令,就可以把 ID 号为 1001 的车辆的当前经纬度位置存入 GEO 集合中:

GEOADD cars:locations 117.273521 39.884737 1001

当用户想要查看自己附近的网约车是,LBS 应用就可以使用 GEORADIUS 命令。例如,LBS 应用执行下面的命令时,Redis 会根据输入的用户的经纬度信息( 117.273521 , 39.884740),查找这个经纬度为中心的 5 公里内的车辆信息,并返回给 LBS 应用。

GEORADIUS cars:locations 117.273521 39.884740 5 km ASC COUNT 10

当然,你可以修改 “5” 这个参数,来返回更大或更小范围内的车辆信息。此外,还可以进一步限定返回的车辆信息。

  • 比如我们可以使用 ASC 选项,让返回的车辆按照距离这个中心位置从近到源的方式来排序,以边防选择最近的车辆;
  • 还可以使用 COUNT 选项,指定返回的车辆信息的数量。毕竟,5 公里内的车辆可能有很多,如果返回全部信息,会占用比较多的数据带宽,这个选项可以帮助控制返回的数据量,节省带宽。

可以看到,使用 GEO 数据类型可以非常轻松地操作经纬度这种信息。

相关文章:

  • 深度强化学习(王树森)笔记09
  • Golang的数字签名之旅:crypto/ecdsa库详解
  • 2024美赛数学建模C题思路+模型+代码+论文
  • 提效IntelliJ IDEA插件
  • react经验10:与jquery配合使用
  • 笔记本电脑Win11重装系统教程
  • uniapp H5 px转换rpx
  • K8S网络
  • 与数组相关经典面试题
  • activemq 默认端口说明
  • 路飞项目--05
  • 牛客每日一解
  • 2023年常用网络安全政策标准整合
  • figure方法详解之清除图形内容
  • Springboot项目基础配置:小白也能快速上手!
  • 【面试系列】之二:关于js原型
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • 〔开发系列〕一次关于小程序开发的深度总结
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • Fabric架构演变之路
  • Java IO学习笔记一
  • JS正则表达式精简教程(JavaScript RegExp 对象)
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • Python学习之路13-记分
  • react-native 安卓真机环境搭建
  • RxJS 实现摩斯密码(Morse) 【内附脑图】
  • win10下安装mysql5.7
  • 使用前端开发工具包WijmoJS - 创建自定义DropDownTree控件(包含源代码)
  • 详解移动APP与web APP的区别
  • 新版博客前端前瞻
  • 学习JavaScript数据结构与算法 — 树
  • 译有关态射的一切
  • 用简单代码看卷积组块发展
  • 《天龙八部3D》Unity技术方案揭秘
  • ​比特币大跌的 2 个原因
  • $.ajax中的eval及dataType
  • (3)STL算法之搜索
  • (C++17) optional的使用
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (二) Windows 下 Sublime Text 3 安装离线插件 Anaconda
  • (接口自动化)Python3操作MySQL数据库
  • (三分钟)速览传统边缘检测算子
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (最优化理论与方法)第二章最优化所需基础知识-第三节:重要凸集举例
  • * CIL library *(* CIL module *) : error LNK2005: _DllMain@12 already defined in mfcs120u.lib(dllmodu
  • .NET Core/Framework 创建委托以大幅度提高反射调用的性能
  • .Net MVC4 上传大文件,并保存表单
  • .NET 编写一个可以异步等待循环中任何一个部分的 Awaiter
  • @ModelAttribute注解使用
  • @RequestBody的使用
  • @Transaction注解失效的几种场景(附有示例代码)
  • @Transient注解
  • [.net] 如何在mail的加入正文显示图片
  • [AIGC] 开源流程引擎哪个好,如何选型?
  • [Android]RecyclerView添加HeaderView出现宽度问题