题记:
文章内容输出来源:拉勾教育Java高薪训练营。
本篇文章是 Redis 学习课程中的一部分笔记。
Redis作为Key-Value存储系统,数据结构如下:
Redis没有表的概念,Redis实例所对应的db以编号区分,db本身就是key的命名空间。比如:user:1000作为key值,表示在user这个命名空间下id为1000的元素,类似于user表的id=1000的行。
一、RedisDB结构
Redis中存在“数据库”的概念,该结构由redis.h中的redisDb定义。当redis 服务器初始化时,会预先分配 16 个数据库,所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中,redisClient中存在一个名叫db的指针指向当前使用的数据库。
RedisDB结构体源码:
typedef struct redisDb {
int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; //存储的数据库对象的平均ttl(time to live),用于统计
dict *dict; //存储数据库所有的key-value
dict *expires; //存储key的过期时间
dict *blocking_keys;//blpop 存储阻塞key和客户端对象
dict *ready_keys;//阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象
dict *watched_keys;//存储watch监控的的key和客户端对象
} redisDb;
1、id
id是数据库序号,为0-15(默认Redis有16个数据库)
2、dict
存储数据库所有的key-value
3、expires
存储key的过期时间
二、RedisObject结构
Value是一个对象,包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象
1、结构信息概览
typedef struct redisObject {
unsigned type:4;//类型 五种对象类型
unsigned encoding:4;//编码
void *ptr;//指向底层实现数据结构的指针
//...
int refcount;//引用计数
//...
unsigned lru:LRU_BITS; //LRU_BITS为24bit 记录最后一次被命令程序访问的时间
//...
}robj;
2、4位type
type 字段表示对象的类型,占 4 位;
REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
当我们执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型
127.0.0.1:6379> type a1
string
3、4位encoding
encoding 表示对象的内部编码,占 4 位。每个对象有不同的实现编码,Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。
通过 object encoding 命令,可以查看对象采用的编码方式
127.0.0.1:6379> object encoding a1
"int"
4、24位LRU
lru 记录的是对象最后一次被命令程序访问的时间,( 4.0 版本占 24 位,2.6 版本占 22 位)。
高16位存储一个分钟数级别的时间戳,低8位存储访问计数(lfu : 最近访问次数)
5、refcount
refcount 记录的是该对象被引用的次数,类型为整型。
refcount 的作用,主要在于对象的引用计数和内存回收。
当对象的refcount>1时,称为共享对象
Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。
6、ptr
ptr 指针指向具体的数据,比如:set hello world,ptr 指向包含字符串 world 的 SDS。
三、type
1、字符串对象
C语言: 字符数组 “\0”
Redis 使用了 SDS(Simple Dynamic String)。用于存储字符串和整型数据。
struct sdshdr{
//记录buf数组中已使用字节的数量
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
SDS的优势:
使用场景:
SDS的主要应用在:存储字符串和整型数据、存储key、AOF缓冲区和用户输入缓冲。
2、跳跃表(重点)
跳跃表是有序集合(sorted-set)的底层实现,效率高,实现简单。
跳跃表的基本思想:将有序链表中的部分节点分层,每一层都是一个有序链表。
这种数据结构,就是跳跃表,它具有二分查找的功能。
插入与删除
上面例子中,9个结点,一共4层,是理想的跳跃表。
通过抛硬币(概率1/2)的方式来决定新插入结点跨越的层数:
正面:插入上层
背面:不插入
达到1/2概率(计算次数)
删除
找到指定元素并删除每层的该元素即可
跳跃表特点:
跳跃表的实现
//跳跃表节点
typedef struct zskiplistNode {
/*
* 存储字符串类型数据 redis3.0版本中使用robj类型表示, 但是在redis4.0.1中直接使用sds类型表示
*/
sds ele;
//存储排序的分值
double score;
//后退指针,指向当前节点最底层的前一个节点
struct zskiplistNode *backward;
/*
* 层,柔性数组,随机生成1-64的值
*/
struct zskiplistLevel {
//指向本层下一个节点
struct zskiplistNode *forward;
//本层下个节点到本节点的元素个数
unsigned int span;
} level[];
} zskiplistNode;
//链表
typedef struct zskiplist{
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
3、字典(重点+难点)
字典dict又称散列表(hash),是用来存储键值对的一种数据结构。
Redis整个数据库是用字典来存储的。(K-V结构)
对Redis进行CURD操作其实就是对字典中的数据进行CURD操作。
4、Hash表
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表数组的大小
unsigned long size;
// 用于映射位置的掩码,值永远等于(size-1)
unsigned long sizemask;
// 哈希表已有节点的数量,包含next单链表数据
unsigned long used;
} dictht;
hash表的数组初始容量为4,随着k-v存储量的增加需要对hash表数组进行扩容,新扩容量为当前量的一倍,即4,8,16,32
索引值=Hash值&掩码值(Hash值与Hash表容量取余)
typedef struct dictEntry {
// 键
void *key;
// 值v的类型可以是以下4种类型
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个哈希表节点,形成单向链表 解决hash冲突
struct dictEntry *next;
} dictEntry;
key字段存储的是键值对中的键
v字段是个联合体,存储的是键值对中的值。
next指向下一个哈希表节点,用于解决hash冲突
5、dict字典
typedef struct dict {
// 该字典对应的特定操作函数
dictType *type;
// 上述类型函数对应的可选参数
void *privdata;
/*
* 两张哈希表,存储键值对数据,ht[0]为原生 哈希表, ht[1]为 rehash 哈希表
*/
dictht ht[2];
/*
* rehash标识 当等于-1时表示没有在 rehash, 否则表示正在进行rehash操作,存储的值表示 hash表 ht[0]的rehash进行到哪个索引值 (数组下标)
*/
long rehashidx;
// 当前运行的迭代器数量
int iterators;
} dict;
type字段,指向dictType结构体,里边包括了对该字典操作的函数指针
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 比较键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
Redis字典除了主数据库的K-V数据存储以外,还可以用于:散列表对象、哨兵模式中的主从节点管理等在不同的应用中,字典的形态都可能不同,dictType是为了实现各种形态的字典而抽象出来的操作函数(多态)。
完整的Redis字典数据结构:
字典扩容
字典达到存储上限,需要rehash(扩容)
扩容流程:
说明:
渐进式rehash
当数据量巨大时rehash的过程是非常缓慢的,所以需要进行优化。
服务器忙,则只对一个节点进行rehash
服务器闲,可批量rehash(100节点)
应用场景:
6、压缩列表
压缩列表(ziplist)是由一系列特殊编码的连续内存块组成的顺序型数据结构
节省内存:是一个字节数组,可以包含多个节点(entry)。每个节点可以保存一个字节数组或一个整数。
压缩列表的数据结构如下:
zlbytes:压缩列表的字节长度
zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量
zllen:压缩列表的元素个数
entry1…entryX : 压缩列表的各个节点
zlend:压缩列表的结尾,占一个字节,恒为0xFF(255)
entryX元素的编码结构:
previous_entry_length:前一个元素的字节长度
encoding:表示当前元素的编码
content:数据内容
ziplist结构体如下:
typedef struct zlentry {
//previous_entry_length字段的长度
unsigned int prevrawlensize;
//previous_entry_length字段存储的内容
unsigned int prevrawlen;
//encoding字段的长度
unsigned int lensize;
//数据内容长度
unsigned int len;
//当前元素的首部长度,即previous_entry_length字段长 度与 encoding字段长度之和。
unsigned int headersize;
//数据类型
unsigned char encoding;
//当前元素首地址
unsigned char *p;
} zlentry;
7、整数集合
整数集合(intset)是一个有序的(整数升序)、存储整数的连续存储结构。
当Redis集合类型的元素都是整数并且都处在64位有符号整数范围内(2^64),使用该结构体存储。
127.0.0.1:6379> sadd set:001 1 3 5 6 2
(integer) 5
127.0.0.1:6379> object encoding set:001
"intset"
127.0.0.1:6379> sadd set:004 1 100000000000000000000000000 9999999999
(integer) 3
127.0.0.1:6379> object encoding set:004
"hashtable"
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
应用场景:
可以保存类型为int16_t、int32_t 或者int64_t 的整数值,并且保证集合中不会出现重复元素。
8、快速列表(重要)
快速列表(quicklist)是Redis底层重要的数据结构。是列表的底层实现。(在Redis3.2之前,Redis采用双向链表(adlist)和压缩列表(ziplist)实现。)在Redis3.2以后结合adlist和ziplist的优势Redis设计出了quicklist。
127.0.0.1:6379> lpush list:001 1 2 5 4 3
(integer) 5
127.0.0.1:6379> object encoding list:001
"quicklist"
快速列表
quicklist是一个双向链表,链表中的每个节点时一个ziplist结构。quicklist中的每个节点ziplist都能够存储多个数据元素。
quicklist的结构定义如下:
typedef struct quicklist {
// 指向quicklist的头部
quicklistNode *head;
// 指向quicklist的尾部
quicklistNode *tail;
// 列表中所有数据项的个数总和
unsigned long count;
// quicklist节点的个数,即ziplist的个数
unsigned int len;
// ziplist大小限定,由list-max-ziplist-size给定 (Redis设定)
int fill : 16;
// 节点压缩深度设置,由list-compress-depth给定(Redis 设定)
unsigned int compress : 16;
} quicklist;
quicklistNode的结构定义如下:
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;
// 编码方式,1--ziplist,2--quicklistLZF
unsigned int encoding : 2;
// 预留字段,存放数据的方式,1--NONE,2--ziplist
unsigned int container : 2;
// 解压标记,当查看一个被压缩的数据时,需要暂时解压,标 记此参数为 1,之后再重新进行压缩
unsigned int recompress : 1;
// 测试相关
unsigned int attempted_compress : 1;
// 扩展字段,暂时没用
unsigned int extra : 10;
} quicklistNode;
数据压缩
quicklist每个节点的实际数据存储结构为ziplist,这种结构的优势在于节省存储空间。为了进一步降低ziplist的存储空间,还可以对ziplist进行压缩。Redis采用的压缩算法是LZF。其基本思想是:数据与前面重复的记录重复位置及长度,不重复的记录原始数据。
压缩过后的数据可以分成多个片段,每个片段有两个部分:解释字段和数据字段。quicklistLZF的结构体如下:
typedef struct quicklistLZF {
// LZF压缩后占用的字节数
unsigned int sz;
// 柔性数组,指向数据部分
char compressed[];
} quicklistLZF;
应用场景
列表(List)的底层实现、发布与订阅、慢查询、监视器等功能。
9、流对象
stream主要由:消息、生产者、消费者和消费组构成。
Redis Stream的底层主要使用了listpack(紧凑列表)和Rax树(基数树)。
四、encoding
encoding 表示对象的内部编码,占 4 位。Redis通过 encoding 属性为对象设置不同的编码,对于少的和小的数据,Redis采用小的和压缩的存储方式,体现Redis的灵活性,大大提高了 Redis 的存储量和执行效率
比如Set对象:
intset : 元素是64位以内的整数
hashtable:元素是64位以外的整数
如下所示:
至此,结束
最后
在这个知识付费的时代,每一位热爱技术分享、奋笔直书的人,都值得我们尊敬!所以,请不要吝啬您手中的鼠标,按下左键,为小编点个赞吧。
更多内容,请关注微信公众号:架构视角
特别鸣谢
感谢老猫老师风趣幽默的讲解,让我对所学知识点记忆深刻!
感谢木槿导师的认真和负责,每一次作业点评都是我前进的动力!
感谢班主任毕老师的负责和耐心,每次不厌其烦的上课通知都是我不忘初心,保持良好学习状态的精神支柱!
感谢拉勾教育平台,给我这次花少量的钱就能报名第一期拉钩训练营,就能学习到很多深层次的技术精华的机会。而且,在学习过程中还认识了很多技术大佬,可以请教他们一些问题,比如张大佬、卢大佬、雨生大佬等等。