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的其他数据类型吗
应该是布隆过滤器做限流吧