Redis的扩展数据类型

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应用中经纬度的存取特点。

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

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

这种数据记录模式属于一个key对应一个value,当有很多车辆信息要保存时,就需要有一个集合来保存一系列的key和value。Hash集合类型可以快速存取一系列的key和value,正好可以用来记录一系列车辆ID和经纬度的对应关系。Redis的扩展数据类型_第1张图片

同时,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,元素的权重分数时经纬度信息。Redis的扩展数据类型_第2张图片

这时问题来了,Sorted Set元素的权重分数是一个浮点数,而一组经纬度包含的是经纬度的两个值,是没法直接保存为一个浮点数的。

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

以下是经度和纬度单独编码的过程。

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

假设要编码的经度值是116.37,用5位编码值,也就是N=5。得到5位编码值,即11010。Redis的扩展数据类型_第3张图片

对纬度的编码方式,和经度一样,只是纬度的范围是[-90,90],接下来对纬度值39.86进行二分区编码,N=5,得到5位编码值10111。Redis的扩展数据类型_第4张图片

当一组经纬度值都编完码后,再把它们的各自编码组合在一起,组合的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中偶数位从0开始,奇数位从1开始。

刚刚计算的经纬度(116.37,39.86)的各自编码值是11010和10111,组合之后,得到最终编码值1110011101。Redis的扩展数据类型_第5张图片

用了GeoHash编码后,原来无法用一个权重分数表示的一组经纬度(116.37,39.86)就可以用1110011101这一个值来表示,就可以保存位Sorted Set的权重分数了。

使用GeoHash编码后,我们相当于把整个地理空间划分成了一个个方格,每个方格对应了GeoHash中的一个分区。

使用GeoHash编码后,就相当于把整个地理空间划分成一个个方格,每个方格对应了GeoHash中的一个分区。

把经度区间[-180,180]做一次二分区,把纬度区间[-90,90]做一次二分区,就会得到4个分区。

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

这4个分区对应了4个方格,每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有的方格的编码值映射到一维空间时,相邻方格的GeoHash编码值基本上也是接近的。

Redis的扩展数据类型_第6张图片

所以,使用Sorted Set范围查询得到相应编码值,在实际的地理空间上,也是相邻的方格,这就实现了LBS应用搜索附件的人或物的功能了。

有的编码值虽然在大小上接近,但是实际对应的方格却距离比较远。例如用4位来做GeoHash编码,把经度区间[-180,180]和纬度[-90,90]各分成4个分区,一共16个分区,对应了16个方格。编码值0111和1000的两个方格就离得比较远。

所以,为了避免查询不准确的问题,我们可以同时查询给定经度纬度所在的方格周围的4个方格或8个方格。

如何操作GEOO类型?

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

  • GEOADD:用于把一组经纬度信息和相对应的一个ID记录到GEO类型集合中;
  • 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指针。

  • type:表示值的类型,涵盖了五大基本类型;
  • encoding:是值的编码方式,用来表示Redis中实现各个基本类型的底层数据结构,例如SDS、压缩列表、哈希表、跳表等;
  • lru:记录了这个对象最后一次被访问的时间,用于淘汰过期的键值对;
  • refcount:记录了对象的引用此处;
  • *ptr:是指向数据的指针。

RedisObject结构借助*ptr指针,就可以指向不同的数据类型,例如,*ptr指向一个SDS或一个调表,就表示键值对是String类型或Sorted Set类型。所以,在定义了新的数据类型后,也只要在RedisObject中设置好了新类型的type和encoding,再用*ptr指向新类型的实现就行了。

开发一个新的数据类型

开发一个名字叫做NewTypeObject的新数据类型,需要4个步骤。

Redis的扩展数据类型_第7张图片

 

 

你可能感兴趣的:(Redis技术学习,数据库,redis,数据结构)