文章目录

  • 缓存理解
  • 带来问题
  • 本地缓存解决方案
  • 分布式缓存
  • 缓存读写模式/更新策略
  • 正文
  • 第一部分 数据结构与对象
  • 第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-1
	unsigned 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不在进行时,值为-1
	in 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 字符串命令的实现

redis 设计范例 redis设计与实现最新版_redis

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上限值后,空转时长较高的那部分键会优先被释放,从而回收内存。