《Redis设计与实现》笔记1 | Redis单机数据库的实现

2022-02-17 14:04:13 浏览数 (1)

参考《Redis设计与实现》

1.对象

1.1 类型

创建键值对时包含 键对象 和 值对象 ,键对象总是一个字符串对象,值对象则有五种常用对象:字符串对象、列表对象、哈希对象、集合对象、有序集合对象。查看对象类型 type [key]

1.2 内存回收

采用引用计数实现内存回收机制,计数次数会根据使用状态变化。

  • 创建新对象时,引用计数 1
  • 对象被新程序使用,引用计数 1
  • 对象不在被使用时,引用计数-1
  • 引用计数为0时,内存释放

查看引用次数 object refcount [key]

1.3 对象共享

在值相同的情况下,该对象的内存可以被多个键共享,每共享一次,引用计数次数 1。

目前:redis会在初始化服务器时创建1万个字符串对象,包含0-9999的所用整数值,优先使用这些共享值,而不是新创建对象

1.4 对象空转时长

空转时长表示某个键从现在起距离最后一次访用的间隔时长,命令 object idletime [key]

2.单机数据库

redis服务器默认会创建16个数据库(0-15),默认为0号数据库,切换命令为select [num]

2.1 RDB

RDB全称Redis DataBase,Redis是内存数据库,把数据存储在内存,但是不能持久,所以redis提供了RDB持久化功能,可以把内存中的数据库状态保存到磁盘中,避免数据意外丢失。

过程:redis内存数据库状态——>RDB文件(经过压缩的二进制文件),落盘——>还原为数据库状态

两个命令生成RDB文件:savebgsave

save命令会阻塞服务器进程,拒绝客户端发送的所有请求,直到RDB文件创建完毕

bgsave命令则是派生一个子进程负责创建RDB文件,服务器进程继续执行客户端的命令请求

在启动redis服务器后会自动载入RDB文件(载入期间服务器会处于阻塞状态)

代码语言:javascript复制
$redis-server
49917:M 23 Dec 2021 14:03:26.107 # Server initialized
49917:M 23 Dec 2021 14:03:26.108 * Loading RDB produced by version 6.2.4
49917:M 23 Dec 2021 14:03:26.108 * RDB age 81588 seconds
49917:M 23 Dec 2021 14:03:26.108 * RDB memory usage when created 0.98 Mb
49917:M 23 Dec 2021 14:03:26.108 * DB loaded from disk: 0.000 seconds
49917:M 23 Dec 2021 14:03:26.108 * Ready to accept connections

注意:

  • 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态,因为AOF文件的更新频率通常比RDB文件的更新频率高
  • 只有AOF处于关闭状态,才会使用RDB文件来还原数据库状态

自动间隔性保存

只要满足一条就会执行bgsave命令,通过子进程执行,不影响父进程

代码语言:javascript复制
$save 900 1
$save 300 10
$save 60 10000

只要满足其中一条就会执行:

  • 服务器900秒内对数据库至少进行了1次修改
  • 服务器300秒内对数据库至少进行了10次修改
  • 服务器60秒内对数据库至少进行了10000次修改

2.2 AOF

AOF全称Append Only File,RDB持久化是通过保存数据库中具体的键值对,而AOF持久化则是通过保存redis服务器所执行的写命令

AOF持久化的实现:

  • 命令追加 AOF打开状态下,服务器执行完写命令后,会将该命令追加到aof_buf缓冲区的末尾,
  • 文件写入和同步 redis服务器每次结束一个服务器进程之前,都会调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区中的内容写入和保存到 AOF文件里面。flushAppendOnlyFile函数行为由appendfsync选项的值决定,有三种行为:
    • always:将aof_buf缓冲区中所有内容写入并同步到AOF文件(效率最慢,安全性最高)
    • everysec:将aof_buf缓冲区中所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过1秒,再次对AOF文件进行同步,同步操作由一个线程专门负责(若没有为appendfsync主动设置值,则该行为默认情况)(效率够快,1秒同步一次,安全性较好)
    • no:将aof_buf缓冲区中所有内容写入到AOF文件,但不同步,由操作系统决定何时同步(效率最快,同步时间最长,安全性较低)

有两个同步函数,fsync和fdatasync可以强制让操作系统立即将缓冲区中的数据写入到硬盘里

AOF文件载入和数据还原:

创建一个不带网络连接的伪客户端(因为redis命令只能在客户端上下文中执行)——>从AOF文件中取出一条写命令——>用为客户端执行该命令——>循环2、3步,直到所有命令执行完毕

AOF文件重写:

命令:bgrewriteaof

随着服务器运行的时间增加,AOF文件中的内容会越来越多,体积越来越大,用AOF文件来进行数据还原所需的时间就要更多,为了解决这个问题,redis提供了重写(rewrite)功能,即通过创建一个新的AOF文件来代替现有的AOF文件,新旧文件保存的数据库状态相同,但新AOF文件没有那么多冗余命令,所以会比旧AOF文件小

为了尽量减少冗余命令,AOF文件重写不需要操作旧AOF文件里的命令,而是读取服务器当前数据库状态来实现。

如下,旧AOF文件需要保存3条命令:

代码语言:javascript复制
127.0.0.1:6379> sadd key1 a b
(integer) 2
127.0.0.1:6379> sadd key1 c d
(integer) 2
127.0.0.1:6379> sadd key1 e f
(integer) 2

重写的AOF文件则只需要保存一条命令,即用1条命令代替3条命令:

代码语言:javascript复制
127.0.0.1:6379> sadd key1 a b c d e f
(integer) 6

因为AOF重写是一种辅助性维护手段,所以AOF重写会放到子进程执行,不会阻塞主进程。但是这样也会造成一个问题,由于服务器主进程在处理命令请求时,子进程可以同时执行重写,这就可能导致服务器当前数据库状态和重写后的AOF文件保存的数据库状态不一致,即数据不一致问题。

为了解决数据不一致问题,redis设置了一个AOF重写缓冲区,当redis服务器执行完一个写命令后,会同时把写命令发送给AOF缓冲区和AOF重写缓冲区,当子进程完成AOF重写后,会通知父进程将AOF重写缓冲区中内容写入到新AOF文件中,此时新AOF文件中所保存的数据库状态和服务器当前的数据库状态一致,然后对新的AOF文件改名,覆盖旧的AOF文件,即解决了数据不一致的问题

2.3 事件

redis的事件包括文件事件和时间事件

redis的服务器和客户端通信是通过套接字,会产生相应的文件事件,文件事件是服务器对套接字操作的抽象,通过监听这些事件来完成一系列网络通信

时间事件就是redis服务器的一些操作需要在给定的时间点执行

文件事件

每当一个套接字准备好执行连接应答、写入、读取、关闭操作时,就会产生一个文件事件

多个事件可能会并发抛出,但总是被I/O多路复用程序放到队列里,每次同步有序的只传送一个套接字给文件事件分派器

下图是客户端和服务器的通信过程

时间事件

redis的时间事件分为两类:

定时事件:让一段程序在指定的时间之后再执行一次

周期性事件:让一段程序每隔指定的时间就执行一次

服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,就遍历整个链表,查找所有已到达的时间事件,并调用

相应的事件处理器

serverCron函数:

持续运行的redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期和稳定运行,这些定期操作由serverCron函数完成。每隔一段时间serverCron就会执行一次,直到服务器关闭,默认每秒运行10次,即间隔为100毫秒

事件的调度与执行

由于同时存在文件事件和时间事件,所以需要调度,决定何时处理何种文件。

首先,文件事件是随机出现的,时间事件是定时出现的,所以在定时事件的间隔处会执行文件事件,并等待下一次文件事件,直到时间事件的到来。

注意:每一次事件执行都是原子操作

2.4客户端

redis服务器和客户端是一对多的关系,客户端所包含的状态信息:

  • 套接字描述符:fd为-1表示伪客户端,fd为大于-1的整数表示普通客户端
  • 名字:默认情况连接到服务器的客户端是没有名字的,可以通过命令client setname设置名字,然后用命令client list查看所有客户端
  • 标志:记录了客户端的角色和客户端目前所处状态
  • 输入缓冲区:用于保存客户端发送的命令请求
  • 输出缓冲区:服务器执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里
  • 身份验证:客户端状态的authenticated为0表示客户端未通过身份验证,为1表示通过了身份验证
  • 时间:记录客户端创建时间、客户端和服务器最后一次互动时间、客户端空转时间,通过client list得出的age和idle可以看出创建时间(即连接了多少秒)和空转时间

创建客户端:通过链表的方式连接创建的客户端

关闭客户端:client kill ip:port

伪客户端:伪客户端在整个服务器运行生命周期中会一直存在,直到服务器关闭

2.5服务器

服务器启动到能处理客户端的命令请求所需要执行的步骤:

初始化服务器状态——>载入服务器配置——>初始化服务器数据结构——>还原数据库状态——>执行事件循环

3.常用命令

清空数据库键值对 flushdbflushall

  • flushdb只清空当前数据库内容,但不执行持久化操作,即RDB文件不会改变,而redis的数据是从RDB快照文件中读取加载到内存的,所以在flushdb之后,如果想恢复数据库,则可以直接kill掉redis-server进程,然后重新启动服务,这样redis重新读取RDB文件,数据恢复到flushdb操作之前的状态。
  • flushall 清空数据库并执行持久化操作,也就是RDB文件会发生改变,变成76个字节大小(初始状态下为76字节),所以执行flushAll之后数据库真正意义上清空了。

随机返回数据库某个键 randomkey

返回数据库键值对数量 dbsize

是否存在某个键 exists [key]

重新命名键 rename [oldkey] [newkey]

匹配某些键 keys

代码语言:javascript复制
redis> MSET firstname Jack lastname Stuntman age 35
"OK"
redis> KEYS *name*
1) "firstname"
2) "lastname"
redis> KEYS a??
1) "age"
redis> KEYS *
1) "age"
2) "firstname"
3) "lastname"

0 人点赞