【Redis】《Redis设计与实现》读书笔记

一、简单了解Redis

    Redis是以快速读写数据为优势的工具,它是单线程的,占用较少的内存,开源的,以C语言实现的一种Key_Value数据库,在热点数据缓存,限时业务(利用了Redis的键生存时间),游戏排行榜后端服务器等等方面都有应用。

二、Redis的数据结构

2.1 字符串变量

    Redis并没有直接使用C字符串作为自己的字符串变量,而是进行了一层封装,消除了C字符串的隐患,提高了效率,封装后的结构称为简单动态字符串(SDS),结构如下:

struct sdshdr {
// 记录buf 数组中已使用字节的数量
// 等于SDS 所保存字符串的长度
int len;
// 记录buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};

封装之后的优劣势显而易见,总结如下:
1、可以直接重用一部分C字符串的函数。(底层还是char数组)
2、获取字符串长度时间复杂度从O(N)到O(1)。 (通过len字段直接获得长度)
3、杜绝了C字符串在操作时发生缓冲区溢出的问题。(通过先判断空间实现)
4、减小修改字符串时带来的内存重新分配的次数。(空间预分配,很多地方都有应用到此策略)
5、惰性空间释放,部分空间不再使用SDS不会立刻释放这部分空间,而是保存下来,下次继续用,也有提供直接释放空间的API避免空间浪费。
6、SDS可以保存文本和二进制数据,C字符串只能保存文本数据。
7、但是SDS只能使用部分<string.h>中的函数。

2.2 五种基本数据结构

2.2.1 链表

    C语言并无内置链表结构,估计就算有Redis还是会再封装一次,Redis封装的链表结构如下:

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;

//链表节点listNode结构:
typedef struct listNode {

    // 前置节点
    struct listNode *prev;

    // 后置节点
    struct listNode *next;

    // 节点的值
    void *value;

} listNode;

    通过结构可以看到这是一个双端无环链表;链表节点使用void* 指针来保存节点值,并且可以通过list 结构的dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值,实现了多态。len字段使获得链表长度成为一个O(1)的操作。Redis发布与订阅,慢查询,监视器都用到了链表。

2.2.2 字典

     Redis 的数据库就是使用字典(redis自己实现的字典)来作为底层实现的, 对数据库的增、删、查、改操作也是构建在对字典的操作之上的。
字典的实现又是使用哈希表,哈希表dictht的结构如下:
在这里插入图片描述
字典的结构:

typedef struct dict {
// 类型特定函数
dictType *type;   // type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数
// 私有数据
void *privdata;  // 保存了需要传给那些类型特定函数的可选参数
// 哈希表
dictht ht[2];   // 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

    当要将一个新的键值对添加到字典里面时, 程序需要先根据键值对的键计算出哈希值( MurmurHash2算法)和索引值, 然后再根据索引值, 将包含新键值对的哈希表节点放到哈希表数组的指定索引上面,Redis使用开链法解决hash冲突,当数据变化很多导致哈希大小不合理时,rehash对哈希表的大小进行扩展或者收缩,这样来维持一个合理的负载因子( 哈希表已保存节点数量 / 哈希表大小)
rehash增大的时机:
1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
收缩的时机:
当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。
渐进式rehash: 采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量,字典的rehashidx记录当前rehash的索引值,结束置为-1。

2.2.3 跳跃表

    跳跃表应用于:有序集合键,在集群节点中用作内部数据结构,通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。
    Redis 的跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成, 其中 zskiplist 用于保存跳跃表信息(比如表头节点、表尾节点、长度),而 zskiplistNode 则用于表示跳跃表节点。
    每个跳跃表节点的层高都是 1 至 32 之间的随机数。
    在同一个跳跃表中, 多个节点可以包含相同的分值, 但每个节点的成员对象必须是唯一的。
    跳跃表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象的大小进行排序。

2.2.4 整数集合

     集合只包含整数元素且数量不多时,Redis使用整数集合作为集合键的底层实现,封装使得存储数据的长度是可变的,结构如下:

typedef struct intset {
// 编码方式,决定了contents里面数据长度
uint32_t encoding; 
// 集合包含的元素数量,即contents的数组长度
uint32_t length;
// 保存元素的数组,int8_t并不是它真正的长度,由encodint决定
int8_t contents[];
} intset;

    当新插入的数据长度比集合当前结构长时,对集合进行升级,如int32插入encoding为int16的集合中,该集合所有的数据类型都升级为int32,保证新的数据能够正常插入。优势在于,当全部数据都为长度比较短的数据时,大大的节约了内存,即使更长的数据插入,也不会出错。

2.2.5 压缩列表

Redis为了节约内容而开发的一系列特殊编码内存块组成的顺序性数据结构。
每个压缩列表节点可以保存一个字节数组或者一个整数值。
每个压缩列表节点都由 previous_entry_length 、 encoding 、 content 三个部分组成
previous_entry_length :记录压缩节点上一个节点的长度,通过它可以实现从尾部向头部的遍历,当前节点的起始指针减去这个字段值可得出指向前一个节点起始地址ip。
插入一个新的字节数大于其插入位置节点provious_entry_length可以存下的长度,则有可能引发连锁更新,删除也是一样的,但是可能性也很小。

2.3 对象

Redis使用上述的几种数据结构创建了字符串对象,列表对象,哈希对象,集合对象,有序集合对象的集合系统,对象使用引用计数
创建一个键值对时至少创建了两个对象,一个对象作为键对象(一定是一个字符串对象),另一个为值对象(可以是上述任意对象)。
对象结构:

typedef struct redisObject {
// 类型
unsigned type:4;      //表示对象类型(字符串对象、列表对象。。。)
// 编码
unsigned encoding:4;        //指明对象使用了什么数据结构作为对象的底层实现,当当前使用的编码方式不再是最适合的时候,编码方式会改变
// 指向底层实现数据结构的指针
void *ptr;
int refcount;          //引用计数
unsigned lru;    //记录对象最后一次被命令程序访问的时间,当服务器内存超出maxmemory,有可能会释放这个字段比较大的对象
// ...
} robj;

Redis使用dict字典保存数据库中所有的键值对,使用expires保存键的过期时间。
Redis使用惰性删除(碰到过期的键)和定期删除(定期扫描清理)来清除过期的键。

三、持久化模式

3.1 RDB

RDB保存数据库快照的磁盘中,重启时加载,SAVE和BGSAVE都调用rdbSave保存数据,SAVE时redis完全阻塞,不接受客户端任何请求,BGSAVE用子进程调用rdbSave,主进程仍然可以接收请求。

3.2 AOF

AOF是以协议文本的方式,记录所有对数据库进行写入的命令以及参数(网络通讯协议的格式)。为了避免AOF文件越来越大,会对AOF文件执行 BGREWRITEAOF ,重写AOF文件(子进程执行)
AOF三种保存方式:不保存(Redis停机时才保存,主进程执行),一秒钟保存一次(主线程执行,安全性相对高),执行一个命令保存一次(主进程执行,安全性最高,性能最差)

3.3 事件

Redis 服务器是一个事件驱动程序,要处理的事件分类如下:
在这里插入图片描述
文件事件和时间事件之间是合作关系, 服务器会轮流处理这两种事件, 并且处理事件的过程中也不会进行抢占。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

四、多机数据库的复制

在这里插入图片描述

五、独立功能

5.1 发布与订阅

Redis 通过 PUBLISH 、 SUBSCRIBE 等命令实现了订阅与发布模式, 这个功能提供两种信息机制, 分别是订阅/发布到频道和订阅/发布到模式。

每个redis服务器维护一个表示服务器状态的redisServer

struct redisServer {
// ...
dict *pubsub_channels;       //订阅频道,键值为正在被订阅的频道,字典的值是保存所有订阅这个频道的客户端链表
list *pubsub_patterns;     //订阅模式
// ...};

订阅:当客户端调用订阅 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来(插入对应链表的末尾)。
取消订阅:UNSUBSCRIBE,删除对应键内该客户端的节点
订阅模式的结构:

typedef struct pubsubPattern {
redisClient *client;     //订阅模式的客户端
robj *pattern;           //客户端订阅的模式
} pubsubPattern;

订阅一个模式:PUSHSCRIBE,创建一个包含客户端信息和被订阅模式的oubsubPattern插入到pubsub_patterns中。
PUBLISH命令发送信息到订阅的客户端和匹配的模式。

5.2 事务

事务是将多个命令打包,一次性按顺序执行的机制,有如下一些命令:
MULTI命令:开始一个事物,输入该命令后输入的命令都不会立即被执行,是加入到事务队列里。
EXEC命令:执行一个事务,将事务队列里的命令按顺序执行并以FIFO顺序返回给客户端结果队列。
DISCARD命令:取消一个事务。
WATCH命令(类似于订阅频道的实现):在开始一个事务之前使用,监视指定的键是否被其他客户端修改,若是,则整个事务不再执行,直接返回失败。
事务的ACID性质:保证一致性、隔离性、原则性,不保证持久性。

5.3 慢查询日志

记录执行时间超过指定时长(slow-log-slower-than 微妙为单位)的命令,最多存储slowlog-max-len条命令,多的话会删掉最前面的。
以链表形式存储(slowlog),每个节点的结构:

typedef struct slowlogEntry {
// 唯一标识符
long long id;
// 命令执行时的时间,格式为 UNIX 时间戳
time_t time;
// 执行命令消耗的时间,以微秒为单位
long long duration;
// 命令与命令参数
robj **argv;
// 命令与命令参数的数量
int argc;
} slowlogEntry;

SLOWLOG_GET命令查看所有慢查询日志。

5.4 监视器

发送MONITOR命令可以让一个普通的客户端变成一个监视器,发送后服务器在自己的monitors链表上追加发送该指令的客户端,只要是服务器执行的命令都会被发送到该客户端。

5.5 排序

SORT命令可以对包含数字值的键key的内容排序,快排实现。

一个有详细注释的Redis3.0版本的源码链接:this

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值