Redis的5大基本数据类型:String、List、Hash、Set和Sorted Set,它们可以满足大多数的数据存储需求,但是在面对海量数据统计时,它们的内存开销很大,而且对于一些特殊的场景,它们是无法支持的。所以Redis还提供了3种扩展数据类型,分别是Bitmap、HyperLogLog和GEO。
GEO的实现原理和使用方法
GEO是面向LBS(Location Based Server)应用的GEO数据类型。在日常生活中,附近的餐馆、打车软件上叫车都离不开基于位置信息服务的应用。LBS应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO就非常适合应用在LBS服务的场景中。
GEO的底层结构
一般来说,在设计一个数据类型的底层结构时,我们首先需要知道,要处理的数据有什么访问特点。所以,需要先搞清楚位置信息到底是怎么被存取的。
以叫车服务为例,分析一下LBS应用中经纬度的存取特点。
可以看到,一辆车或一个用户对应一组经纬度,并且随着车或用户的位置移动,相应的经纬度也会变化。
这种数据记录模式属于一个key对应一个value,当有很多车辆信息要保存时,就需要有一个集合来保存一系列的key和value。Hash集合类型可以快速存取一系列的key和value,正好可以用来记录一系列车辆ID和经纬度的对应关系。
同时,Hash类型的HSET操作命令,会根据key来设置相应的值,所以可以用它来快速地更新车辆变化的经纬度信息。
但问题是,对于一个LBS应用来说,除了记录经纬度信息,还需要根据用户的经纬度信息在车辆的Hash集合中进行范围查询。一旦涉及到范围查询,就意味着集合中的元素需要有序,但是Hash类型的元素是无序的,显然不能满足要求。
Sorted Set类型也支持一个key对应一个value的记录模式,其中,key就是Sorted Set中的元素,而value则是元素的权重分数,更重要的是,Sorted Set可以根据元素的权重分数排序,支持范围查询,这就能满足LBS服务中查找相邻位置的需求了。
实际上,GEO类型的底层数据结构就是用Sorted Set实现的。用Sorted Set来保存车辆的经纬度信息时,Sorted Set的元素时车辆ID,元素的权重分数时经纬度信息。
这时问题来了,Sorted Set元素的权重分数是一个浮点数,而一组经纬度包含的是经纬度的两个值,是没法直接保存为一个浮点数的。
这就要用到GEO类型中的GeoHash编码了,这个方法的基本原理就是“二分区间,区间编码”。要对一组经纬度进行GeoHash编码时,要先对经度和纬度分别编码,然后再把经纬度各自的编码组合成一个最终编码。
以下是经度和纬度单独编码的过程。
假设要编码的经度值是116.37,用5位编码值,也就是N=5。得到5位编码值,即11010。
对纬度的编码方式,和经度一样,只是纬度的范围是[-90,90],接下来对纬度值39.86进行二分区编码,N=5,得到5位编码值10111。
当一组经纬度值都编完码后,再把它们的各自编码组合在一起,组合的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中偶数位从0开始,奇数位从1开始。
刚刚计算的经纬度(116.37,39.86)的各自编码值是11010和10111,组合之后,得到最终编码值1110011101。
用了GeoHash编码后,原来无法用一个权重分数表示的一组经纬度(116.37,39.86)就可以用1110011101这一个值来表示,就可以保存位Sorted Set的权重分数了。
使用GeoHash编码后,我们相当于把整个地理空间划分成了一个个方格,每个方格对应了GeoHash中的一个分区。
使用GeoHash编码后,就相当于把整个地理空间划分成一个个方格,每个方格对应了GeoHash中的一个分区。
把经度区间[-180,180]做一次二分区,把纬度区间[-90,90]做一次二分区,就会得到4个分区。
这4个分区对应了4个方格,每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有的方格的编码值映射到一维空间时,相邻方格的GeoHash编码值基本上也是接近的。
所以,使用Sorted Set范围查询得到相应编码值,在实际的地理空间上,也是相邻的方格,这就实现了LBS应用搜索附件的人或物的功能了。
有的编码值虽然在大小上接近,但是实际对应的方格却距离比较远。例如用4位来做GeoHash编码,把经度区间[-180,180]和纬度[-90,90]各分成4个分区,一共16个分区,对应了16个方格。编码值0111和1000的两个方格就离得比较远。
所以,为了避免查询不准确的问题,我们可以同时查询给定经度纬度所在的方格周围的4个方格或8个方格。
如何操作GEOO类型?
在使用GEO类型时,我们经常会用到两个命令,分别是GEOADD和GEORADIUS。
假设车辆ID是33,经纬度位置是(116.034579,39.030452),我们可以用一个GEO集合保存所有车辆的经纬度,集合key是cars:locations。
# 保存车辆位置信息
192.168.125.128:7001>geoadd cars:locations 116.034579 39.030452 33
# 通过用户位置信息搜索车辆信息
192.168.125.128:7001>georadius cars:locations 115.054579 39.030452 10 km asc count 10
如何自定义数据类型?
为了实现自定义数据类型,首先需要了解Redis的基本对象结构RedisObject,因为Redis键值对中每一个值都是用RedisObject保存的。
Redis的基本对象结构
RedisObject的内部组成包括了type、encoding、lru和refcount 4个元数据,以及1个*ptr指针。
RedisObject结构借助*ptr指针,就可以指向不同的数据类型,例如,*ptr指向一个SDS或一个调表,就表示键值对是String类型或Sorted Set类型。所以,在定义了新的数据类型后,也只要在RedisObject中设置好了新类型的type和encoding,再用*ptr指向新类型的实现就行了。
开发一个新的数据类型
开发一个名字叫做NewTypeObject的新数据类型,需要4个步骤。