文章目录

    • 缓存理解
    • 带来问题
    • 本地缓存解决方案
    • 分布式缓存
    • 缓存读写模式/更新策略
  • 正文
    • 第一部分 数据结构与对象
      • 第2章 简单动态字符串
        • 2.1 SDS的定义
        • 2.2 SDS与C字符串的区别
          • 2.2.1 常数复杂度获取字符串长度
          • 2.2.2 杜绝缓冲区溢出
          • 2.2.3 减少修改字符串时带来的内存重分配次数
          • 2.2.4 二进制安全
          • 2.2.5 兼容部分C字符串函数
          • 2.2.6 总结
        • 2.3 SDS API
      • 第3章 链表
        • 3.1 链表和链表节点的实现
        • 3.2 链表和链表节点的API
      • 第4章 字典
        • 4.1 字典的实现
          • 4.1.1 哈希表
          • 4.1.2哈希表节点
          • 4.1.3 字典
        • 4.2 哈希算法
        • 4.3 解决键冲突
        • 4.4 rehash
        • 4.5 渐进式rehash
        • 4.6 字典API
      • 第5章 跳跃表
        • 5.1 跳跃表的实现
          • 5.1.1 跳跃表节点
      • 第6章 整数集合
        • 6.1 整数集合的实现
        • 6.2 升级
        • 6.3 升级的好处
          • 6.3.1 提升灵活性
          • 6.3.2 节约内存
        • 6.4 降级
      • 第7章 压缩列表
        • 7.1 压缩列表的构成
        • 7.2 压缩列表节点的构成
          • 7.2.1 previous_entry_length
          • 7.2.2 encoding
          • 7.2.3 content
        • 7.3 连锁更新
        • 7.4 压缩列表API
        • 7.5 重点回顾
      • 第8章 对象
        • 对象的类型与编码
          • 8.1.1 类型
          • 8.1.2 编码和底层实现
        • 8.2 字符串对象
          • 8.2.1 编码的转换
          • 8.2.2 字符串命令的实现
        • 8.3 列表对象
          • 8.3.1 编码转换
          • 8.3.2 列表命令的实现
        • 8.4 哈希对象
          • 8.4.1编码转换
          • 8.4.2 哈希命令的实现
        • 8.5 集合对象
          • 8.5.1 编码的转换
          • 8.5.2 集合命令的实现
        • 8.6 有序集合对象
          • 8.6.1 编码的转换
          • 8.6.2 有序集合命令的实现
        • 8.7 类型检查与命令多态
          • 8.7.1 类型检查的实现
          • 8.7.2 多态命令的实现
        • 8.8 内存回收
        • 8.9 对象共享
        • 8.10 对象的空转时长

缓存理解

缓存的思想:解决访问速度过于缓慢的问题。
比如CPU Cache缓存的是内存数据用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。
再比如操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把 快表 理解为一种特殊的高速缓冲存储器。

带来问题

  1. 系统复杂性增加:引入缓存之后,要维护缓存和数据库的数据一致性维护热点缓存等等。
  2. 系统开发成本增加:引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。但是,如果你只是简单的使用一下本地缓存存储一下简单的数据,并且数据量不大的话,那么就不需要单独去弄一个缓存服务。

本地缓存解决方案

  1. JDK自带的HashMap和ConcurrentHashMap
  2. Ehcache、Guava Cache、 Spring Cache本地缓存框架
  3. Caffeine

分布式缓存

分布式缓存看作是一种内存数据库的服务,最终作用就是提供缓存数据的服务。

缓存读写模式/更新策略

  1. Cache Aside Pattern(旁路缓存模式)
  2. Read/Write Through Pattern(读写穿透)
  3. Write Behind Pattern(异步缓存写入)

正文

第一部分 数据结构与对象

第2章 简单动态字符串

Redis使用一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。

2.1 SDS的定义

每个sds.h/sdshdr结构表示一个SDS的值:

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

SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作,都是由SDS函数自动完成的,所以这个空字符对SDS的使用者来说是完全透明的。好处是SDS可以直接重用一部分C字符串函数库里面的函数。

2.2 SDS与C字符串的区别

2.2.1 常数复杂度获取字符串长度
2.2.2 杜绝缓冲区溢出
2.2.3 减少修改字符串时带来的内存重分配次数

SDS通过未使用空间free解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加1,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。
SDS实现了空间预分配和惰性空间释放两种优化策略。

  • 空间预分配:额外分配未使用空间数量由以下的公式决定:小于1MB,分配和len属性同样大小的未使用空间。大于1MB,那么程序会分配1MB的未使用空间。
  • 惰性空间释放,使用free属性将这些字节的数量记录起来,并等待将来使用。
2.2.4 二进制安全
2.2.5 兼容部分C字符串函数
2.2.6 总结
C字符串 SDS
获取字符串长度的复杂度O(N) 获取字符串长度的复杂度O(1)
API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

2.3 SDS API

第3章 链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
链表被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。
应用:列表键的底层实现之一就是链表

3.1 链表和链表节点的实现

每个链表节点使用一个adlist.h/listNode结构来表示:

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;// 节点值复制函数,dup函数用于复制链表节点所保存的值void *(*dup)(void *ptr);// 节点值释放函数,free函数用于释放链表节点所保存的值void *(*free)(void *ptr);// 节点对比函数,match函数用于对比链表节点所保存的值和另一个输入值是否相等int (*match)(void *ptr, void *key);
}list;

实现特性总结如下:

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度O(1)
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

3.2 链表和链表节点的API

第4章 字典

字典经常作为一种数据结构内置在很多高级编程语言里面,但Redis所使用的C语言并没有内置这种数据结构,所以Redis构建了自己的字典实现
应用:对数据库的增、删、改、查操作时构建在对字典的操作之上的。

4.1 字典的实现

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

4.1.1 哈希表

Redis字典所使用的哈希表由dict.h/dictht结构定义:

typedef struct dictht {// 哈希表数组,dictEntry **table;// 哈希表大小unsigned long size;// 哈希表大小掩码,用于计算索引值// 总是等于size-1unsigned long sizemask;// 该哈希表已有节点的数量unsigned long used;
}dictht;

table属性是一个数组,数组找那个的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。
size属性记录了哈希表的大小,也即是table数组的大小,而used属性则记录了哈希表目前已有节点(键值对)的数量。
sizemask属性的值总是等于size-1,这个属性和哈希值一起决定了一个键应该被放到table数组的哪个索引上面。

4.1.2哈希表节点

哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存一个键值对:

typedef struct dictEntry {// 键void *key;// 值union{void *val;uint64_tu64;int64_ts64;}dictEntry;// 指向下个哈希表节点,形成链表struct dictEntry *next;
}dictEntry;
4.1.3 字典

Redis中的字典由dict.h/dict结构表示

typedef struct dict {// 类型特定函数dictType *type;// 私有数据void *privdata;// 哈希表dictht ht[2];// rehash 索引//当rehash不在进行时,值为-1in trehashidx; /*rehashing not in progress if rehashidx == -1*/
}dict;

key属性保存着键值对中的键,而v属性保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t函数,又或者是一个int64_t整数。
next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突的问题。
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:

  • type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
  • 而privdata属性则保存了需要传给那些类型特定函数的可选参数。

dictType结构表示

typedef struct dictType {// 计算哈希值的函数unsigned int (*hashFunction)(const void *key);// 复制键的函数void *(*keyDup)(void *privdata, const void *key);// 复制值的函数void *(*keyDup)(void *privdata, const void *obj);// 对比键的函数int (*keyCompare)(void *privdata, const void *key1, const void *key2);// 销毁键的函数void (*keyDestructor)(void *privdata, void *key);// 销毁值的函数void (*keyDestructor)(void *privdata, void *obj);
}dictType;

ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
rehashidx属性,记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1。

4.2 哈希算法

当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
Redis计算哈希值和索引值的方法如下:

# 使用字典设置的哈希函数,计算键key的哈希值
hash = dict->type->hashFunction(key);
# 使用哈希表的sizemask属性和哈希值,计算出索引值
# 根据情况不同,ht[x]可以是ht[0]或者ht[1]
index = hash & dict->ht[x].sizemask;

当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。

4.3 解决键冲突

Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

4.4 rehash

随着操作的不断执行,哈希表保存的键值对会逐渐地增多或减少,为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表大小进行相应的扩展或者收缩。可以通过rehash操作来完成。
Redis对字典的哈希表执行rehash步骤:

  1. 为字典分配ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性的值):(1)扩展操作,那么ht[1]的大小等于ht[0]。used*2的2n次方;(2)收缩操作,那么ht[1]的大小为第一个大于扥估ht[0].used的2n次方。
  2. 将保存在ht[0]中所有的键值对rehash到ht[1]上面:rehash指的事重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
  3. 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。

4.5 渐进式rehash

rehash动作不是一次性、集中式完成的,而是分多次、渐进式地完成。
原因:键值对数量过多,庞大的计算量可能会导致服务器在一段时间内停止服务。
渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新的操作上。
哈希表渐进式的详细步骤:

  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[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。

4.6 字典API

第5章 跳跃表

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
平均复杂度O(logN)、最坏复杂度O(N)
Redis使用跳跃表作为有序集合键的底层实现之一。
Redis只在两个地方用到了跳跃表

  • (1)一个是实现有序集合键,
  • (2)另一个是在集群节点中用作内部数据结构。

5.1 跳跃表的实现

Redis的跳跃表由redis.h/zskiplistNoderedis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构用于保存跳跃表节点的相关信息。
zskiplist结构包含属性:

  • header:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)
    zskiplistNode结构包含属性:
  • 层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针跨度前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。每个跳跃表节点的层高都是1至32之间的随机数
  • 后退(backward):节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
  • 分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到达排序。
  • 成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序
5.1.1 跳跃表节点

由redis.h/zskiplistNode结构定义:

typedef struct zskiplistNode {// 层struct zskiplistLevel {// 前进指针struct zskiplistNode *forward;// 跨度unsigned int span;} level[];// 后退指针struct zskiplistNode *backward;// 分值double score;// 成员对象robj *obj;
}zskiplistNode;

第6章 整数集合

整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

6.1 整数集合的实现

整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存为int16_t
int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
intset.h/intset结构表示一个整数集合:

typedef struct intset {// 编码方式unit32_t encoding;// 集合包含的元素数量uint32_t length;// 保存元素的数组int8_t contents[];
}intset;

contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
length属性记录整数集合包含的元素数量,即contents数组的长度。
虽然声明为int8_t类型的数组,但实际上conents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值:INTEST_ENC_INT16、INT32、INT64

6.2 升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,帧数集合需要先进行升级,然后才能将新元素添加到整数集合里面。
升级整数集合并添加新元素共分为三步进行:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置在正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
  3. 将新元素添加到底层数组里面。

6.3 升级的好处

6.3.1 提升灵活性
6.3.2 节约内存

6.4 降级

整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

第7章 压缩列表

压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

7.1 压缩列表的构成

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存开组成的顺序型数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
压缩列表各个组成部分的详细说明:

属性 类型 长度 用途
zlbytes uint32_t 4字节 一路整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算zlend的位置时使用
zltail uint32_t 4字节 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无须遍历整个列表就可以确定表尾节点的地址
zllen uint16_t 2字节 记录了压缩列表包含的节点数量:当这个属性的值小于UINT16_MAX(65535)时,这个属性的值就是压缩列表包含节点的数量;当这个值等于UINT16_MAX时,节点的真实数量需要遍历整个压缩列表才能计算得出
entryX 列表节点 不定 压缩列表包含的各个节点,节点的长度由节点保存的内容决定
zlend uint8_t 1字节 特殊值0xFF(十进制255),用于标记压缩列表的末端

7.2 压缩列表节点的构成

每个压缩列表节点可以保存一个字节数组或者一个整数值。
字节数组可以是以下三种长度:

  • 长度小于等于63(2的6次方-1)字节的字节数组;
  • 长度小于等于16383(2的14次方-1)字节的字节数组;
  • 长度小于等于4294967295(2的32次方-1)字节的字节数组;
    整数值可以是以下6中长度:
  • 4位长,介于0到12之间的无符号整数‘
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int16_t类型整数
  • int32_t类型整数
  • int64_t类型整数

每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成。

7.2.1 previous_entry_length

previous_entry_length以字节为单位,记录了压缩列表中前一个节点的长度,可以是1字节或者5字节。用前一节点的长度大于等于254字节,判断是否是1字节或5字节,

7.2.2 encoding

encoding属性记录了节点的content属性所保存数据的类型以及长度:
(1)1字节、2字节或5字节。值的最高位为00、01或者10是字节数组编码,数组的长度由编码出去最高两位之后的其它位记录。
(2)1字节长,值的最高位以11开头的是指整数编码。

7.2.3 content

content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点encoding属性决定的.

7.3 连锁更新

在特殊情况下产生的连续多次空间扩展操作称之为“连锁更新”。

7.4 压缩列表API

7.5 重点回顾

  • 压缩列表是一种为节约内存而开发的顺序型数据结构
  • 压缩列表被用作列表键和哈希键的底层实现之一
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
  • 添加新节点到压缩列表,或者从压缩列表删除节点,可能会引起连锁更新操作,但这种操作出现的几率不高

第8章 对象

Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象5种类型的对象,每种对象都用到了至少一种前面的数据结构。
好处:

  1. 根据不同的对象类型判断一个对象是否可以执行给定的命令
  2. 针对不同的使用场景,为对象设置多种不同的数据结构实现,优化对象在不同场景下的使用效率
  3. Redis的对象系统还实现了基于引用计数技术的内存回收机制;还通过引用计数技术实现了对象共享机制,这一机制可以再适当的条件下,通过让多个数据库键共享同一个对象来节约内存
  4. Redis的对象带有访问时间记录信息,在服务器启用了maxmemory功能的情况下,空转时长较大的哪些键可能会优先被服务器删除。

对象的类型与编码

Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象。
每个对象都由一个redisObject结构表示:

typedef struct redisObject {// 类型unsigned type:4;// 编码unsigned encoding:4;// 指向底层实现数据结构的指针void *ptr;// ...
}robj;
8.1.1 类型

对象的类型

类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

键总是一个字符串对象,而值可以是任意一个。

8.1.2 编码和底层实现

对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。

redis> OBJECT ENCODING msg

8.2 字符串对象

字符串对象的编码可以是int、raw或者embstr。

8.2.1 编码的转换

int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
append命令,向一个保存整数值的字符串对象追加了一个字符串值,因为追加操作只能对字符串值执行,所以程序会先将之前保存的整数值转换为字符串值,然后再执行追加操作。
Redis没有为embstr编码的字符串对象编写任何相应的修改程序(int和raw有),所以实际上是只读。修改时,先转为raw,再执行修改命令。

8.2.2 字符串命令的实现

8.3 列表对象

列表对象的编码可以是ziplist或者linkedlist。

  • ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素。
  • linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
  • 字符串对象是Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象。
8.3.1 编码转换

列表对象使用ziplist编码的条件:

  • 列表对象保存的所有字符串的长度都小于64字节;
  • 列表对象保存的元素数量小于512个;

不能满足这两个条件的列表对象需要使用linkedlist编码。
上述两个条件的上限值可以修改。配置文件中list-max-ziplist-value和list-max-ziplist-entries

8.3.2 列表命令的实现

8.4 哈希对象

哈希对象的编码可以是ziplist和hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表的表尾。
hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:

  • 字典的每个键都是一个字符串对象,对象中保存了键值对的键;
  • 字典的每个值都是一个字符串对象,对象中保存了键值对的值;
8.4.1编码转换

当哈希对象可以同时满足以下2个条件时,哈希对象使用ziplist编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个;

不能满足这两条件的哈希对象需要使用hashtable编码。
上述两个条件的上限值可以修改。配置文件中hash-max-ziplist-value和hash-max-ziplist-entries

8.4.2 哈希命令的实现

8.5 集合对象

集合对象的编码可以是intset或者hashtable。
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。

8.5.1 编码的转换

当集合对象可以同时满足以下两个条件时,对象使用intset编码:

  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个。

不能满足这两个条件的集合对象需要使用hashtable编码。
上述两个条件的上限值可以修改。配置文件中set-max-ziplist-entries

8.5.2 集合命令的实现

8.6 有序集合对象

有序集合的编码可以是ziplist和skiplist
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
压缩列表内的集合元素按分值从小到大排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。

8.6.1 编码的转换

当集合对象可以同时满足以下两个条件时,对象使用ziplist编码:

  • 有序集合保存的元素数量小于128个
  • 有序集合保存的所有元素成员的长度都小于64字节。
    不能满足这两个条件的集合对象需要使用skiplist编码。
    上述两个条件的上限值可以修改。配置文件中zset-max-ziplist-entries和zset-max-ziplist-value。
8.6.2 有序集合命令的实现

8.7 类型检查与命令多态

Redis中用于操作键的命令基本上可以分为两种类型
其中一种命令可以对任何类型的键执行,比如DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等。
另一种命令只能对特定类型的键执行。

8.7.1 类型检查的实现

类型特定命令锁进行的类型检查是通过redisObject结构的type属性来实现的

  • 在执行一个类型特定命令之前,服务器会先检查输入数据库键的值对象是否为执行命令锁需的类型,如果是的话,服务器就对执行指定的命令;
  • 否则,服务器将拒绝执行命令,并想客户端返回一个类型错误。
  • 如LLEN命令,在执行LLEN命令之前,服务器会先检查输入数据库键的值对象是否为列表类型,也即是,检查值对象redisObject结构type属性的值是否为REDIS_LIST,如果是的话,服务器对键执行。
  • 否则的话,服务器就拒绝执行命令并向客户端返回一个类型错误。
8.7.2 多态命令的实现

Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据值对象的编码方式,选择正确的命令实现代码来执行
如,列表对象有ziplist和linkedlist两种编码,前种用压缩列表后者是双端链表API。执行LLEN命令,除了要确保执行命令是列表键之外,还需要根据键的值对象所使用的编码来选择正确的LLEN命令实现:(1)如果列表对象的编码为ziplist,那么说明列表对象的实现为压缩列表,程序使用ziplistLen函数来返回表的长度;(2)为linkedlist,列表对象的实现为双端链表,使用listLength函数来返回双端链表的长度。
所以借用面向对象的术语,可以认为LLEN命令时多态的。

8.8 内存回收

因为C语言不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收
每个对象的引用计数信息由redisObject结构的refcount属性记录:

typedef struct redisObject {// ...// 引用计数int refcount;// ...
}robj;

引用计数信息随着对象的使用状态而不断变化:

  • 在创建一个新对象时,初始化为1;
  • 当对象被一个新程序使用时,加1;
  • 当对象不再被一个程序使用时,减1;
  • 当对象的引用计数值变为0时,对象所占用的内存会被释放。

8.9 对象共享

对象的引用计数属性除了实现内存回收机制外,还带有对象共享的作用。
在Redis中,让多个键共享一个值对象需要执行以下两个步骤:

  1. 将数据库键的值指针指向一个现有的值对象
  2. 将被共享的值对象的引用计数增1。

目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999所有整数值。
Redis只对包含整数值的字符串对象进行共享。原因:共享对象越复杂,验证相同对象消耗的CPU时间越多。

8.10 对象的空转时长

redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间:

typedef struct redisObject {// ...unsigned lru:22;// ...
}robj;OBJECT IDLETIME命令可以打印空转时长。

空转时长:通过当前时间减去键的值对象lru时间计算得出的
键的空转时长作用:如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过maxmemory上限值后,空转时长较高的那部分键会优先被释放,从而回收内存。

Redis设计与实现-笔记(一)相关推荐

  1. Redis设计与实现笔记2

    文章目录 1.主从复制 主从复制的作用 实现 同步 命令传播 心跳检测 2.Sentinel 3.集群 节点 槽指派 重新分片 ASK错误 故障检测和故障转移 参考<Redis设计与实现> ...

  2. Redis设计与实现 笔记 第十七章 集群 cluster

    集群 Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能 17.1 节点 一个 Redis 集群通常由多个节点组成,在刚开始的时候,每个节点都 ...

  3. redis设计与实现 笔记(一)

    Redis简单动态字符串 Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic st ...

  4. Redis设计与实现笔记

    第一部分:数据结构与对象     简单动态字符串     链表     字典     跳跃表     整数集合     压缩列表     对象 第二部分:单机数据库的实现     数据库     RD ...

  5. 《redis 设计与实现》读书笔记

    大家好,我是烤鸭:     <redis 设计与实现>,读书笔记. 第一部分 数据结构与对象 第2章 简单动态字符串 Redis 使用SDS 作为字符串表示. O(1) 复杂度获取字符串长 ...

  6. Redis 设计与实现 读书笔记(菜鸟版)

    Redis 设计与实现 读书笔记(简略版) 写在前面 第一章(内部数据结构) SDS List Dictionary Rehash Rehash 与 COW 渐进式Rehash 字典收缩 Skipli ...

  7. 《Redis设计与实现》笔记|SDS动态字符串|链表字典跳跃表整数集合压缩列表结构|redis中的对象|数据库原理|RDB持久化|AOF持久化|事件与多路利用模型|发布订阅原理|事务原理|慢查询日志

    <Redis设计与实现>笔记 前记: 参考配套网站:http://redisbook.com 带注释的源码地址:https://github.com/huangz1990/redis-3. ...

  8. 《redis设计与实现》 读书笔记

    <redis设计与实现> 作者:黄健宏 读书笔记 一.前言 什么是redis: Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数 ...

  9. redis设计与实现学习笔记1

    文章目录 1.对象 1.1 类型 1.2 内存回收 1.3 对象共享 1.4 对象空转时长 2.单机数据库 2.1 RDB 2.2 AOF 2.3 事件 2.4客户端 2.5服务器 3.常用命令 参考 ...

最新文章

  1. Pandas中iloc、loc、ix三者的区别
  2. 小派(PiMax)vr Artisan固件升级失败,没反应了
  3. python之sys模块详解_(转)python之os,sys模块详解
  4. html表单输入的文本框的值,网页设计关于表单输入框的技巧代码
  5. 使用intellij查看scala变量的具体类型
  6. 公共计算机课程思政建设实施方案,公共计算机教研组开设课程思政公开课
  7. 12 CO配置-控制-成本中心会计-将分解结构分配至成本中心
  8. pq 中m函数判断嵌套_你还在用IF函数进行逻辑判断吗?试试PQ,简单又实用
  9. 【数字信号调制】基于matlab GUI ASK+OOK+BPSK+8PSK+QPSK+AM调制解调【含Matlab源码 1368期】
  10. 一个示例让你明白界面与数据分离
  11. php 100元 换算,货币换算的PHP脚本
  12. 15款免费的Wi-Fi安全测试工具
  13. 一名软件测试工程师的日常
  14. 服务总线 开源_UltraESB企业服务总线将开源
  15. 数据科学家VS大数据专家VS数据分析师:有什么不同?
  16. 各大互联网公司薪酬盘点!哪一家薪资最高?
  17. VB 生成0~1的随机小数(不包含1),再用VB,感慨万千
  18. 离散数学 —— 集合论(集合的传递性与自反性、幂集、交集、并集、相对补集、绝对补集、对称差或异或、序偶或序对、集合的规模或基数)
  19. 高通平台读取USB ID
  20. ilove中文_ILOVEYOU是什么意思

热门文章

  1. TensorFlow搭建CNN实现时间序列预测(风速预测)
  2. M1芯片实现Kail虚拟机(无Parallels)
  3. 微信小程npm相关问题
  4. 吴恩达深度学习课件和课后习题
  5. [论文阅读]《Using Conditional Functional Dependency to Discover Abnormal Data in RDF Graphs》阅读笔记
  6. 什么是SWFObject,如何使用!
  7. 多线程高并发,spring整合kafka消费
  8. jQuery介绍、jQuery引入
  9. 漏洞预警|Apache Karaf 存在远程代码执行漏洞
  10. 3分钟了解Kfaka