0 基本数据类型应用场景
0.1 string
1.计数器
2.记录用户的token
0.2 list
1.消息队列
2.文章列表
0.3 set
1.标签。例如博客网站中使用的标签:mysql,java,计网等。发布的文章可以指定属于某个标签
2.朋友圈点赞,用户的好友关注,两用户的共同关注
0.4 zset
1.排行榜:按照时间,播放量,点赞数对视频,帖子等排名
2.做带权重的队列。普通消息score=1,重要消息score=2。工作线程可以选择按score的倒序获取工作任务。
3.商品评价标签,(好,中,差)
0.5 hash
购物车,对象缓存
注意:本文中redis版本为2.9
1. redis的存储方式
redis是key-value存储系统,key类型一般为字符串,value类型为RedisObject对象
redis是键值对存储的方式,每存储一条数据,redis至少产生2个对象,一个是redisObject,另一个是具体存储的数据
redisObject,用来描述具体数据的类型,比如用的是哪种数据类型,底层用了哪种数据结构
2. redis为什么要使用2个对象,好处
1.redis在执行命令的时候,可以通过redisObject的类型和编码确定是否可以执行相应的命令,不用等操作具体的数据时才发现不行
2.可以基于redisObject中的refcount 引用计数进行内存回收机制,自动释放对象所占用的内存
3.redis可以根据lru 记录最后一次访问时间,针对时间较长对象的进行删除
4.针对不同的场景,可以为对象设置不同的数据结构,从而优化了对象在不同场景下的使用效率
5.可以让多个数据库来共享一个对象来节省空间
3. redisObject对象解析
typedef struct redisObject{
//当前值对象的数据类型
unsigned type:4;
//当前值对象的底层编码类型
unsigned encoding:4;
//对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS
//引用计数
int refcount
//指向底层实现数据结构的指针
void *ptr;
}
解释上文中的字段
- type
value对象的数据类型:(string,hash,list,set,zset)
- encoding
当前值对象底层编码的实现方式。不同type对象对应不同的编码。每种对象至少对应了两种编码。
每种编码对应一种底层数据结构
- lru
记录此对象最后一次访问的时间。当redis内存回收算法设置为volatile-lru或allkeys-lru时,redis会优先释放最久没有被访问的数据
- refcount
用于共享计数,当refcount=0时,表示没有其他对象引用,可以释放此对象
- ptr
指向对象的底层实现数据结构
4. String类型
redis设计了可变的字符串长度对象,叫SDS(simple dynamic string)
简单动态字符串结构体
struct sdshdr{
//记录buf数组中已存的字节数量,也就是sds保存的字符串长度
int len;
// buf数组中剩余可用的字节数量
int free;
//字节数组,用来存储字符串的
char buf[];
}
这样设计的优点:
1.杜绝缓冲区溢出。SDS在合并字符串时,sds api会先检查空间是否满足需求,如果满足,直接执行修改操作;如果不满足,将空间修改至满足需求的大小,然后再执行修改操作。在C语言中进行两个字符串拼接时可以使用strcat函数,如果没有足够的内存空间,会造成缓冲区溢出
2.空间预分配
2.1如果修改后sds的len小于1MB,程序会给free分配与len相同的空间。即若len=600k,则free=600k
2.2惰性空间释放,当缩短sds的存储内容时,不会立即使用内存重分配来回收字符串缩短后的空间,通过free接收len中多余的空间。真正需要释放内存的时候,通过调用api来释放内存
2.3redis有效的减少了执行字符串增长所需要的内存分配次数,C语言字符串不记录字符串长度,如果要修改字符串需要重新分配内存
2.4如果修改后len大于1MB,则给free分配1MB空间
3.结构体中定义了len,获取字符串长度的时间复杂度O(1)。C语言中是O(n)
4.每个字符串都是以空字符串结尾,重用了C语言库中<string.h>中一部分函数
5.二进制安全。C语言中字符串以空字符串作为字符串结束的标识,对于一些二进制文件(如图片),内容可能包括空字符串,会导致C语言中字符串无法正确存取。SDS中的API都是以二进制的方式处理buf中的元素,并且SDS不是以空字符串来判断是否结束,而是以len表示的长度判断字符串是否结束。
4.1 int整数值实现
如果一个字符串对象存储的数据是整数,且这个整数值可以用long类型表示时,字符串对象会将整数值 保存在字符串对象结构的ptr属性中(将void * 转换成long)。并将字符串对象的编码设置为int。
4.2 embstr
当存储的数据是字符串时,且字节数<=32,使用embstr编码(3.2版本之前字节数<=39,3.2版本之后字节数<=44)
解析
优点:
1.相比于raw编码调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,embstr编码只调用一次内存分配函数,其分配了一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。
2.释放embstr编码的字符串对象 只需调用一次内存释放函数
3.所有数据存放在一块连续的内存中,可以更好的利用缓存带来的优势
缺点:
1.当字符串增加时,其长度会增加,需要重新分配内存,导致redisObject和sdshdr都需要重新分配空间,影响性能。所以,用embstr实现string时,只允许读。如果要修改数据,编码格式转为raw。
2.由于是连续的空间,只适合长度较小的字符串
4.3 raw
当存储的数据是字符串时,且字节数>32,使用embstr编码(3.2版本之前字节数>39,3.2版本之后字节数>44)
4.4 关于为什么<=44字节用emstr编码(32,39,44随版本变化)
此处以redis版本3.2之后解释
redisObject结构
/*
* Redis 对象
*/
typedef struct redisObject {
// 类型 4bits
unsigned type:4;
// 编码方式 4bits
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock) 24bits
unsigned lru:22;
// 引用计数 Redis里面的数据可以通过引用计数进行共享 32bits
int refcount;
// 指向对象的值 64-bit
void *ptr;
} robj;
redisObject占用空间
1byte = 8bits
4+4+24+32+64 = 128bits = 16字节
sdshdr8占用空间
1(uint8_t)+1(uint8_t)+1(unsigned char)+1(buf[]中结尾的'\0'字符) = 4字节
初始最小分配为64字节 所以只分配一次空间的embstr最大为64 - 16 - 4 = 44字节
5. List类型
5.0 各版本底层数据结构的变化
Redis3.2之前的底层实现方式:压缩列表ziplist 或者 双向循环链表linkedlist
当list存储的数据量比较少且同时满足一下条件时,list底层使用ziplist数据结构存储数据
- list中保存的每个元素的长度小于64字节
- list中的数据个数少于512个
Redis3.2及之后的底层实现方式(redis6.4之后取消):quicklist
quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和压缩链表的优点
Redis5.0引入listpack
List结构体
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 存储的数据
void *value;
} listNode;
typedef struct list {
// 链表头节点
listNode *head;
// 链表尾节点
listNode *tail;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 链表所有节点数量
unsigned long len;
} list;
5.1 ziplizt压缩链表
ziplist由多个部分组成
- zlbytes
记录整个压缩列表占用的字节数
用于压缩列表内存重分配或计算zleng位置
- zltail
记录最后一个节点离列表起始位置有多少个字节。
通过这个偏移量,不用遍历整个列表就能知道 尾节点的位置
- zllen
记录列表中的节点数,当值小于unint16_max(65535)时,该值就是列表中的节点数。如果该值等于65535,需要遍历整个列表
-
entry()
列表中的各个节点,长度由各个节点决定
entry由一下几个部分组成
- previous_entry_length:保存前一个节点的长度。如果前一个字节长度小于254字节,该属性长度为1字节,前一字节的长度就保存在这一个字节中。如果大于等于254字节,该属性长度为5字节。其中第一个字节为0xFE(十进制值254),后四个字节用于保存前一字节的长度。
- encoding:记录content属性所保存的数据的数据类型及长度
- content:保存实际的数据
-
zlend
特殊值,用于标记列表的末端
优点:
存储空间连续,更能节省内存空间
缺点:
1.极端情况下会出现连锁更新现象。
entry的previous_entry_length属性,记录上一个节点的长度。如果每个节点的长度都在250-253之间。此时在头部插入一个新的节点,且该节点长度大于254字节。则之后的每个节点的previous_entry_length属性都会从1字节变成5字节。导致整个列表的节点都需要更新。
2.只能存储小数据的值,且存储的数据量小
ziplist是一段连续的内存,插入,删除的时间复杂度为O(n)。每次插入或删除一个元素,都需要频繁调用realloc()函数进行内存的扩展或减少,然后进行数据搬移
5.2 linkedlist双向链表
使用list存储数据时,如果存储数据较大,ziplist无法满足,则使用linkedlist
优缺点:
优点:
插入,删除,修改节点的效率高
缺点:
需要保存前后节点,会占用内存
节点分散寻址麻烦
5.3 quicklist快速链表
quicklist是一个双向链表,链表中每个元素是一个ziplist
typedef struct quicklist {
// 指向quicklist的头部
quicklistNode *head;
// 指向quicklist的尾部
quicklistNode *tail;
//所有ziplist中节点的个数
unsigned long count;
//quicklistNode的个数
unsigned int len;
// ziplist大小限定,由redis.conf文件的list-max-ziplist-size给定
int fill : 16;
// 节点压缩深度设置,由redis.conf文件的llist-compress-depth给定
unsigned int compress : 16;
} quicklist;
typedef struct quicklistNode {
// 指向上一个ziplist节点
struct quicklistNode *prev;
// 指向下一个ziplist节点
struct quicklistNode *next;
// 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构
unsigned char *zl;
// 表示指向ziplist结构的总长度(内存占用长度)
unsigned int sz;
// ziplist数量
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
// 预留字段,存放数据的方式,1--NONE,2--ziplist
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
// 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
// 扩展字段
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklistLZF {
// LZF压缩后占用的字节数
unsigned int sz; /* LZF size in bytes*/
// 柔性数组,存放压缩后的ziplist字节数组
char compressed[];
} quicklistLZF;
作用:
减少了数据插入时内存空间的重新分配,以及内存数据的拷贝。同时,quicklist限制了每个节点上ziplist的大小,如果一个ziplist过大,就会新增quicklist节点。但由于quicklist中使用quicklistNode结构指向每个ziplist,增加了内存开销。
5.4 listpack
redis5.0之后 产生了listpack。为了减少内存开销和避免ziplist的连锁更新问题
struct listpack<T>{
int32 total_bytes; //总字节数
int16 size; //元素个数
T[] entries; //紧促排列的元素列表
int8 end; //与zlend一样,恒为0xFF
}
struct lpentry{
int<var> encoding; //当前元素的编码类型
optional byte[] content; //元素数据
int<var> length; //编码类型和元素数据这两部分的长度
}
listpack的节点中不再存储前一个节点的长度,避免了连锁更新。listpack支持正、反向查询列表的。
6. Hash类型
当hash中数据项较少且数据长度较小时,使用ziplist。否则使用dict(字典)
例子
hash可以理解为key代表一条记录,val中包含的(key-val)代表这条记录的每个字段及其值
1001(key) -> val
key为编号
val 为多个键值对
name:"tom"
age:10
height:180
weight:75
country:china
6.1 哈希表dictht
redis的字典使用哈希表作为底层实现,一个哈希表里面可以由多个哈希表节点。每个哈希表节点保存了字典中的一个键值对
typedef struct dictht {
//哈希表节点数组
dictEntry ••table;
//数组长度
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size一1
unsigned long sizemask;
//该哈希表巳有节点的数量
unsigned long used;
} dictht;
6.2 哈希表节点dicthtEntry
typedf struct dictEntry{
// 键
void *key;
// 值,可以是一个指针,也可以是不同类型的整数,浮点数
union{
void val;
unit64_t u64;
int64_t s64;
double d;
}v;
// 指向下一个节点的指针。将多个哈希值相同的键值对连接起来,解决哈希冲突
struct dictEntry *next;
}dictEntry;
k1和k0计算的哈希值相同,使用dictEntry中的next连接
6.3 字典dict
typedf struct dict{
// 类型特定函数,包括一些自定义函数
// 这些函数使得key和value能够存储
dictType *type;
// 私有数据
void *private;
// 两张hash表
dictht ht[2];
// rehash索引,字典没有进行rehash时,此值为-1
int rehashidx;
// 正在迭代的迭代器数量
unsigned long iterators;
}dict;
type和privdata是针对不同类型的键值对,为创建多态字典而设置的。
ht两个哈希表,一般只使用ht[0]哈希表,当ht[0]进行rehash时使用ht[1]。
rehashidx记录reshah的进度,如果此时没有进行rehash,值为-1。
6.4 hash表的扩展和收缩
Redis
中,三条关于扩容和缩容的规则:
- 没有执行
BGSAVE
和BGREWRITEAOF
指令的情况下,负载因子大于等于1时进行扩容; - 正在执行
BGSAVE
和BGREWRITEAOF
指令的情况下,哈希表的负载因大于等于5时进行扩容; - 负载因子小于0.1时,
Redis
自动开始对哈希表进行缩容操作;
dict中 负载因子 = 哈希表中已存在节点数/哈希表大小
扩容后哈希表节点数组 长度 *2
缩容后哈希表节点数组 长度与之前一致,缩容的过程是将数组中元素位置重排,变得紧凑
6.5 渐进式rehash
过程
1.为ht[1]分配空间,让字典同时拥有两个哈希表
2.将索引计数器rehashidx置为0 表示开始进行rehash
3.在rehash进行期间,每次对字典进行CRUD时,将ht[0]中下标为rehashidx的元素rehash到ht[1]中。rehash完成后,rehashidx+1
4.当ht[0]中所有元素都rehash到ht[1]中后,将rehashidx置为-1
5.将ht[0]释放,将ht[1]设置成ht[0],为ht[1]分配一个空白哈希表
进行rehash时,只能对ht[0]元素执行查询和删除操作;可以对ht[1]元素执行查询,删除,插入,这样将rehash的操作分摊到每次CRUD上,避免集中式rehash。
7. Set类型
实现set的底层数据结构:dict和intset
当满足以下条件时,使用intset
- 存储的数据都是整数
- 存储的数据元素个数<512
7.1 intset整数集合
typedef struct intset {
// 编码类型 int16_t、int32_t、int64_t
uint32_t encoding;
// 长度 最大长度:2^32
uint32_t length;
// 用来存储数据的柔性数组
int8_t contents[];
} intset;
contents数组中没有重复元素,元素在数组中按从小到大排序
注意:
当插入数据时,如果插入的元素类型 大于intset的encoding的值(例如插入元素为int32_t,而encoding值为int16_t)。为了防止溢出,会对intset进行升级操作。将数组中原有元素变成新插入元素的类型
升级整数集合并添加新元素分为三个步骤:
1.根据新元素类型,扩展intset的contents数组的空间大小,并为新元素分配空间
2.将contents中元素转成插入元素的类型,并将转换后的元素放到正确的位置上,元素有序性不变
3.将新元素加入contents中
intset优缺点:
优点:
1.通过升级数据类型,可以将多种数据类型元素插入数组
2.intset能同时保存三种不同类型的值,又可以确保升级操作只会在有需要的时候进行,节省内存
缺点:
1.不支持降级
2.添加和删除均需要realloc(动态内存调整)操作
8. ZSet类型
实现ZSet的底层数据结构:ziplist和skiplist(跳表)