前面,介绍了 Redis 的 5 大基本数据类型:String、List、Hash、Set、Sorted Set,它们可以满足绝大多数的数据存储需求,但是在面对海里数据统计时,它们的内存开销很大。所以对于一些特殊的场景,它们是无法支持的。所以,Redis 还提供了 3 种扩展数据类型,分别是 Bitmap、HyperLogLog、GEO。今天再介绍下 GEO。
日常生活中,“附近停车场”、打车软件的叫车,这些都离不开基于位置信息服务(LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,且要能查询相邻的经纬度范围,Redis 的 GEO 就非常适合应用在 LBS 服务的场景中。
在设计一个数据类型的底层结构时,首先要知道,要处理的数据有什么访问特点。所以,需要先搞清楚位置信息到底是怎么存取的。
以叫车服务为例,来分析下 LBS 应用中经纬度的存取特点:
117.273521
, 39.884737
)发给叫车应用。117.273521
, 39.884740
)查找用户附件的车辆,并进行匹配。可以看到,一辆车(或一个用户)对应一组经纬度,并且随着车(或用户)的位置移动,相应的经纬度也会变化。
这种数据属于一个 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 编码了。
Redis 采用了 GeoHash 编码方法,这个方法的基本原理就是“二分区间,区间编码”。当我们要对一组经纬度进行 GeoHash 编码时,要先对经度和纬度分别编码,然后把经纬度各自的编码组合成一个最终编码。
首先,看下经度和纬度的单独编码过程。
举个例子,假设我们要编码的经度值是 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编码 |
---|---|---|---|---|---|
第一次 | -90 | 0 | 90 | [0, 90] | 1 |
第二次 | 0 | 45 | 90 | [0, 45) | 0 |
第三次 | 0 | 22.5 | 45 | [22.5, 45] | 1 |
第四次 | 22.5 | 33.75 | 45 | [33.75, 45] | 1 |
第五次 | 33.75 | 39.375 | 45 | [39.375, 45) | 1 |
我们再把一组经纬度值都编完码后,再把它们组合在一起,组合的规则是:
我们把刚刚计算的经纬度(117.273521
, 39.884737
)的各自编码值 11010 和 10111 ,组合之后:
1
1 0
0 1
1 1
0 1
(加粗的为经度编码值,其他是纬度编码值)用了 GeoHash 编码后,原本无法表示权重的经纬度,就可以用 1110011101
这个值来表示,就可以保存为 Sorted Set 的权重分数了。
其实,使用 GeoHash 编码后,就相当于把整个地理空间划分成一个个放个,每个放个对应了一个 GeoHash 中的一个分区。举个例子。 我们把经度区间 [-180,180] 做一次二分区,把维度区间 [-90,90] 做一次二分区间,就会得到 4 个分区。我们看看经度和纬度范围及对应的 GeoHash 组合编码:
这 4 个分区对应了 4 个方格,每个方格覆盖了一定范围内的经纬度,分区越多,每个方格能覆盖到的地理位置就越小,也就越精准。我们把所有方格的编码值映射到一维空间内,相邻方格的 GeoHash 编码值基本也是接近的,如下所示:
所以,我们使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用搜索附件人或物的功能了。
不过,需要注意的是,有的编码值虽然在大小上接近,但实际对应的方格确距离比较远。例如,我们用 4 魏来做 GeoHash 编码,把经度区间 [-180,180] 和纬度区间 [-90,90] 分成了 4 个分区,一共 16 个分区。编码值为 0111 和 1000 的两个方格就离的比较远,如下所示:
所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或 8 个方格。
好了,现在我们知道 GEO 类型是把经纬度所在区间编码作为 Sorted Set 中元素的权重分数,把和经纬度相关的车辆 ID 作为 Sorted Set 中元素本身的值保存下来,这样相邻经纬度的查询就可以通过编码值的大小范围来实现了。
在使用 GEO 类型的时,我们经常会用到两个命令,分别是 GETADD 和 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” 这个参数,来返回更大或更小范围内的车辆信息。此外,还可以进一步限定返回的车辆信息。
可以看到,使用 GEO 数据类型可以非常轻松地操作经纬度这种信息。