Gakkiyomi Gakkiyomi

是爆炸的烟火

目录
Redis学习笔记
/  

Redis学习笔记

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 文件体积膨胀的问题,Redis 提供了 AOF 文件重写功能,新生成一个 AOF 文件来替代现有的 AOF 文件,新旧两个文件所保存的数据库状态相同,但新文件不会包含任何冗余命令,所以新 AOF 文件的体积会比旧的文件小。

Redis 的重写 aof 算法非常的聪明。

直接读取 key 的值,获取最新的 key 当前的值,然后用一条命令就可以做为这个 key 的当前状态。

伪代码:

def aof_rewrite(new_aof_file_name):
    # 创建新的AOF文件
  	f = create_file(new_aof_file_name)

    # 遍历数据库
    for db in redisServer.db:
       # 忽略空数据库
       if db.is_empty(): continue

       # 显示指定数据库
       f.write_command("SELECT "+ db.id)

       for key in db:
       	# 忽略已过期的key
       	if key.is_expired(): continue
         # 根据key的类型对key进行重新
         switch(key.type):
             case String:
               rewrite_string(key) #根据key获取到所有的value 然后拼成写入命令即可
             case List:
               rewrite_list(key)
             case Hash:
               rewrite_hash(key)
             case Set:
               rewrite_set(key)
             case SortedSet:
               rewrite_sorted_set(key)
  				if key.have_expire_time()
             rewrite_expire_time(key)
     #写入完毕,关闭文件
     f.close()

**ps:**在实际中,重写程序在处理列表,哈希表,集合,有序集合这四种带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,那么重写程序将使用多条命令来记录键的值,而不是单单一条命令。在 Redis 2.9 版本中这个常量的值为 64。

AOF 后台重写

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

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

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

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

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


标题:Redis学习笔记
作者:Gakkiyomi2019
地址:https://fangcong.ink/articles/2021/02/20/1613806285111.html