读书笔记:Redis5设计与源码分析

Redis5设计与源码分析, 陈雷

本书赞誉

前言

第1章 引言1

1.1 Redis简介1

Redis由Salvatore Sanfilippo在2009年发布初始版本,开源后不断发展壮大。
Redis优点:Redis的工作模式为单线程,不需要线程间的同步操作。Redis采用单线程主要因为其瓶颈在内存和带宽上,而不是CPU。

1.2 Redis 5.0的新特性2

将集群管理功能完全用C语言集成到redis-cli中。

1.3 Redis源码概述3

Redis源代码主要存放在src文件夹中。
核心部分:
(1)基本的数据结构

  • 动态字符串 sds.c
  • 整数集合intset.c
  • 压缩列表ziplist.c
  • 快速链表quicklist.c
  • 字典dict.c
  • Streams的低层实现结构listpack.c和rax.c

(2)Redis数据类型的低层实现

  • Redis对象object.c
  • 字符串t_string.c
  • 列表t_list.c
  • 字典t_hash.c
  • 集合及有序集合t_set.c和t_zset.c
  • 数据流t_stream.c

(3)Redis数据库的视线

  • 数据库的底层实现db.c
  • 持久化reb.c和aof.c

(4)Redis服务端和客户端实现

  • 事件驱动ae.c和ae_epoll.c
  • 网络连接anet.c和networking.c
  • 服务端程序server.c
  • 客户端程序redis_cli.c

(5)其他

  • 主从复制replication.c
  • 哨兵sendtinel.c
  • 集群cluster.c
  • 其他数据结构,如hyperloglog.c、geo.c等
  • 其他功能,如pub/sub、Lua脚本

1.4 Redis安装与调试4

使用redis-server启动服务端程序

/usr/local/bin/redis-server

使用redis-cli连接Redis服务器并添加键值对

redis-cli -h 127.0.0.1 -p 6379

GDB启动redis-server服务端程序

gdb /usr/local/bin/redis-server

1.5 本章小结6

第2章 简单动态字符串7

2.1 数据结构7

为了方便上层的接口调用,该结构还需要记录一些统计信息,如当前数据长度和剩余容量等,例如:

struct sds{
	int len;// buf中已占用字节数
	int free;// buf中剩余可用字节数
	char buf[];//数据空间
}

说明:内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。
柔性数组成员(flexible array member),也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。
之所以用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置)。
源码中的__attribute__((__packed__))需要重点关注。一般情况下,结构体会按其所有变量大小的最小公倍数做字节对齐,而用packed修饰后,结构体则变为按1字节对齐。

2.2 基本操作11
2.2.1 创建字符串11
2.2.2 释放字符串12
2.2.3 拼接字符串12
2.2.4 其余API15
2.3 本章小结15

第3章 跳跃表17

对于有序集合的底层实现,我们可以使用数组、链表、平衡树等结构。数组不便于元素的插入和删除;链表的查询效率低,需要遍历所有元素;平衡树或者红黑树等结构虽然效率高但实现复杂。Redis采用了一种新型的数据结构--跳跃表 。

3.1 简介17

通过将有序集合的部分节点分层,由最上层开始依次向后查找,如果本层的next节点大于要查找的值或next节点为NULL,则从本节点开始,降低一层继续向后查找,依次类推,如果找到则返回节点;否则返回NULL。采用该原理查找节点,在节点数量比较多 时,可以跳过一些节点,查询效率大大提升,这就是跳跃表的基本思想。

3.2 跳跃表节点与结构19

3.2.1 跳跃表节点19

跳跃表节点结构体

typedef struct zskiplistNode {
	struct zskiplistLevel {
		struct zskiplistNode *forward;
	}level[];
}zskiplistNode;

3.2.2 跳跃表结构20
3.3 基本操作20
3.3.1 创建跳跃表21
3.3.2 插入节点22
3.3.3 删除节点28
3.3.4 删除跳跃表30
3.4 跳跃表的应用31
3.5 本章小结32

第4章 压缩列表33

当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构。

4.1 压缩列表的存储结构33

压缩列表结构

zlbytes 压缩列表的字节长度
zltail 压缩列表尾元素相对偏移量
zllen 压缩列表的元素个数
entryX 压缩列表存储的元素
...
zlend 压缩列表的结尾

4.2 结构体35
4.3 基本操作37
4.3.1 创建压缩列表37
4.3.2 插入元素38
4.3.3 删除元素42
4.3.4 遍历压缩列表44
4.4 连锁更新44
4.5 本章小结45

第5章 字典47

5.1 基本概念47

字典又称散列表,是用来存储键值(key-value)对的一种数据结构。
5.1.1 数组48

5.1.2 Hash函数49

好的Hash算法是经过Hash计算后其输出值具有强随机分布性。例如Daniel J.Bernstein在comp.lang.c上发布的"times 33"散列函数,其使用的核心算法是:“hash(i)=hash(i-1)*33+str[i]”,这是针对字符串已知的最好的散列函数之一,因其计算速度快,而且输出值分布得很好。
那过大的Hash值与较小的数组下标怎么关联呢?最简单的办法是,用Hash值与数组容量取余,会得到一个永远小于数组容量大小的值,此时的值也就恰好可以当作数组下标来使用,我们把取余之后的值称为键在该字典中的索引值,即“索引值=数组下标值”。但此方法并不是完美的,还会出现一个问题,Hash冲突。

5.1.3 Hash冲突51

5.2 Redis字典的实现52

Hash表中的元素是用dictEntry结构体来封装的

trypedef struct dictEntry{
	vodi *key; /* 存储键 */
	union{
		void *val;  /* db.dict中的val */
		uint64_t u64;
		int64_t s64;  /* db.expires中存储过期时间 */
		double d;
	}v;               /* 值,是个联合体 */
	struct dictEntry *next;  /* 当Hash冲突时,指向冲突的元素,形成单链表 */
}dictEntry;

5.3 基本操作55

5.3.1 字典初始化55

5.3.2 添加元素56

redis-server启动完后,再启动redis-cli连上Server,执行命令:

# ./redis-cli
> set k1 v1

5.3.3 查找元素60
5.3.4 修改元素61
5.3.5 删除元素61

5.4 字典的遍历62

遍历数据库的原则为:1. 不重复出现数据;2. 不遗漏任何数据。遍历Redis整个数据库主要有两种方式:全遍历(例如keys命令)、间断遍历(hscan命令)。

5.4.1 迭代器遍历62

迭代器——可在容器(容器可为字典、链表等数据结构)上遍历访问的接口,设计人员无须关心容器的内容。
Reids源码中迭代器实现的数据结构:

trypedef struct dictIterator{
	dict *d; /* 迭代的字典 */
	int index;/* 当前迭代到Hash表中哪个索引值 */
	int table, safe; /* table用于表示当前正在迭代的Hash表, safe用于表示当前创建的是否为安全迭代器 */
	dictEntry *entry, *nextEntry;/* 当前节点,下一个节点 */
	long long fingerprint; /* 字典的指纹,当字典未发生改变时,该值不变,发生改变时则值也随着改变 */
}dictIterator;

5.4.2 间断遍历65
5.5 API列表70
5.6 本章小结71

第6章 整数集合72

6.1 数据存储72

intset结构体如下

trypedef struct intset{
	uint32_t encoding; /* 编码类型 */
	uint32_t length; /* 元素个数 */
	int8_t contents[]; /* 柔性数组,根据encoding字段决定几个字节表示一个元素 */
}intset;

6.2 基本操作75
6.2.1 查询元素75
6.2.2 添加元素78
6.2.3 删除元素82
6.2.4 常用API83
6.3 本章小结85

第7章 quicklist的实现86

7.1 quicklist简介86

quicklist是一个双向链表,链表中的每个节点是一个ziplist结构。
7.2 数据存储87

7.3 数据压缩91

7.3.1 压缩92

LZF数据压缩的基本思想是:数据与前面重复的,记录重复位置以及重复长度,否则直接记录原始数据内容。

7.3.2 解压缩93
7.4 基本操作94
7.4.1 初始化94
7.4.2 添加元素95
7.4.3 删除元素96
7.4.4 更改元素98
7.4.5 查找元素99
7.4.6 常用API100
7.5 本章小结101

第8章 Stream102

消息队列是分布式系统中不可缺少的组件之一,主要有异步处理、应用解耦、限流削峰的功能。目前应用较为广泛的消息队列有RabbitMQ、RocketMQ、Kafaka等。Redis的stream。
8.1 Stream简介102
8.1.1 Stream底层结构listpack103
8.1.2 Stream底层结构Rax简介104
8.1.3 Stream结构108
8.2 Stream底层结构listpack的实现112
8.2.1 初始化112
8.2.2 增删改操作112
8.2.3 遍历操作113
8.2.4 读取元素113
8.3 Stream底层结构Rax的实现114
8.3.1 初始化114
8.3.2 查找元素114
8.3.3 添加元素116
8.3.4 删除元素118
8.3.5 遍历元素120
8.4 Stream结构的实现123
8.4.1 初始化124
8.4.2 添加元素124
8.4.3 删除元素125
8.4.4 查找元素128
8.4.5 遍历129
8.5 本章小结131

第9章 命令处理生命周期132

文件事件即socket的读写事件,时间事件用于处理一些需要周期性执行的定时任务。

9.1 基本知识132

9.1.1 对象结构体robj132
9.1.2 客户端结构体client136
9.1.3 服务端结构体redisServer138
9.1.4 命令结构体redisCommand139

9.1.5 事件处理141

事件驱动程序通常存在while/for循环,循环等待事件发生并处理,Redis也不例外

while(!eventLoop->stop){
	if(eventLoop->beforesleep != NULL)
		eventLoop->beforesleep(eventLoop);
	aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_AFTER_SLEEP);
}

socket读写操作有阻塞与非阻塞之分。采用阻塞模式时,一个进程只能处理一条网络连接的读写事件,为了同时处理多条网络连接,通常会采用多线程或者多进程,效率低下; 非阻塞模式下,可以使用目前比较成熟的I/O多路复用模型,如select/epoll/kqueue等,视不同操作系统而定。

epoll是Linux内核为处理大量并发网络连接而提出的解决方案,能显著提升系统CPU利用率。epoll使用非常简单,总共只有3个API: epoll_ create函数创建一个epoll专用的文件描述符,用于后续epoll相关API调用: epoll_ctl 函数向epoll注册、修改或删除需要监控的事件; epoll_wait 函数会阻塞进程,直到监控的若干网络连接有事件发生。

Redis并没有直接使用epoll提供的API,而是同时支持4种I/O多路复用模型,并将这些模型的API进一步统一封装,由文件ae_evport.cae_epollc. ae_kqueuc.cac_select.c实现。

以epoll为例,acApiCreate 函数是对epoll_create 的封装; aeApiAddEvent 函数用于添加事件,是对epoll_ctl 的封装; acApiDelEvent 函数用于删除事件,是对epoll_ctl 的封装;
aeApiPoll是对epoll_wait的封装。

2.时间事件
Redis服务器内部有很多定时任务需要执行,比如定时清除超时客户端连接,定时删除过期键等,定时任务被封装为时间事件aeTimeEvent对象,多个时间事件形成链表,存储在aeEventLoop结构体的timeEventHead字段,它指向链表首节点。

注意时间事件处理函数timeProc返回值retval, 其表示此时间事件下次应该被触发的时间,单位为毫秒,且是一个相对时间,即从当前时间算起,retval毫秒后此时间事件会被触发。

Redis 创建时间事件节点的函数为aeCreateTimeEvent,内部实现非常简单,只是创建时间事件并添加到时间事件链表。

9.2 server启动过程149

9.2.1 server初始化149

服务器初始化主流程可以简要分为7个步骤:

  1. 初始化配置,包括用户可配置的参数,以及命令表的初始化;
  2. 加载并解析配置文件;
  3. 初始化服务端内部变量,其中就包括数据库;
  4. 创建事件循环eventLoop ;
  5. 创建socket并启动监听;
  6. 创建文件事件与时间事件;
  7. 开启事件循环。

9.2.2 启动监听152

Redis过期键删除有两种策略:1 访问数据库键时,校验该键是否过期,如果过期则删除;2 周期性删除过期键,beforeSleep函数与serverCron函数都会执行。
当应用层调用write函数发送数据时,TCP 并不一定会立刻将数据发送出去,根据Nagle算法,还必须满足一定条件才行。 Nagle是这样规定的:如果数据包长度大于一定门限时,则立即发送; 如果数据包中含有FIN (表示断开TCP链接)字段,则立即发送; 如果当前设置了TCP_NODELAY选项,则立即发送; 如果以上所有条件都不满足,则默认需要等待200毫秒超时后才会发送。

TCP是可靠的传输层协议,但每次都需要经历“三次握手”与“四次挥手”,为了提升效率,可以设置SO_KEEPALIVE, 即TCP长连接,这样TCP传输层会定时发送心跳包确认该连接的可靠性。

9.3 命令处理过程155

此过程分为3个阶段:解析命令请求,调用命令和返回结果给客户端。

9.3.1 命令解析156

TCP是一种基于字节流的传输层通信协议,因此接收到的TCP数据不一定是一个完整的数据包,其有可能是多个数据包的组合,也有可能是某一 个数据包的部分,这种现象被称为半包(应用层一个大包拆多个小包发送)与粘包(应用层多个包合并成一个包发送)。
Redis采用自定义协议格式实现不同命令请求的区分,例如redis-cli客户端键入命令:

SET redis-key value1

客户端会将命令转换为一下协议格式

*3\r\n$3\r\nSET\r\n$9\r\nredis-key\r\n$6\r\nvalue1\r\n

其中,换行符\r\n用于区分命令请求的若干参数,“*3”表示该命令请求有3个参数,“$3”“$9”“$6”等表示该参数字符串长度

Redis服务器接收到的命令请求首先存储在客户端对象的querybuf输人缓冲区,然后解析命令请求各个参数,并存储在客户端对象的argv(参数对象数组)和argc (参数数目)字段。

flowchart TD     a1[processInputBuffer] -->a2{请求类型为内联命令?}     a3[processMultibulkBuffer解析命令请求] a4[调用processInlineBuffer解析命令请求] a5{命令请求解析成功?}     a2 -->|否| a3-->a5     a2 -->|是| a4-->a5     a5 -->|否| a6[释放客户端对象resetClient]     a5 -->|是| a7[执行命令processCommand]

假设客户端命令请求为“SET redis-key value1”,在函数processMultibulkBuffer添加断点,GDB打印客户端输
人缓冲区内容如下:

(gdb) p c->querybuf
$3 = (sds) 0x7fff "*3\r\n$3\r\nSET\r\n$9\r\nredis-key\r\n$6\r\nvalue1\r\n"

9.3.2 命令调用159

校验: 如果配置文件中使用指令“requirepass password" 设置了密码,且客户端未认证通过,只能执行auth命令,auth 命令格式为“ AUTH password”。

if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand) {
	addReply (C,shared . noautherr);
	return C_ _OK;
}

9.3.3 返回结果161
9.4 本章小结163

第10章 键相关命令的实现164

10.1 对象结构体和数据库结构体回顾164

对象的操作离不开redisObject结构体,数据库的操作离不开redisDb结构体

10.1.1 对象结构体redisObject164

当用于LRU时表示最后一次访问时间,当用于LFU时,高16位记录分钟级别的访问时间,低8位记录访问频率0到255。

10.1.2 数据库结构体redisDb166

redisDb结构体定义如下:

typedef struct redisDb { 
	dict *dict; /* 键空间字典 */ 
	dict *expires; /* key的超时时间字典 */ 
	dict *blocking_keys; /* 阻塞的key */ 
	dict *ready_keys; /* 准备好的key */ 
	dict *watched_keys; /* 执行事务的key */ 
	int id; /* 数据库ID */ 
	long long avg_ttl; /* 平均生存时间,用于统计 */ 
	list *defrag_later; /* 逐渐尝试逐个碎片整理的key列表*/ 
} redisDb;

10.2 查看键信息166

10.2.1 查看键属性166
10.2.2 查看键类型169

10.2.3 查看键过期时间170

格式:

ttl key

说明: 返回键剩余的生存时间,单位秒。

10.3 设置键信息171
10.3.1 设置键过期时间171
10.3.2 删除键过期时间172
10.3.3 重命名键173
10.3.4 修改键最后访问173
10.4 查找键174
10.4.1 判断键是否存在174
10.4.2 查找符合模式的键175
10.4.3 遍历键176
10.4.4 随机取键177

10.5 操作键178

10.5.1 删除键178

格式:

del key [key ...]

说明: 以阻塞方式删除key。

unlink命令
该命令以异步方式删除key,这可以避免del删除大key的问题。

void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
	struct bio_job *job = zmalloc(sizeof(*job));
	job->time = time(NULL);
	job->arg1 = arg1;
	job->arg2 = arg2;
	job->arg3 = arg3;
	pthread_mutex_lock(&bio_mutex[type]); // 线程互斥锁
	listAddNodeTail(bio_jobs[type],job); // 追加任务到对应类型的链表尾部
	bio_pending[type]++; // 标记未处理数据量
	pthread_cond_signal(&bio_newjob_cond[type]); // 唤醒一个异步处理线程
	pthread_mutex_unlock(&bio_mutex[type]); // 解锁
}

bioCreateBackgroundJob函数创建一个bio任务
10.5.2 序列化/反序列化键182
10.5.3 移动键183
10.5.4 键排序185
10.6 本章小结187

第11章 字符串相关命令的实现188

11.1 相关命令介绍188

11.2 设置字符串189

格式:

SET key value [NX] [XX] [EX ] [PX ]

参数:
NX: 当数据库中key不存在时,可以将key-value添加到数据库。
XX: 当数据库中key存在时,可以将key-value设置到数据库,与 NX参数互斥。
EX: key的超时秒数。
PX: key的超时毫秒数,与EX参数互斥。

11.2.1 set命令189
11.2.2 mset命令195
11.3 修改字符串196
11.3.1 append命令196
11.3.2 setrange命令197
11.3.3 计数器命令197
11.4 字符串获取199
11.4.1 get命令199
11.4.2 getset命令199
11.4.3 getrange命令199
11.4.4 strlen命令200
11.4.5 mget命令201

11.5 字符串位操作201

11.5.1 setbit命令201
11.5.2 getbit命令203

11.5.3 bitpos命令203

bitpos命令将key所存储的字符串当作一个字节数组,从第start个字节开始(注意已经经过了8*start个索引),返回第一个被设置为bit值的 索引值。
CPU可以一次性从内存读取8字节的数据,有的CPU甚 至只能从地址的8字节整数倍开始获取数据。如果字符串如果比较长, 那么字符串的首地址可能不在8字节的整数倍上,所以需要先处理这部 分数据。如果这部分数据不是0,则表示要查询的值在 这个字节上,标识found=1。

11.5.4 bitcount命令205

计算二进制串中1的数量比较常见的算法有3种
(1)遍历法
(2)快速法。每次将二进制串的最低位1变为0,直到n变为0为止。
(3)variable-precision swar算法。

int swar(uint32_t i)
{
	//计算每2位二进制数中1的个数
	i = ( i & 0x55555555) + ((i >> 1) & 0x55555555);
	//计算每4位二进制数中1的个数
	i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
	//计算每8位二进制数中1的个数
	i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F);
	//将每8位当作一个int8的整数,然后相加求和
	i = (i * 0x01010101) >> 24);
	return i;
}

11.5.5 bitop命令208
11.5.6 bitfield命令209
11.6 本章小结212

第12章 散列表相关命令的实现213

为了与Redis中的key-value散列做区分,我们称value的散列结构的键值对为field-value(域值对)。
12.1 简介213
12.1.1 底层存储213
12.1.2 底层存储转换215
12.1.3 接口说明215
12.2 设置命令216
12.3 读取命令217
12.3.1 hexists命令218
12.3.2 hget/hmget命令218
12.3.3 hkeys/hvals/hgetall命令219
12.3.4 hlen命令220
12.3.5 hscan命令220
12.4 删除命令221
12.5 自增命令222
12.6 本章小结224

第13章 列表相关命令的实现225

13.1 相关命令介绍225
13.1.1 命令列表225
13.1.2 栈和队列命令列表226
13.2 push/pop相关命令228
13.2.1 push类命令的实现228
13.2.2 pop类命令的实现229
13.2.3 阻塞push/pop类命令的实现230
13.3 获取列表数据234
13.3.1 获取单个元素234
13.3.2 获取多个元素235
13.3.3 获取列表长度236
13.4 操作列表236
13.4.1 设置元素237
13.4.2 插入元素237
13.4.3 删除元素238
13.4.4 裁剪列表239
13.5 本章小结240

第14章 集合相关命令的实现241

14.1 相关命令介绍241

14.2 集合运算254

集合运算是Redis集合重要的功能,Redis实现了集合求交集、求差 集、求并集三种基本运算。Redis通过优秀的设计,保证了集合运算的效率。

14.2.1 交集254

集合求交集的基本逻辑是:先将集合按基数大小排序,以一个集合(基数最小)为标准,遍历该集合中的所有元素,依次判断该元素是否在其余所有集合中:如果不在任一集合,舍弃该元素,否则加入结果集。
14.2.2 并集258
14.2.3 差集260
14.3 本章小结263

第15章 有序集合相关命令的实现264

15.1 相关命令介绍264
15.2 基本操作272
15.2.1 添加成员272
15.2.2 删除成员275
15.2.3 基数统计276
15.2.4 数量计算277
15.2.5 计数器279
15.2.6 获取排名279
15.2.7 获取分值279
15.2.8 遍历280
15.3 批量操作280
15.3.1 范围查找280
15.3.2 范围删除283
15.4 集合运算284
15.5 本章小结284

第16章 GEO相关命令285

Redis提供了一些命令来帮助我们有效地处理位置信息,比如计算两点间的距离,这类命令统称为GEO相关的命令。
geohash算法在2008年公开,该算法可以把二维的经纬度信息降维到一维,并通过Base32编码将其转换为字符串。Ardb的作者提供了geohash-int的实现。R

16.1 基础知识285

Z阶曲线可以把二维空间转换为一个连续的曲线

  1. 前缀相同的位数越多,两个位置越相邻;
  2. 已知一个数字代表的区域可以方便地计算它们相邻的区域;
  3. 支持用任意的精度查找指定范围内的目标。

2、geohash算法
Gustavo Niemeyer在2008年2月上线了geohash.org网站,该网站可以把(经度,纬度)坐标转换成URL,方便大家在邮件、网站、论坛等分享地址信息。

16.2 命令实现288
16.2.1 使用geoadd添加坐标288
16.2.2 计算坐标的geohash291
16.2.3 使用geopos查询位置经纬度292
16.2.4 使用geodist计算两点距离295
16.2.5 使用georadius/georadius-bymembe查询范围内元素295
16.3 本章小结297

第17章 HyperLogLog相关命令的实现298

独立IP数、搜索记录数等需要去重和计数的问题该如何解决呢?我们把这类求集合中不重复元素个数的问题称为基数计数。

17.1 基本原理298

17.1.1 算法演进299

前计算较大数据的基数计数方法有下面两类:基于bitmap的基数计数;基于概率的基数计数。
(1)基于bitmap的基数计数
在bitmap中,bit是数据的最小存储单位,且bitmap自身具有去重的特点,bitmap中1的数量就是集合的基数值,合并计算复杂度也很低。但bitmap的长度与集合中元素个数无关,而是与基数上限有关,并且在统计多个这样的数据时(如每天的独立访客)会成倍增长,也不适用于大数据场景。
(2)基于概率的基数计数
目前常见的有线性计数算法、对数计数算法、超对数计数算法及自适应计数算法等。

17.1.2 线性计数算法299

初始化长度为m(基数)的bitmap,每个bit为一个组,集合中元素经散列计算后,将在bitmap对应的bit上设置1,当集合所有元素散列计算并设置完成后,bitmap上0的个数为u,可以通过0的个数估算出元素个数:$n=m*log(m/u)$
17.1.3 对数计数算法300
17.1.4 自适应计数算法302
17.1.5 超对数计数算法302
17.2 HLL Redis实现302
17.2.1 HLL头对象303
17.2.2 稀疏编码304
17.2.3 密集编码306
17.2.4 内部编码308
17.2.5 编码转换309
17.3 命令实现310
17.3.1 添加基数310
17.3.2 近似基数311
17.3.3 合并基数313
17.4 本章小结314

第18章 数据流相关命令的实现315

18.1 相关命令介绍315
18.2 基本操作命令原理分析323
18.2.1 添加消息323
18.2.2 删除消息325
18.2.3 范围查找326
18.2.4 获取队列信息327
18.2.5 长度统计327
18.2.6 剪切消息328
18.3 分组命令原理分析328
18.3.1 分组管理328
18.3.2 消费消息330
18.3.3 响应消息331
18.3.4 获取未响应消息列表331
18.3.5 修改指定未响应消息归属331
18.4 本章小结332

第19章 其他命令333

19.1 事务333
19.1.1 事务简介333
19.1.2 事务命令实现334

19.2 发布-订阅命令实现339

Redis的发布-订阅功能解耦了生产者和消费者,生产者可以向指定的channel发送消息而无须关心是否有消费者以及消费者是谁,而消费者订阅指定的channel之后可以接收发送给该channel的消息,也无须关心由谁发送。
1.发布命令
格式

publish channel message

返回值: 整型数,收到消息的客户端个数。
publish执行流程如下。
1)从pubsub_channels字典中以推送到的channel为key,取出所有订阅了该channel的客户端,依次向每个客户端返回推送的数据。
2)依次遍历pubsub_patterns,将链表中每个节点的模式字段pattern和推送的channel进行比较,如果能够匹配,则向节点中订阅了该pattern的客户端返回推送的数据。

2.订阅命令
格式

subscribe channel [channel ...]

返回值: 数组,第一个元素固定为subscribe,第二个元素为订阅的channel,第三个元素为该客户端总共订阅的channel个数(包括模式订阅)。

19.3 Lua脚本345
19.3.1 初始化Lua环境345
19.3.2 在Lua中调用Redis命令347
19.3.3 Redis和Lua数据类型转换349
19.3.4 命令实现351
19.4 本章小结356

第20章 持久化357

Redis有两种持久化方式:一种为RDB方式,RDB保存某一个时间点之前的数据;另一种为AOF方式,AOF保存的是Redis服务器端执行的每一条命令。

20.1 RDB358

20.1.1 RDB执行流程358

RDB快照有两种触发方式,其一为通过配置参数,

save 60 1000

则在60秒内如果有1000个key发生变化,就会触发一次RDB快照的执行。

20.1.2 RDB文件结构359

20.2 AOF367

AOF是Redis的另外一种持久化方式。简单来说,AOF就是将Redis服务端执行过的每一条命令都保存到一个文件,这样当Redis重启时只要按顺序回放这些命令就会恢复到原始状态。
我们还是从RDB和AOF的实现方式考虑:RDB保存的是一个时间点的快照,那么如果Redis出现了故障,丢失的就是从最后一次RDB执行的时间点到故障发生的时间间隔之内产生的数据。如果Redis数据量很大,QPS很高,那么执行一次RDB需要的时间会相应增加,发生故障时丢失的数据也会增多。
而AOF保存的是一条条命令,理论上可以做到发生故障时只丢失一条命令。但由于操作系统中执行写文件操作代价很大,Redis提供了配置参数,通过对安全性和性能的折中,我们可以设置不同的策略。

既然AOF数据安全性更高,是否可以只使用AOF呢?为什么Redis推荐RDB和AOF同时开启呢?
如果Redis有大量的修改操作,RDB中一个数据的最终态可能会需要大量的命令才能达到,这会造成AOF文件过大并且加载时速度过慢

但假设线上同时配置了RDB和AOF,那么会带来如下的两难选择:重启时如果优先加载RDB,加载速度更快,但是数据不是很全;如果优先加载AOF,加载速度会变慢,但是数据会比RDB中的要完整。

20.2.1 AOF执行流程368

2.AOF文件写入
AOF持久化最终需要将缓冲区中的内容写入一个文件,写文件通过操作系统提供的write函数执行。但是write之后数据只是保存在kernel的缓冲区中,真正写入磁盘还需要调用fsync函数。fsync是一个阻塞并且缓慢的操作,所以Redis通过appendfsync配置控制执行fsync的频次。具体有如下3种模式。

  • no:不执行fsync,由操作系统负责数据的刷盘。数据安全性最低但Redis性能最高。
  • always:每执行一次写入就会执行一次fsync。数据安全性最高但会导致Redis性能降低。
  • everysec:每1秒执行一次fsync操作。属于折中方案,在数据安全性和性能之间达到一个平衡。

20.2.2 AOF重写369

2.混合持久化
加载时,首先会识别AOF文件是否以REDIS字符串开头,如果是,就按RDB格式加载,加载完RDB后继续按AOF格式加载剩余部分。

20.3 RDB与AOF相关配置指令372
20.4 本章小结374

第21章 主从复制375

Redis支持主从复制功能,用户可以通过执行slaveof命令或者在配置文件中设置slaveof选项来开启复制功能。

21.1 主从复制功能实现375

为什么需要主从复制功能呢?
1)读写分离,单台服务器能支撑的QPS是有上限的,我们可以部署一台主服务器、多台从服务器,主服务器只处理写请求,从服务器通过复制功能同步主服务器数据,只处理读请求,以此提升Redis服务能力;另外我们还可以通过复制功能来让主服务器免于执行持久化操作:只要关闭主服务器的持久化功能,然后由从服务器去执行持久化操作即可。
2)数据容灾,任何服务器都有宕机的可能,

对于本例来说slaveof命令的主要流程如下。
1)从服务器127.0.0.1:6379向主服务器127.0.0.1:7000发送sync命令,请求同步数据。
2)主服务器127.0.0.1:7000接收到sync命令请求,开始执行bgsave命令持久化数据到RDB文件,并且在持久化数据期间会将所有新执行的写入命令都保存到一个缓冲区。
3)当持久化数据执行完毕后,主服务器127.0.0.1:7000将该RDB文件发送给从服务器127.0.0.1:6379,从服务器接收该RDB文件,并将文件中的数据加载到内存。
4)主服务器127.0.0.1:7000将缓冲区中的命令请求发送给从服务器127.0.0.1:6379。
5)每当主服务器127.0.0.1:7000接收到写命令请求时,都会将该命令请求按照Redis协议格式发送给从服务器127.0.0.1:6379,从服务器接收并处理主服务器发送过来的命令请求。
但是注意到步骤2中存在持久化操作(bgsave),而这是一个非常耗费资源的操作。
21.2 主从复制源码基础378

21.3 slaver源码分析382

这里可能存在两个疑问:
1)replica-ofCommand函数只是记录主服务器IP地址与端口,什么时候连接主服务器呢?
2)变量repl_state有什么作用?

我们先来回答第一个问题。replicaofCommand函数实现并没有向主服务器发起连接请求,说明该操作应该是一个异步操作,那么很有可能是在时间事件中执行,搜索时间事件处理函数serverCron会发现,以一秒为周期执行主从复制相关操作:

21.4 master源码分析388
21.5 本章小结391

第22章 哨兵和集群392

22.1 哨兵392

哨兵通过与Master和Slave的通信,能够清楚每个Redis服务的健康状态。这样,当Master发生故障时,哨兵能够知晓Master的此种情况,然后通过对Slave健康状态、优先级、同步数据状态等的综合判断,选取其中一个Slave切换为Master,并且修改其他Slave指向新的Master地址。
22.1.1 哨兵简介393
22.1.2 代码流程394
22.1.3 主从切换396
22.1.4 常用命令399

通过本节介绍,我们回答一下之前提出的3个问题。
1)主从切换完成之后,客户端和其他哨兵如何知道现在提供服务的Redis Master是哪一个呢?
回答 :可以通过subscribe__sentinel__:hello频道,知道当前提供服务的Master的IP和Port。
2)执行切换的哨兵发生了故障,切换操作是否会由其他哨兵继续完成呢?
回答 :执行切换的哨兵发生故障后,剩余哨兵会重新选主,并且重新开始执行切换流程。
3)故障Master恢复之后,会继续作为Master提供服务还是会作为Slave提供服务?
回答 :Redis中主从切换完成之后,当故障Master恢复之后,会作为新Master的一个Slave来提供服务。

22.2 集群400

图中有3个Redis Master,每个Redis Master挂载一个Redis Slave,共6个Redis实例。集群用来提供横向扩展能力,即当数据量增多之后,通过增加服务节点就可以扩展服务能力。背后理论思想是将数据通过某种算法分布到不同的服务节点,这样当节点越多,单台节点所需提供服务的数据就越少。很显然,集群首先需要解决如下问题。
1)分槽(slot):即如何决定某条数据应该由哪个节点提供服务;
2)端如何向集群发起请求(客户端并不知道某个数据应该由哪个节点提供服务,并且如果扩容或者节点发生故障后,不应该影响客户端的访问)?
3)某个节点发生故障之后,该节点服务的数据该如何处理?
4)扩容,即向集群中添加新节点该如何操作?
5)同一条命令需要处理的key分布在不同的节点中(如Redis中集合取并集、交集的相关命令),如何操作?

22.2.1 集群简介401

Redis将键空间分为了16384个slot,然后通过如下算法:

HASH_SLOT = CRC16(key) mod 16384

22.2.2 代码流程402
22.2.3 主从切换404

22.2.4 副本漂移406

但假设若集群中有100个主服务,为了更高的可靠性,就需要增加100个实例。有什么方法既能提高可靠性,又可以做到不随集群规模线性增加从服务实例的数量呢?
Redis中提供了一种副本漂移的方法,
我们只给其中一个主C增加两个从服务。假设主A发生故障,主A的从A1会执行切换,切换完成之后从A1变为主A1,此时主A1会出现单点问题。当检测到该单点问题后,集群会主动从主C的从服务中漂移一个给有单点问题的主A1做从服务。

22.2.5 分片迁移407

有很多情况下需要进行分片的迁移,例如增加一个新节点之后需要把一些分片迁移到新节点,或者当删除一个节点之后,需要将该节点提供服务的分片迁移到其他节点,甚至有些时候需要根据负载重新配置分片的分布。

22.2.6 通信数据包类型409
22.3 本章小结415

posted @ 2024-12-22 21:33  liqinglucky  阅读(21)  评论(0编辑  收藏  举报