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当前的编码