12.GEO类型到自定义数据类型

首先说本文分为两个部分,上半部分我们讲述Redis中最后一个特殊数据类型,Geo

下半部分,我们讲述如何编写开发自定义的新数据类型,保证不用遭受基本数据类型的限制

首先上半部分的GEO

在日常中,我们经常使用位置信息服务 Location-Based Service,LBS应用访问的数据就是经纬度,要查询相邻的经纬度范围,GEO就是针对其的数据结构

我们首先说下GEO的实现原理

首先说,在其中,每一个用户或者车都有一个自己的坐标,包含经度和纬度..

用户在叫车的时候,应用就会根据用户自己的经纬度位置,进行匹配

应用程序就需要获取车辆的信息,返回给用户

常见的保存方式就是如下,一个Key对应一个value(一组经纬度),当很多的车辆信息需要保存的时候,基本的保存方式如下

图片

但是这遇到一个问题,就是在实际应用中,我们需要获取到离用户最近的几个车辆信息,这就需要对Value进行排序,而基本的经纬度,不方便进行相关的排序

GEO就是讲经纬度转换为了一组方便排序的二进制编码

GEO的解决方案就是二分区间,区间编码

GeoHash编码的时候,要分别对经度和纬度进行编码,然后将各自的编码组合成为一个最终的编码

编码的流程如下

对于一个经度位置信息来说,经度范围是[-180,180],GeoHash编码会将一个经度值编码成为一个N位的二进制编码,N是一个可以设置的数值,那么我们就按照N位对经度范围进行分区操作,这个分区采用的就是二分查找,我们先假设我们的经度为116.37,第一次分区,将[-180,180]分为了[-180,0]和[0,180].如果在左分区,用0表示,如果是右分区,用1表示,这样第一次就是右分区,即1.接下来是第二次编码,将[0,180]再次分为[0,90]和[90,180],那么第二次就落到了[90,180]的区间范围,编码就是1,直到第三次分区,落在了左分区[90,135]中,编码位0

那么这样就基本完成了经度的编码

那么经度我们确定为了11010

维度也是一样的二分法进行划分,假设划分之后经纬度各自的编码位 11010和10111

在之后将各自的编码值组合在一起,最终编码上,偶数位是经度的编码值,奇数位一次是纬度的编码值,其中,偶数位从0开始,奇数位从1开始

那么11010和10111,最终的编码就是1110011101

图片

使用了GeoHash编码之后,就相当于将整个地理空间划分为一个个方格,每个方格都对应了GeoHash的一个分区

举个例子,将经度空间 [-180,180]做一次分区,纬度空间[-90,90]做一次分区,就得到了4个分区

这样,就可以大概的落在哪个位置了

那么说一下Redis中的GEO类型,GEO类型底层利用了Sorted Set,利用GEO换算后的二进制数组作为排序方式,塞入Sorted Set作为元素的权重分数,从而确定编码值的范围查询

具体的操作GEO,常用的命令为GEOADD和GEORADIUS

GEOADD命令,将一组经纬度的信息和一个相对应的ID记录到GEO类型集合中

GEORADIUS命令,根据输入的经纬度位置,查找这个经纬度为中心的范围内其他元素

假设车辆ID为33,经纬度的信息为(116.034,39.03015),那么可以直接用GEO集合保存所有车辆的经纬度信息,集合key是cars:locations,具体命令如下

GEOADD cars:location 116.034 39.03015 33

对应的想要查找附近的网约车的时候,LBS可以使用GEORADIUS命令

GEORADIUS cars:locations 166.05 39.03 5 km ASC COUNT 10

查询上述坐标范围内5km的车辆,并按照从近到远,选择10个最近的车辆

那么这就基本讲述完Redis中的GEO数据类型了

第二,讲述如何自定义数据类型

首先需要明确的是Reids的对象结构 RedisObject,其中Reids键值对中每一个值都是用RedisObject保存的

RedisObject包括了元数据和指针,元数据的功能就是方便区分不同的数据类型,指针用于指向具体的数据类型

而RedisObject内部组成包括了type,encoding ,lru,refcount 4个元数据,1个*ptr指针

type:表示值的类型,涵盖了之前学习的五大基本类型

encoding:值的编码方式,表示Redsi中各个基本类型的底层数据结构

lru:记录了最后一次被访问的时间

refcount,记录了对象的引用计数

*ptr: 指向数据的指针

图片

而在其中,定义了新的数据类型后,只要在设置好心的type和encoding之后,再用*ptr指向新类型的实现

具体的开发步骤可以分为

第一步,定义新数据的底层结构

使用一个文件来保存类型定义,具体定义如下

struct NewTypeObject {

struct NewTypeNode *head;

size_t len;

}NewTypeObject;

NewTypeObject中维护了NewTypeNode作为底层实现

struct NewTypeNode {

long value;

struct NewTypeNode *next;

};

在Node其中,维护了实际的数据value,next指向下一个NewTypeNode结构,这其实就是一个Long类型的单向链表

然后在RedisObject的type属性中,增加这个类型的定义

在Redis的server.h文件中,增加一个OBJ_NEWTYPE的宏定义,在代码中指代NewTypeObject的新类型

#define OBJ_STRING 0    /* String object. */

#define OBJ_LIST 1      /* List object. */

#define OBJ_SET 2       /* Set object. */

#define OBJ_ZSET 3      /* Sorted set object. */

#define OBJ_NEWTYPE 7

然后书写对应的常见和释放函数

createNewTypeObject

robj *createNewTypeObject(void){

NewTypeObject *h = newtypeNew();

robj *o = createObject(OBJ_NEWTYPE,h);

return o;

}

内部调用newtypeNew获取一个指针,然后传入了createObject函数

首先是newtypeNew函数,为新数据类型初始化内存结构的,初始化过程主要是zmalloc做底层结构来分配空间,方便写入数据

NewTypeObject *newtypeNew(void){

NewTypeObject *n = zmalloc(sizeof(*n));

n->head = NULL;

n->len = 0;

return n;

}

这个newTypeNew函数会放在一个对应的文件中,一般为 t_newtype.c文件中

createObject是Redis提供的RedisObject常见函数,参数为数据类型的type和指向数据类型的指针*ptr

释放函数则和创建函数相反

最后开发新类型的命令操作

只需要在t_newtype.c文件中增加命令操作的实现

定义一个ntinsertCommand函数,实现插入

并在server.h文件中,声明我们已经实现的命令,方便在server,c文件中引用这个命令

void ntinsertCommand(client *c)

最后在server.c文件中,将新增的命令和实现的函数关联起来,

struct redisCommand redisCommandTable[] = {

{“ntinsert”,ntinsertCommand,2,”m”,…}

}

这就是一个基本数据类型的保存

总结一下,我们学习了Reids的扩展数据类型GEO,方便记录经纬度信息,广泛的用于LBS服务中,GEO内部使用了Sorted Set作为数据类型

利用了GeoHash编码作为经纬度到Sorted Set中元素权重的转换,本质上就是对二维地图做区间划分,然后进行编码,编码后作为Sorted Set的权重分数进行排序,并且利用了Sorted Set的范围查找特性,实现了搜索附近的需求

最后简要的说了一下如何实现一个自定义的数据类型

最后可以说一下还有用过Reids的其他数据类型吗

应该是布隆过滤器做限流吧

发表评论

邮箱地址不会被公开。 必填项已用*标注