一、redis的简介
Redis 是一个开源的、使用 C 语言编写的 NoSQL 数据库,基于内存运行并支持持久化,采用key-value(键值对)的存储形式。
1.1、Redis的特点
读写速度快:redis官网测试读写能到10万左右每秒。速度快的原因这里简单说一下,第一是因为数据存储在内存中,其次是Redis采用单线程的架构,避免了上下文的切换和多线程带来的竞争,也就不存在加锁释放锁的操作,减少了CPU的消耗,第三点是采用了非阻塞IO多路复用机制。
数据结构丰富:Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构。
支持持久化:Redis提供了RDB和AOF两种持久化策略,能最大限度地保证Redis服务器宕机重启后数据不会丢失。
支持高可用:可以使用主从复制,并且提供哨兵机制,保证服务器的高可用。
客户端语言多:因为Redis受到社区和各大公司的广泛认可,所以客户端语言涵盖了所有的主流编程语言,比如Java,C,C++,PHP,NodeJS等等
二、String(字符串)
2.1、定义
string 是 redis 最基本的类型,一个 key 对应一个 value。string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
2.2、编码类型
字符串类型一共有 3 种编码:
数据类型 | 说明 | 编码 | 使用的数据结构 |
OBJ_STRING | 字符串 | OBJ_ENCODING_INT | longlong、long |
OBJ_ENCODING_EMBSTR | string | ||
OBJ_ENCODING_RAW | string |
OBJ ENCODING_EMBSTR:长度小于或等于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT(44 字节)的字符串。
在该编码中,redisObject、sds 结构存放在一块连续内存块中,如图 1-3 所示。
redisObject | sdshdr | |||||||
type | encoding | lru | refcount | ptr | len | alloc | flags | buf |
图1-3
OBJ_ENCODING_EMBSTR 编码是 Redis 针对短字符串的优化,有如下优点:
内存申请和释放都只需要调用一次内存操作函数。
redisObject、sdshdr 结构保存在一块连续的内存中,减少了内存碎片
OBJ_ENCODING_RAW:长度大于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 的字符串,在该编码中,redisObiect、sds 结构存放在两个不连续的内存块中。
OBJ_ENCODING_INT:数值格式,将数值型字符串转换为整型,可以大幅降低数据占用的内存空间,如字符串“123456789012”需要占用 12 字节,在 Redis 中,会将它转化为longlong类型,只占用8 字节。
> SET msg "hello world"
OK
> TYPE msg
string
> OBJECT ENCODING msg
"embstr"
> SET Introduction "Redis is an open source (BSD licensed), in-memory data structurestore, used as a database, cache and message broker."
OK
> TYPE Introduction
string
> OBJECT ENCODING Introduction
"raw"
> SET page 1
OK
> TYPE page
string
> OBJECT ENCODING page
"int"
2.3、基本命令
三、list(列表)
3.1、定义
List的底层是通过双向链表和压缩列表来实现的,但是由于C 语言本身没有链表这个数据结构的,所以 Redis 自己设计了一个链表数据结构。在3.0之后,List是通过quicklist来实现的。
3.1.1、双向链表
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;
因为有前置节点和后置节点,所以可以看出这是一个双向链表。不过,Redis 在 listNode 结构体基础上又封装了 list 这个数据结构,这样操作起来会更方便。list中提供了链表的头节点、尾节点、链表数量以及一些可以自定义实现的函数。加了list之后的结构是这样的。
![](https://img-blog.csdnimg.cn/img_convert/8a721c4c26f76fd1ce90c52d2485c060.png)
3.1.2、ziplist(压缩列表)
Redis 内部使用双向链表保存运行数据。但 Redis 并不使用该链表保存用户列表数据,因为它对内存管理不够友好:
(1):链表中每一个节点都占用独立的一块内存,导致内存碎片过多。
(2):链表节点中前后节点指针占用过多的额外内存。
(3):当数据比较少时,比如只有一个节点,也需要一个链表节点结构头(list)的分配,内存开销较 大。因此,List 对象在数据量比较少的情况下,会采用压缩列表作为底层数据结构的实现。
ziplist是一种类似数组的紧凑型链表格式。它会申请一整块内存,在这个内存上存放该链表所有数据这就是 ziplist 的设计思想。quicklist 的结构体跟链表的结构体类似,都包含了表头和表尾,区别在于 quicklist 的节点是 quicklistNode。
typedef struct quicklist {
//quicklist的链表头
quicklistNode *head; //quicklist的链表头
//quicklist的链表头
quicklistNode *tail;
//所有压缩列表中的总元素个数
unsigned long count;
//quicklistNodes的个数
unsigned long len;
...
} quicklist;
quicklistNode 的结构定义:
typedef struct quicklistNode {
//前一个quicklistNode
struct quicklistNode *prev; //前一个quicklistNode
//下一个quicklistNode
struct quicklistNode *next; //后一个quicklistNode
//quicklistNode指向的压缩列表
unsigned char *zl;
//压缩列表的的字节大小
unsigned int sz;
//压缩列表的元素个数
unsigned int count : 16; //ziplist中的元素个数
....
} quicklistNode;
在quicklistNode中,链表节点的元素不再是单纯保存元素值,而是保存了一个压缩列表,所以 quicklistNode 结构体里有个指向压缩列表的指针 zl。
在向 quicklist 添加一个元素的时候,不会像普通的链表那样直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构。
quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,但是这并没有完全解决连锁更新的问题。
![](https://img-blog.csdnimg.cn/img_convert/f117e1d2aa4d7e613c2f2e68272dfa13.png)
3.2、编码类型
数据类型 | 说明 | 编码 | 使用的数据结构 |
OBJ_LIST | 列表 | OBJ_ENCODING_QUICKLIST | quicklist |
3.3、基本命令
四、hash(散列)
4.1、定义
hash键的底层在redis3.0中式通过压缩列表和哈希表来实现的,在3.0以后式通过listpack和哈希表实现的。
4.2、结构设计
typedf struct dict{
dictType *type;//类型特定函数,包括一些自定义函数,这些函数使得key和value能够存储
void *private;//私有数据
dictht ht[2];//两张hash表
int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1
unsigned long iterators; //正在迭代的迭代器数量
}dict;
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;
typedf struct dictEntry{
void *key;//键
union{
void val;
unit64_t u64;
int64_t s64;
double d;
}v;//值
struct dictEntry *next;//指向下一个节点的指针
}dictEntry;
哈希表是一个数组(dictEntry **table),数组的每个元素是指向哈希表节点(dictEntry)的指针。
![](https://img-blog.csdnimg.cn/img_convert/d81249449513b613f4b4a39143206772.png)
4.3、hash冲突
原因:哈希表中桶的数量是有限的,当Key的数量较大时自然避免不了哈希冲突(多个Key落在了同一个哈希桶中)。
解决方式:Redis 采用了「链式哈希」的方法来解决哈希冲突。
具体实现:实现的方式就是每个哈希表节点都有一个 next 指针,用于指向下一个哈希表节点,因此多个哈希表节点可以用 next 指针构成一个单项链表,被分配到同一个哈希桶上的多个节点可以用这个单项链表连接起来,这样就解决了哈希冲突。不过,链式哈希局限性也很明显,随着链表长度的增加,在查询这一位置上的数据的耗时就会增加,毕竟链表的查询的时间复杂度是 O(n)。
4.4、rehash
触发条件:
为了能够减少哈希冲突,其实最直接的做法是增加哈希桶数量从而让元素能够更加均匀的分布在哈希表中。而Redis中的Rehash操作的原理其实也是如此,只不过他的设计更加巧妙。
具体实现:
Redis中其实有两个「全局哈希表」,一开始时默认使用的Hash Table1来存储数据,而Hash Table2并没有分配内存空间。随着Hash Table1中的元素越来越多时,Redis会进行Rehash操作。首先会给Hash Table2分配一定的内存空间(肯定比哈希表一大),然后将Hash Table1中的元素重新映射至Hash Table2中,最后会释放Hash Table1。这样来看的话,Redis的Rehash操作的确能减少哈希冲突,但是你有没有想过如果Hash Table1中的元素特别多时,如果这么粗暴的将数据往Hash Table2中搬,那势必会阻塞Redis的主线程进而影响Redis的性能。其实Redis也考虑到了这个问题,那么接下来我们看看Redis是如何解决这种问题的
4.5、渐进式Rehash
原因:因为Hash Table1中的数据rehash到Hash Table2中的手,是阻塞Redis主线程进而影响Redis的性能。所以Redis采用了渐进式Rehash
具体步骤:在将数据拷贝至Hash Table2时,Hash Table1仍然对客户端提供服务。当客户端访问Hash Table1时,Hash Table1 将索引位置为1的Bucket1中的Entery全部拷贝至Hash Table2,同理当客户端再一次访问Hash Table1时,Hash Table1 将索引位置为2的Bucket2中的Entery全部拷贝至Hash Table2。再拷贝数据进Hash Table2的同时会对数据做重新的Bucket分配,从而减少Hash冲突。
如此往复下去,当Hash Table1的元素都拷贝Hash Table2时,Hash Table2对顶替Hash Table1 与客户端进行交互,此时Hash Table1会被释放,等待下一次Rehash使用。
![](https://img-blog.csdnimg.cn/img_convert/b2c608b5a23988b002345e6d1e3c5113.png)
当Hash Table中的元素逐渐增多时,会在Bucket中形成链表,一旦链表过长则会严重影响Redis的查询性能,这对以速度著称的Redis是不能接受的。所以Redis采用了增加Hash Table的容量来解决这个问题,也就是所谓的Rehash机制,而Redis为了减轻Rehash时数据大量拷贝锁带来的压力,从而采用了渐进式Rehash来分批次的进行数据拷贝。
4.6、扩容/缩容
当哈希表中元素数量逐渐增加时,此时产生 hash 冲突的概率逐渐增大,且由于 dict也是采用拉链法解决 hash 冲突的,随着 hash冲突概率上升,链表会越来越长,因此当元素越来越多的时候就需要进行扩容,这就会导致查找效率下降。相反,当元素不断减少时,元素占用 dict 的空间就越少,出现对于内存的极致利用,此时就需要进行缩容操作。
既然说到扩容和缩容,那就想到了负载因子。负载因子一般用于描述集合当前被填充的程度。在 Redis 的字典 dict 中,负载因子 = 哈希表中已保存节点数量 / 哈希表的大小,即:
load factor = ht[0].used / ht[0].size
Redis中,三条关于扩容和缩容的规则:
1. 没有执行 BGSAVE 和 BGWRITEAOF 指令的情况下,哈希表的负载因子大于等于 1 时进行扩容。
2. 正在执行 BGSAVE 和 BGWRITEAOF 指令的情况下,哈希表的负载因子大于等于 5 时进行扩容。
3. 负载因子小于 0.1 时,Redis 自动开始对哈希表进行收索操作。
其中,扩容和缩容的数量大小也是有一定的规则:
1. 扩容:扩容后的 dicEntry 数组数量为第一个大于等于 ht[0].used * 2 的 2^n
2. 缩容:缩容后的 dicEntry 数组数量为第一个大于等于 ht[0].used 的 2^n
4.7、编码类型
数据类型 | 说明 | 编码 | 使用的数据结构 |
OBJ_HASH | 散列 | OBJ_ENCODING_HIT | dict |
OBJ_ENCODING_ZIPLIST | ziplist |
4.8、基本命令
五、SET(集合)
5.1、定义
set类型的特点很简单,无序,不重复,跟Java的HashSet类似。它的编码有两种,分别是intset和hashtable。如果value可以转成整数值,并且长度不超过512的话就使用intset存储,否则采用hashtable。Redis为set类型提供了求交集,并集,差集的操作,可以非常方便地实现譬如共同关注、共同爱好、共同好友等功能。
5.2、结构设计
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
encoding:有三种,分别是INTSET_ENC_INT16、INSET_ENC_INT32、INSET_ENC_INT64,代表着整数值的取值范围。Redis会根据添加进来的元素的大小,选择不同的类型进行存储,可以尽可能地节省内存空间。
length:记录集合有多少个元素,这样获取元素个数的时间复杂度就是O(1)。
contents:存储数据的数组,数组按照从小到大有序排列,不包含任何重复项。
5.3、整数集合的升级操作
场景:这里我们可能会提出疑问,如果一开始存的是INTSET_ENC_INT16(范围在-32,768~32,767),如果这时添加了一个40000的数,怎么升级为INSET_ENC_INT32呢?
升级过程:
1、根据新元素的类型扩展数组contents的空间。
2、从尾部将数据插入。
3、根据新的编码格式重置之前的值,因为这时的contents存在着两种编码的值。从插入的数据的位置,也就是尾部,从后到前将之前的数据按照新的编码格式进行移动和设置。从后到前调整是为了防止数据被覆盖。
升级的优点:根据存储的数据大小选择合适的编码方式,节省了内存。
升级的缺点:升级会消耗系统资源。而且升级是不可逆的,也就是一旦对数组进行升级,编码就会一直保持升级后的状态。
5.4、编码类型
数据类型 | 说明 | 编码 | 使用的数据结构 |
OBJ_SET | 集合 | OBJ_ENCODING_HIT | dict |
OBJ_ENCODING_INTSET | intset |
5.5、基本命令
六、ZSET(有序集合)
6.1、定义:
zset和set一样是不可重复的,区别在于多了score值,用来代表排序的权重。也就是当你需要一个有序的,不可重复的集合列表时,就可以考虑使用这种数据类型。zset的编码有两种,分别是:ziplist、skiplist。当zset的长度小于 128,并且所有元素的长度都小于 64 字节时,使用ziplist存储;否则使用 skiplist 存储。
6.2、跳表
6.2.1、定义
跳表是一种特殊的链表,特殊的点在于其可以进行二分查找。普通的链表要查找元素只能挨个遍历链表中的所有元素,而跳表则利用了空间换时间的策略,在原来有序链表的基础上面增加了多级索引,然后利用类似二分查找的思路来快速实现查找功能。跳表可以支持快速的查找,插入,删除等操作,时间复杂度为O(logn),空间复杂度为O(n)。
跳表由zskiplistNode和zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表 尾节点的指针等等。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zskiplistNode {
//Zset 对象的元素值
sds ele;
//元素权重值
double score;
//后向指针保存的是该节点的前一个节点,是为了方便从跳表的尾节点开始访问节点
struct zskiplistNode *backward;
//节点的level数组,保存每层上的前向指针(下一个节点)和跨度
struct zskiplistLevel {
struct zskiplistNode *forward;
//span为跨度,跨度用来记录两个节点之间的距离
unsigned long span;
} level[];
} zskiplistNode;
6.2.2、查询步骤
1、level2找到结点Node75小于80,且level2.Node75->next 大于80,则进入level1查找(此处已经跳过了13~75中间的结点(22),
2、level1.Node75 < 80 < level1.Node75->next,进入level0
3、level0.Node75->next 等于80,找到结点80。
![](https://img-blog.csdnimg.cn/img_convert/66113b8af9c82240298c599ee88aad53.png)
6.3、listpack
listpack的目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
(1)结构设计
![](https://img-blog.csdnimg.cn/img_convert/3497b0ad22e84dfa39b071576a03d0e8.png)
listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量。
每个 listpack 节点结构如下:
![](https://img-blog.csdnimg.cn/img_convert/9814ece11a5b47d3a026fee03b92a4dc.png)
encoding:定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
data:实际存放的数据;
len:encoding+data的总长度;
listpack中没有了prevlen记录前一个节点的长度,而是用len记录当前节点长度,从而避免连锁更新的问题。
6.4、编码类型
数据类型 | 说明 | 编码 | 使用的数据结构 |
OBJ_ZSET | 有序集合 | OBJ_ENCODING_ZIPLIST | ziplist |
OBJ_ENCODING_SKIPLIST | skiplist |
6.5、基本命令
*借道友法力一用:
![](https://img-blog.csdnimg.cn/img_convert/3246eaf25acc72c0ace3802594b58bf9.webp?x-oss-process=image/format,png)
========================== stay hungry stay foolish =============================