redis学习笔记之5大数据类型的基本实现

1.内部编码:

每种数据类型都有不同的编码,在满足一定条件下会进行编码转换

2.各种编码对应的数据结构简单介绍:

A.简单动态字符串(sds,simple dynamic string):

因传统C的字符串不能高效支持长度计算与append操作,故redis采用sds替换C默认的字符串表示

传统C字符串计算长度:strlen(s)复杂度为O(n)

传统C字符串n次append:进行n次内存分配(realloc)

sds的实现:

typedef struct sdshdr{

    int len;//buf已用长度

    int free;//buf剩余可用长度

    char buf[];//实际保存字符串数据的地方

}

因此,sds字符串计算长度复杂度为O(1);append时候,当free‘小于append字符串长度时进行扩容,减少内存分配(分配机制后面介绍)

B.linkedList双端链表:

typedef struct listNode{                                              typedef struct list{

    struct listNode * prev;//前驱结点                                  listNode *head;//表头指针

    struct listNode *next;//后继节点                                   listNode *tail;//表尾指针

    void *value;//节点值                                                    unsigned long len;//节点数量

}listNode;                                                                        void (*dup)(void *ptr);//复制函数

                                                                                       void (*free)(void *ptr);//释放函数

                                                                                       int (*match)(void *ptr,void *key);//对比函数

                                                                                   }list;

性能特征:listNode有prev与next两个指针,可从两个方向进行迭代;list有head何tail两个指针,表头与表尾插入复杂度为O(1),即lpush、lpop、rpoplpush等命令高效的原因;list带len属性,计算链表长度的复杂度为O(1),因此len命令不会成为性能瓶颈

C.字典(k-v映射):

redis字典用途:实现redis数据库键空间、hash的底层实现

字典的实现:

typedef struct dict {

    dictType *type;// 特定于类型的处理函数

    void *privdata;// 类型处理函数的私有数据

    dictht ht[2];// 哈希表(2个,第1个是主hash表,第2个用于rehash)

    int rehashidx;// 记录rehash进度的标志,值为-1 表示rehash 未进行

    int iterators;// 当前正在运作的安全迭代器数量

} dict;

哈希表的实现:

typedef struct dictht {

    dictEntry **table;// 哈希表节点指针数组(俗称桶,bucket)

    unsigned long size;// 指针数组的大小

    unsigned long sizemask;// 指针数组的长度掩码,用于计算索引值

    unsigned long used;// 哈希表现有的节点数量

} dictht;     其中,table属性为数组,数组元素是指向dictEntry结构的指针

dictEntry哈希表节点的实现:

typedef struct dictEntry {

    void *key;// 键

    union { void *val; uint64_t u64; int64_t s64; } v;// 值

    struct dictEntry *next;// 链往后继节点

} dictEntry;

next属性指向下一个dictEntry,多个dictEntry通过next连接成链表,当多个键拥有相同的hash值时,哈希表用链地址法(与jdk1.7的HashMap处理方式一样)来处理键冲突,使用链表将这些键连接起来

因此,整个字典的结构如下:

D.skipList跳跃表:

跳跃表在redis中唯一作用是实现有序集合zset,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的,增、删、查等操作可以在对数期望时间下完成,结构图如下:

    

跳跃表构成:

表头:负责维护跳跃表的节点指针

表尾:全部由null组成,表示跳跃表的末尾

表节点:保存元素值,及多个层

层:保存指向其他元素的指针,高层指针越过的元素数量总是大于等于底层的指针

跳跃表的实现:

typedef struct zskiplist {

    struct zskiplistNode *header, *tail;// 头节点,尾节点

    unsigned long length;// 节点数量

    int level;// 目前表内节点的最大层数

} zskiplist;

跳跃表节点的实现:

typedef struct zskiplistNode {

    robj *obj;// member 对象

    double score;// 分值,允许重复,score相等时还要比较member对象

    struct zskiplistNode *backward;// 后退指针,zrevrange等逆序命令用

    struct zskiplistLevel {

        struct zskiplistNode *forward;// 前进指针

        unsigned int span;// 这个层跨越的节点数量

    } level[];// 层

} zskiplistNode;

E.整数集合intset:

用于有序、无重复地保存多个整数值,会根据元素值自动选择合适长度的整数类型来保存,是集合类型set的底层实现之一,当set只保存整数元素且数量不多时会用intset

intset的实现:

typedef struct intset {

    uintXX_t encoding;// 保存元素所使用的类型的长度

    uintXX_t length;// 元素个数

    intXX_t contents[];// 保存元素的数组,元素不重复且从小到大排序

} intset; (PS:XX可以是16、32、64,添加元素、分配内存由encoding决定)

在添加元素时,若intset当前编码不适用新元素编码,会触发对intset的升级,升级不会改变元素的值,且编码方式由元素中长度最大的那个决定,不可逆,只能由较短编码升级到较长编码,升级会引起intset内存重分配,并移动集合在所有元素,复杂度为O(n),应尽量保持整数范围一致,尽量避免因个别大整数触发升级操作,浪费内存

intset添加元素的执行流程如下:

F.zipList压缩列表:

ziplist 是由一系列特殊编码的内存块构成的列表,可保存字符数组或整数值,是hash、zset、list底层实现之一 分布结构如下图:

zlbytes:整个ziplist列表占用内存数,用于内存重分配、计算末端

zltail:达到ziplist表尾节点的偏移量,可不遍历整个ziplist便弹出尾节点

zllen:ziplist列表的节点数量,该值<65535时表示节点数量,等于时需遍历

entryX:ziplist所保存的节点,节点长度视内容而定

zlend:用于标志ziplist的末端,1字节

zipList节点分布结构:

pre_entry_length:上一个节点长度,从而可通过指针计算跳转到上一个节点

encoding:content部分所保存的数据类型,00、01、10表示保存的是字符数组,11表示保存的是整数

length:content所保存数据的长度

content:保存节点内容,长度与类型由encoding与length决定

使用zipList好处就是可以最大程度上节省内存,适合存储小对象与有限长度数据(512字节以内),但列表长度不可无限制,否则对该列表的操作时间会大大增加,得不偿失

G.redis对象的数据结构:

由于redis的键值可以保存不同类型的值且每种数据类型又对应多种编码,因此需要对键值类型进行检查及"多态"处理,比如:lpush只能用于列表键,del可用于任何键,必须为不同类型的键设置不同的处理方式,因此redis构建了自己的类型系统

redis类型系统的主要功能如下:

a.redisObject的实现:

typedef struct redisObject {

    unsigned type:4;// 类型

    unsigned notused:2;// 对齐位

    unsigned encoding:4;// 编码方式 unsigned

    lru:22;// LRU 时间

    int refcount;// 引用计数

    void *ptr;// 指向对象的值

} robj;

主要属性:

type:对象所保存的值的类型,0-String,1-list,2-set,3-zset,4-hash

encoding:对象所保存的值的编码,0-raw,1-int,2-hashtable,3-zipmap,4-linkedlist,5-ziplist,6-intset,7-skiplist

ptr:指向实际保存值的数据结构,由type与encoding共同决定

lru:记录对象最后一次被访问的时间,用于辅助lru算法删除数据

refcount:当前对象被引用的次数,为0可安全回收

b.基于redisObject对象的类型检查

当执行一个处理数据类型的命令时,redis执行以下步骤:

1.根据给定key,在数据库字典中查找和它像对应的redisObject,如果没找到,就返回NULL;

2.检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;

3.根据redisObject 的encoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构;

4.返回数据结构的操作结果作为命令的返回值

以lpop命令为例,执行步骤如下图:

c.共享对象:

redis内部维护一个0到9999的对象池,可通过redis_shared_integers参数配置,当其他类型(如:list、set、zset、hash)的输入值在该范围内,则该对象值的指针将指向共享对象(PS:共享对象只能被字典和双端链表这类能带有指针的数据结构使用,像整数集合和压缩列表这些只能保存字符串、整数等字面值的内存数据结构,就不能使用共享对象) 对象共享意味着多个引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对象的最后访问时间),因此共享对象池与maxmemory+lru策略冲突,使用需注意

d.对redisObject的分配、共享和销毁机制:

1.每个redisObject 结构都带有一个refcount 属性,指示这个对象被引用了多少次;

2.当新创建一个对象时,它的refcount 属性被设置为1 ;

3.当对一个对象进行共享时,Redis 将这个对象的refcount 增一;

4.当使用完一个对象之后,或者取消对共享对象的引用之后,程序将对象的refcount 减一;

5.当对象的refcount 降至0 时,这个redisObject 结构,以及它所引用的数据结构的内存,都会被释放

3.使用何种编码:

A.对于String类型:

int:8个字节的长整型

embstr:小于等于39个字节的字符串

raw:大于39个字节的字符串

embstr与raw的区别:embstr在创建String对象时,会分配一次空间,包含redisObject与sds,而raw会分配2次空间,分别分配给redisoObject与sds

B.对于hash类型:

当hash类型元素小于hash-max-ziplist-entries配置(默认512个)、且所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现;

当hash类型无法满足ziplist条件时,会使用hashtable编码

C.对于list类型:

当列表的元素个数小于list-max-ziplist-entries配置(默认512个),且列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现;

当列表类型无法满足ziplist条件时会使用linkedlist作为内部实现

D.对于set类型:

当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现;

当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现

F.对于zset类型:

当有序集合的元素个数小于zset-max-ziplistentries配置(默认128个)且每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现;

当ziplist条件不满足时,有序集合会使用skiplist作为内部实现

可使用object encoding key 查看指定key当前的编码

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章