Redis 学习笔记

记录一下个人对于Redis的学习,仅供参考。

SDS 简单动态字符串

redis没有直接使用C语言的传统字符串表示,而是自己构建了(simple dynamic string)的抽象类型,并且广泛运用在redis的代码当中。

传统的c字符串只会在redis的代码中充当字符串字面量使用,也就是类似于打印日志时 log("xxxxx")这样使用。

sds.h/sdshdr 定义了sds的结构

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

image.png

并且还沿用了c字符串的以空字符串'\0'结尾,这样可以重用一部分c字符串函数库里面的函数。

C字符串 SDS
获取字符串长度的复杂度为O(n) 获取字符串长度的复杂度为O(1)
API是不安全的,可能造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然要执行N次内存重分配 修改字符串长度N次最多执行N次内存重分配
只能保存文本数据 可以保存文本或者二进制数据

对象的类型与编码

redis内置有5种对象:字符串,列表,哈希,集合,有序集合。而redis中也自己实现了许多的数据结构例如:SDS,双端链表,字典,跳表,压缩列表,整数集合,哈希表等等,这里不会讨论如何实现这些数据结构,但是redis是用这些实现的数据结构来实现它的5种内置对象的,每种对象都用到了至少一种我们刚才介绍的数据结构。

针对不同的场景,我们可以为对象设置多种不同的数据结构实现,可以优化对象在不同场景下的使用效率。

对象

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

编码

编码常量 数据结构
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 有序集合的对象
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳表,字典

对象与编码的关系

类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数实现的字符串对象
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr编码的动态字符串实现的字符串对象
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端列表实现的列表对象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象
REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳表和字典实现的有序集合对象

谨慎处理多数据库程序

到目前为止,Redis仍然没有可以返回客户端目标数据库的命令,虽然redis-cli客户端会在输入符旁边提示当前所使用的目标数据库,但在其他的redis-sdk中并没有继承,所以为了避免对数据库进行误操作,最好先执行下select命令。

RDB和AOF

rdb和aof 都是redis提供的用于持久化的功能。

RDB持久化保存数据库状态的方法是将数据编码后保存在RDB文件当中,而AOF则是记录执行的SET,SADD,RPUSH三个命令保存到AOF文件当中。

两种恢复手段的载入判断流程。

 st=>start: 服务器启动
 e=>end: 载入AOF文件
 e2=>end: 载入RDB文件
 op=>operation: 执行载入程序
 cond=>condition: 已开启AOF持久化功能?
 io=>inputoutput: 输入/输出
 st->op->cond
 cond(yes)->e
 cond(no)->e2

ps: 为何lute没有识别出流程图的语法呢?

image.png

SAVE 和 BGSAVE

这两个命令都用来生成RDB文件,她们主要的区别如下:

SAVE 命令会阻塞Redis 服务器进程,直到RDB文件创建完毕,在此期间,redis-server不能处理任何命令请求。

BGSAVE 命令会派生出一个子进程,由它来负责创建RDB文件,服务器进程继续进行命令请求。

伪代码:

def save():
   rdbSave()

def bg_save():
   pid = fork() //创建子进程

   if pid == 0
     rdbSave()
     signal_parent() //告诉父进程
   elif pid > 0 //父进程继续处理命令请求,并通过轮训等待子进程的信号
     handle_request_and_wait_signal()
   else:
     //处理出错情况
     handle_fork_error()

与生成rdb文件不同,rdb的载入工作是服务器启动时自动执行的,所以redis并没有专门用于载入rdb文件的命令。

由于BGSAVE命令的保存工作是由子进程执行的,所以在子进程创建RDB文件的过程中,Redis服务器仍然可以处理客户端的命令请求,但是在此期间服务器处理SAVE,BGSAVE,BGREWRITEAOF三个命令的方式会和平时有所不同。

  • 在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,此举是防止父进程和子进程同时执行rdbSave函数调用,防止产生竞争条件。
  • 其次客户端发送BGSAVE也会被拒绝,理由与拒绝SAVE命令一样
  • BGREWRITEAOF 和 BGSAVE两个命令不能同时执行
    • 如果BGSAVE命令正在执行,那么BGREWRITEAOF会被延迟到BGSAVE命令执行完成之后执行
    • 如果BGREWRITEAOF命令正在执行,那么BGSAVE会被服务器拒绝
      • 原因是这两个命令的实际工作都是子进程执行,所以没有冲突和竞争,但是这两个子进程同时执行大量的磁盘写入,会大大的降低性能。

AOF持久化的实现

AOF持久化功能的实现可以分为命令追加(append),文件写入,文件同步(sync) 三个步骤。

命令追加

当AOF功能正处在打开状态时,客户端发送一条写入命令,服务器执行完之后,会以协议格式将这条命令追加到aof_buf缓冲区末尾

struct redisServer {
   // ....
   // AOF 缓冲区
   sds aof_buf;
   // ....
}

这就是AOF持久化命令追加步骤的实现原理。

AOF文件的写入与同步

Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复。那么如果打开了AOF功能,则会将命令尾加到aof_buf缓冲区中,所以在事件结束前都会调用flushAppendOnlyFile函数来考虑是否要将缓冲区里的内容写入和保存到AOF文件当中。

伪代码:

def eventLoop():
   while True:
     //处理文件事件,接收命令请求以及发送命令回复
     processFileEvents();
     //处理时间事件
     processTimeEvents();
     //考虑是否将aof_buf中的内容写入AOF缓冲区
     flushAppendOnlyFile();

flushAppendOnlyFile这个函数的行为由服务器配置的appendfsync选项的值来决定

  • always
    • 将aof_buf缓冲区中的所有内容写入并同步到AOF文件
  • everysec
    • 将oaf_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过了1秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责的
  • no
    • 将aof_buf缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时同步由操作系统来决定。

image.png

AOF文件重写的实现

请看另一篇帖子 AOF 文件重写算法

AOF后台重写

因为redis是使用单线程来处理请求命令,为了不阻塞主进程,所以AOF重写的工作会起一个子进程来进行。

但这样做的同时会导致一个问题,如果子进程在进行重写的同时,主进程继续处理命令请求,而新的命令可能会对现在的数据库状态进行修改,从而使得重写前后的文件保存的数据库状态不一致。

image.png
为了解决这个问题,redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当执行完一个写命令之后,他会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,这样子进程开始后,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面,这样就能解决上面这个问题了。

在整个过程中只有重写完成后的信号处理函数会对主进程造成阻塞,其他时候都不会造成阻塞。

这就是AOF后台重写,也就是BGREWRITEAOF命令的实现原理。

事件

未完待续...