类型 | 特性 |
---|---|
string(字符串) | 二进制安全的,可以包含任何数据,一个键最大能存储512M |
list(列表) | 双向链表,按照插入顺序排序,可以从链表两端进行push和pop操作 |
hash(散列表) | 键值对集合,适合存储对象 |
set(集合) | 元素不重复的无序集合 |
zset(有序集合) | 将set中的元素增加一个权重参数score,元素按score有序排列,数据插入集合时,已经进行天然排序 |
Redis构建了一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作Redis的默认字符串表示
struct sdshdr {
//记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
特性:
1)、获取字符串长度的复杂度为O(1)
2)、API是安全的,不会造成缓冲区溢出
当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至修改所需的大小,然后才执行实际的修改操作
3)、减少修改字符串时带来的内存重分配次数
SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录
1)空间预分配
空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须的空间,还会为SDS分配额外的未使用空间
在扩展SDS空间之前,SDS API会先检查未使用空间是否足够,如果足够的话,API就会直接使用未使用空间,而无须执行内存重分配
2)惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用
4)、二进制安全
通过使用二进制安全的SDS,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据
链表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现
每个链表节点使用一个adlist.h/listNode
结构来表示:
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode;
多个listNode可以通过prev和next指针组成双端链表
使用adlist.h/list
来持有链表
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
}list;
特性:
1)、双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)
2)、无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点
3)、带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)
4)、带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)
Redis的数据库就是使用字典来作为底层实现的,对数据库的CRUD操作也是构建在对字典的操作之上的
一个没有进行rehash的字典如下:
dict结构内部包含两个hashtable,通常情况下只有一个hashtable是有值的。但是在dict扩容缩容时,需要分配新的hashtable,然后进行渐进式搬迁,这时候两个hashtable存储的分别是旧的hashtable和新的hashtable。待搬迁结束后,旧的hashtable被删除,新的hashtable取而代之
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题
渐进式rehash:
为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩,扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成
为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]
1)、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2)、在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始
3)、在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash至ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一
4)、随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成
因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。而新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的,支持平均O(logN)、最坏O(N)复杂度的节点查找
Redis使用跳跃表实现有序集合
上图中一个跳跃表示例,位于图片最左边的是zskiplist结构,该结构包含以下属性:
位于zskiplist结构右方的是四个zskiplistNode结构,该结构包含以下属性:
Redis使用的是惰性删除和定期删除两种策略
过期键的惰性删除策略由db.c/expireIfNeeded
函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
过期键的定期删除策略由redis.c/activeExpireCycle
函数实现,每当Redis的服务器周期性操作redis.c/serverCron
函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键
activeExpireCycle函数的工作模式:
当Redis内存使用达到maxmemory上限时触发内存溢出控制策略,具体策略受maxmemory-policy参数控制,Redis支持6种策略
RDB是Redis默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中,即在指定目录下生成一个dump.rdb文件,Redis重启会通过加载dump.rdb文件恢复数据
1)、RDB文件的创建与载入
有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程继续处理命令请求
RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件
因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:
2)、RDB文件载入时的服务器状态
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,默认不开启
1)、AOF持久化的实现
1)命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾
2)AOF文件的写入与同步
Redis的服务器进程就是一个事件循环,这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行serverCron函数这样需要定时运行的函数
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面
2)、AOF文件的载入与数据还原
Redis读取AOF文件并还原数据库状态的详细步骤如下:
1)创建一个不带网络连接的伪客户端:因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样
2)从AOF文件中分析并读取出一条写命令
3)使用伪客户端执行被读出的写命令
4)一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止
3)、AOF重写
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小很多
1)AOF文件重写的实现
AOF重写功能的实现原理:首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令
aof_rewrite函数生成的新AOF文件只包含还原当前数据库所必须的命令,所以新AOF文件不会浪费任何硬盘空间
2)AOF后台重写
aof_rewrite函数可以很好地完成创建一个新AOF文件的任务,但是因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof_rewrite函数的话,那么在重写AOF文件期间,服务器将无法处理客户端发来的命令请求
Redis将AOF重写程序放到子进程里执行,这样做可以同时达到两个目的:
Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区
在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:
执行客户端发来的命令
将执行后的写命令追加到AOF缓冲区
将执行后的写命令追加到AOF重写缓冲区
Redis4.0版本添加了新的混合持久化方式,混合持久化就是同时结合RDB持久化以及AOF持久化混合写入AOF文件。这样做的好处是可以结合RDB和AOF的优点,快速加载同时避免丢失过多的数据,缺点是AOF里面的RDB部分就是压缩格式不再是AOF格式,可读性差
1)、开启混合持久化
4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,默认是禁用的
2)、混合持久化过程
混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据
3)、数据恢复
当开启了混合持久化时,启动Redis依然优先加载AOF文件,AOF文件加载可能有两种情况如下:
1)、RDB的优点
2)、RDB的缺点
3)、AOF的优点
4)、AOF的缺点
Redis客户端执行一条命令分4个过程:
发送命令-〉命令排队-〉命令执行-〉返回结果
这个过程称为Round trip time(简称RTT, 往返时间),mget mset有效节约了RTT,但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,需要消耗N次RTT ,这个时候需要Pipeline来解决这个问题
未使用Pipeline执行N条命令:
使用了Pipeline执行N条命令:
Redis Pipeline指在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。Pipeline能减少客户端和服务端交互的次数,将客户端的请求批量发送给服务器,服务器针对批量数据分别查询并统一回复
原生批命令(mset、mget)与Pipeline对比
Redis事务可以一次执行多个命令, 并且带有以下三个重要的保证:
一个事务从开始到执行会经历以下三个阶段:
Redis事务相关命令:
watch key1 key2 ...
:监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断(类似乐观锁)multi
:标记一个事务块的开始exec
:执行所有事务块的命令(一旦执行exec后,之前加的监控锁都会被取消掉)discard
:取消事务,放弃事务块中的所有命令unwatch
:取消watch对所有key的监控