《Redis设计与实现》读书笔记(一)

引言

详细、系统地学习Redis,这本书无疑是最佳选择。刷再多的文章比不上看书哈哈,个人观点~

《Redis设计与实现》写得比较通俗易懂,零基础的也易上手,从实现底层出发介绍但又没有那么多让人看者犯晕的源码,涉及到稍微复杂一点的逻辑都用伪代码和流程图实现。唯一的缺点就是内容有一点点冗余。

很早就买了,可惜看的太慢了,只有四分之一吧,也就是第一部分,下面把冗余的部分提炼一下,总结一下我看到的精华。

本文主要介绍Redis的数据结构与对象部分。Redis以5大对象来满足键的数据结构:字符串对对象,列表对象,哈希对象,集合对象,有序集合对象。这些对象的底层实现依赖特定的编码类型,而某种编码类型又对应1种或多种数据结构。下面分别介绍简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表这6种基本数据结构在Redis中的实现。

先上文章思维导图:

文章导读

  • 简单动态字符串(数据结构,区别,内存分配策略,复杂度)
  • 链表(数据结构,复杂度)
  • 字典(数据结构,哈希算法,渐进式rehash,复杂度)
  • 跳跃表(数据结构,插入,查找,删除,复杂度)
  • 整数集合(数据结构,升降级,复杂度)
  • 压缩列表(数据结构,连锁更新,复杂度)
  • 对象(存储结构,5大对象及编码结构,命令类型,内存回收,对象共享)
  • 总结

一、简单动态字符串

Redis中,涉及可以被修改的字符串值时,都用简单动态字符串(simple dynamic string,SDS)来实现。比如包含字符串值的键值对在底层的实现。C字符串(C语言中传统字符串,以空字符串结尾的字符数组)则用于无须对字符串进行修改的地方,比如日志打印。

SDS还被用作缓冲区,比如AOF模块中的AOF缓冲区,客户端状态中的输入缓冲区。

1.1 SDS定义

struct sdshdr{
    //buf已使用的字节数
    int len;
    //buf未使用的字节数
    int free;
    //字节数组,用于保存字符串
    char buf[];
}

buf遵循C字符串以空字符串结尾的惯例,保存空字符串的1字节空间不计算在SDS的len属性里面,并为空字符分配额外1字节空间,对用户来说是透明的。

如中展示了SDS的数据结构,5字节未使用空间,已使用5字节,buf存储了字符串值,最后一个字节保存了空字符'\0'。这里要注意的是,free和len的计算不涉及空字符。

1.2 SDS与C字符串的区别

  1. SDS有常数级的时间复杂度获取字符串长度。
    由于C字符串不会记录自身长度,因此只能遍历,直到遇到结尾的空字符为止,时间复杂度为O(N)。而SDS对于字符串长度的记录都是在其API中执行的,所以时间复杂度为O(1)
  2. SDS杜绝缓冲区溢出。
    由于C字符串未记录自身长度,容易导致缓冲区溢出。在执行字符串拼接时,如果没有足够的空间,并且相邻内存地址被其他字符串占用时,字符串的数据将溢出,且容易意外修改相邻的字符串内容。相比而言,SDS会将这种情况扼杀在摇篮之中,SDS API先判断空间是否满足,如果不满足则将空间扩展至执行修改所需的大小
  3. SDS拥有内存分配策略,详见1.3。
  4. SDS API都是二进制安全的。
    C字符串的字符必须符合某种编码,并且中间不能有空字符,否则读取时会被误以为是字符串结尾。种种局限使得C字符串只能存文本,不能存图片,音频,视频,压缩文件等二进制数据。 为确保Redis对不同使用场景的支持,SDS API都是二进制安全的,也就是所有SDS API都会以二进制的方式存取buf中的数据,数据的写入和读出都是一个样的。由于SDS读取时并不是依靠空字符来判断结束的,而是len属性,所以是二进制安全的。
  5. 兼容部分C字符串函数
    SDS虽然都是二进制安全的,但也遵循以空字符结尾的习惯。SDS API总会在buf数组分配空间时多分配一个字节用于容纳空字符,这是为了保存文本的SDS重用一部分<string.h>库函数,避免代码重复。

1.3 内存分配策略

由于C字符并不记录自身长度,并且需要一个字符空间保存空字符串,因此每次增长或缩短字符串时,就要对其进行一次内存重分配操作。增长字符串时要看空间是否够用,否则会有缓冲区溢出;缩短字符串要释放不用的空间,否则会有内存泄漏

Redis经常被用于速度要求严苛,数据被频繁修改的场合,每次修改字符串都要重新分配内存,就会占用很多时间。为避免这个问题,redis采用了空间预分配惰性空间释放两种策略。

空间预分配

空间预分配用于优化SDS字符串增长操作。在扩展SDS空间前,SDS API会先检查未使用空间够不够,如果不够,则进行空间预分配。此时,程序不仅会为SDS分配修改所必须要的空间,还为其分配额外未使用的空间。

  • 修改后的SDS<1MB,程序分配和len属性同样大小的未使用空间,此时SDS的len与free大小相等。比如修改后实际存储字符串的空间变为13字节,那么len=13,free=13,buf数组整体的长度=13+13+1(额外1字节保存空字符)。
  • 修改后SDS>=1MB。程序会分配1MB的未使用空间。比如修改后实际存储字符串的空间变为2MB,那么len=2M,free=1MB,buf数组整体的长度=2MB+1MB+1byte。

通过空间的预分配,将连续增长N次字符串需要的内存分配次数从一定需要N次变为最多N次。因而可以减少连续执行字符串增长操作所需的内存重分配的次数。

惰性空间释放

惰性空间的释放用于优化SDS字符串缩短操作。当SDS API需要缩短保存的字符串时,程序并不立即回收这部分内存,而是使用free属性将字节的数量记录,等待使用。与此同时,SDS提供了相关API,在有需要时,真正释放未使用空间,不需要担心惰性空间造成的内存浪费。

C字符串与SDS的区别简单来说:

[公式]

1.4 SDS时间复杂度

SDS相关操作及时间复杂度:

[公式]

二、链表

当一个列表键包含了数量比较多的元素,或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。

2.1 链表和链表节点的实现

typedef struct listNode{
    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    //节点的值
    void *value;
}listNode;

节点由前驱后继组成,多个节点组成的链表为双端链表。

使用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;

整个链表串起来后,如下图:

Redis的链表特性可以总结如下:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值