Redis笔记2-单机数据库

本文深入探讨Redis的数据库管理,包括切换数据库、键空间操作、键的生存时间设置及删除策略。详细阐述了RDB和AOF两种持久化机制,包括RDB的保存条件、文件结构以及AOF的命令追加、文件同步和重写过程。同时,介绍了Redis的事件处理机制,包括文件事件和时间事件的调度与执行。
摘要由CSDN通过智能技术生成

数据库

介绍服务器保存数据库的方法,客户端切换数据库的方法,数据库保存键值对的方法,以及对数据库的增删改查

服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态redisServer结构的db数组中,db数组的每一项都是一个redisDb结构,代表一个数据库

struct redisServer{
  redisDb *db; //一个数组,保存所有数据库
  int dbnum;  //服务器数据库数量
}

初始化服务器时,根据redisServer的dbnum属性决定创建多少个数据库,默认为16

切换数据库

Redis客户端的目标数据库默认为0号,执行SELECT命令切换目标数据库
服务器内部,客户端状态redisClient结构的db属性记录客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:

typedef struct redisClient{
  redisDb *db; //记录客户端正在使用的数据库
}

数据库键空间

Redis是KV数据库服务器,服务器每个数据库都是一个redisDb结构表示,redisDb结构的字典dict保存数据库中所有键值对,这个字典称为键空间(key space)

typedef struct redisDb{
  dcit *dict; //保存数据库中所有键值对
}

键空间和用户所见的数据库是直接对应的

  • 键空间的键也就是数据库的键,每个键都是一个字符串对象
  • 键空间的值也就是数据库的值,可以是字符串、列表、哈希表、集合、有序集合对象
    数据库的键空间是一个字典,所以对数据库的增删改查都是通过对字典进行操作来实现

键空间操作

redis> SET string_key, object_value1 //添加键
redis> DEL string_key //删除键
redis> SET string_key, object_value2 //更新键
redis> GET string_key //对键取值

FLUSHDB:清空数据库
RANDOMKEY:随机返回键
DBSIZE:返回数据库键数量
LRANGE
EXISTS
RENAME
KEYS

(待补充)

键生存/过期时间

过期时间是一个UNIX时间戳,当键过期时间来临,服务器自动从数据库中删除这个键
TTL/PTTL命令接受一个带生存时间或过期时间的键,返回其剩余生存时间

设置过期时间

Redis提供了4个命令设置过期时间:

  • EXPIRE<key> <ttl>:将key的生存时间设为ttl秒。
  • PEXPIRE<key> <ttl>:将key的生存时间设为ttl毫秒。
  • EXPIREAT<key> <timestamp>:将key的过期时间设置为timestamp秒数时间戳。
  • PEXPIREAT<key> <timestamp>:将key的过期时间设置为timestamp毫秒数时间戳。
保存过期时间

redisDb中有一个expires的字典数据结构保存所有键的过期时间,也称为过期字典。

  • 过期字典的键是一个指针,指向键空间的某个键对象
  • 过期字典的值是一个long long类型的整数,保存了键所指向的数据库键的过期时间(毫秒精度的Unix时间戳)
typedef struct redisDb{
  dict *expires; //过期字典,保存键的过期时间
}
移除过期时间

PERSIST命令可以移除一个键的过期时间,在过期字段中查找给定键,并解除键和值(过期时间)在过期字典中的关联。

过期键删除策略

三种删除策略:

  • 定时删除:在设置键的过期时间同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
  • 惰性删除:放任键过期不管,但每次从键空间获取键时,检查该键是否过期,如果过期就删除
  • 定期删除:每隔一段时间对数据库检查一次,删除过期键。
定时删除

定时删除策略对内存友好,但对CPU不友好。

能保证过期键尽可能快删除,释放过期键占用的内存。
但删除操作本身占用CPU时间,在CPU时间紧张的情况下,删除当前任务无关的过期键,会影响服务器性能(响应时间和吞吐量)。

Redis定时器需要创建时间事件,时间事件底层由无序链表实现,查找复杂度为O(N),如果需要高效处理必然要创建大量的定时器,并不现实。

惰性删除

惰性删除策略对CPU最友好,对内存最不友好。

程序取出键才进行过期检查,可以保证删除过期键操作只会在非做不可的情况下进行。

过期键仍然保存在数据库中,只要不被删除,占用的内存就不会释放。可能永远不会被删除,造成内存泄漏。

定期删除

前两种删除策略有明显缺陷,定期删除是一种折中的方式,隔一段时间执行一次,并限制删除操作执行的时长和频率减少对CPU的占用。定期删除还能减少庞大的过期键对内存的占用。

如何确定时长和频率是难点,过长或过少,会退变为定时删除和惰性删除。因此服务器需合理设置删除操作执行时长和执行频率

RDB持久化

由于Redis是内存数据库,数据状态都存储于内存,如果不想办法将存储在内存中的数据库状态保存到磁盘里,那么一旦服务器进程退出,服务器中的数据库状态也会消失。

为解决这个问题,Redis提供了持久化的功能,可将内存中的数据库保存到磁盘,防止意外丢失。RDS持久化(默认持久化策略)就是将某一时间点上的状态保存到一个RDB文件里。RDB文件是经过压缩的二进制文件,可通过该文件还原成数据库状态。

RDB文件创建与载入

执行SAVE或BGSAVE创建新RDB文件,过期键不会保存到新创建的RDB
区别:

  • SAVE会阻塞Redis服务器进程,直到RDB文件创建完毕为止,阻塞期间,服务器不能处理任何命令请求。
  • BGSAVE会fork出一个子进程,由子进程负责创建RDB文件,父进程继续处理命令请求。当子进程完成之后,向父进程发送信号。

启动Redis服务器时,如果服务器开启了RDB功能,服务器对RDB文件载入有两种模式:

  • 服务器以主服务器模式运行, 程序对文件保存的键检查,未过期的载入到数据库,过期则忽略。
  • 服务器以从服务器模式运行,文件中保存的键无论是否过期都会被载入到数据库。但主从服务器在进行数据同步时,从服务器的数据库会被清空,所以过期键对从服务器不会造成影响。

另外,如果服务器开启AOF持久化功能,服务器会优先使用AOF文件还原数据库状态

SAVE执行时服务器状态

SAVE命令执行时,Redis服务器会被阻塞,客户端所有命令请求都会被拒绝
执行完SAVE,重新开始接受命令请求之后,客户端发送的命令才会被处理

BGSAVE执行时服务器状态

BGSAVE命令的保存工作是由子进程执行的,Redis仍然可以处理客户端命令
在BGSAVE执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF(AOF持久化命令)三个命令和平时有所不同:

  • 首先,BGSAVE执行期间,会拒绝SAVE命令。为了避免父进程和子进程同时执行rdbSAVE,防止产生竞争条件
  • 其次,BGSAVE执行期间, BGSAVE命令也会被拒绝。因为两个BGSAVE命令也会产生竞争
  • 最后BGSAVE和BGREWRITEAOF不能同时执行,BGREWRITEAOF会被延迟

自动间隔性保存

BGSAVE可在不阻塞服务器的情况下执行,因此Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令
可以通过save选项设置多个保存条件,只要一个被满足就会执行BGSAVE
默认保存条件为

save 900 1
save 300 10
save 60 10000

例如save 900 1指的是服务器在900秒内,对数据库进行了至少1次修改

设置保存条件

服务器会根据save选项所设置的条件,设置服务器状态redisServer结构的saveparams属性:

struct redisServer{
    //...
    struct saveparam *saveparams;  //记录了保存条件的数组
    //...
}

saveparams是一个数组,每个元素都是一个saveparam结构,每个saveparam结构都保存一个save的保存条件
saveparam结构如下:

struct saveparam{
    time_t seconds;
    int changes;
}

dirty计数器和lastsave

dirty计数器记录距离上一次成功执行SAVE或BGSAVE之后,服务器对数据库的修改(包括写入、删除、更新)次数
lastsave是一个UNIX时间戳,记录上一次成功执行SAVE或BGSAVE的时间

struct redisServer{
    long long dirty;
    time_t lastsave;
}

检查保存条件

周期函数serverCron每隔100毫秒执行一次,其中一项工作就是检查保存条件是否满足

def serverCron():
    #遍历所有保存条件
    for saveparam in server.saveparams:
        #计算距离上次执行保存操作的时间
        save_interval = unixtime_now() - server.lastsave
        #如果满足条件,则执行保存操作
        if server.dirty >= saveparam.changes and \
          save_interval > saveparam.seconds:
            BGSAVE()

程序遍历检查saveparams数组中的所有保存条件,只要有一个满足,就会执行BGSAVE命令

RDB文件结构

一个完整RDB文件包含:
在这里插入图片描述
RDB文件保存的是二进制数据,不是C字符串

  • REDIS:长5字节,保存”REDIS“这五个字符,可以快速检查文件是否为RDB
  • db_version:长4字节,值是一个字符串表示的整数,记录RDB文件版本号
  • databases:包含零个或任意个数据库,以及各数据库中键值对数据
    • 如果服务器数据库状态为空,该部分也为空
    • 如果服务器数据库状态不为空,该部分也不为空
  • EOF:常量,长度1字节,标志RDB文件正文内容的结束
  • check_sum:8字节无符号整数,保存校验和

database

每个非空数据库在RDB文件中都可表示为SELECTDB,db_number,key_value_pairs三部分

  • selectdb:1字节,标志位,标志着下一位存储的是数据库号码。
  • db_number:是一个数据库号码。
  • key_value_pairs:保存了数据库中所有键值对数据,如果有过期时间,则过期时间也会保存。

key_value_pairs

不带过期时间的键值对在RDB文件由TYPE,key,value组成,带过期时间则含有EXPIRETIME_MS,ms

EXPIRETIME_MS:标志位,长度为1字节,告知程序下一个读入的是以毫秒为单位的过期时间。
ms:是8字节长的带符号整数,记录UNIX时间戳,即过期时间。
type:记录value的类型,长度1字节,这个常量其实就是Redis对象类型和底层编码的组装

分析RDB

使用od命令分析Redis服务器产生的RDB文件

od -c dump.rdb

Redis本身也带有RDB文件检查工具redis-check-dump

AOF持久化

AOF:Append Only File
与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过Redis服务器执行的写命令来记录数据库状态

实现

AOF持久化实现可分为命令追加(append)、文件写入、文件同步(sync)三个步骤

命令追加

打开AOF持久化功能,服务器执行完写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾:

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

写入与同步

Redis服务器进程就是一个事件循环,循环中,文件事件负责接收客户端命令请求,以及向客户端发送命令回复;时间事件负责执行周期性函数
服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区中的内容写入和保存到AOF文件
flushAppendOnlyFile函数行为由服务器配置的appendfsync选项值决定,不同值对应的行为如图所示
在这里插入图片描述
默认为everysec

为提高写效率,操作系统一般将写入数据暂时保存在内存缓冲区,等缓冲区填满或超过指定时间后才会真正地将数据同步到磁盘里。操作系统提供了fsync和fdatasync两同步函数,可强制操作系统同步数据,保证数据安全性。

也就是说,每一次的事件循环,aof_buf中的指令都会被写入操作系统的缓冲区,根据appendfsync配置,当操作系统缓冲区满足一定条件后,就被真实地写入磁盘内。

AOF载入与数据还原

载入过程如图所示
在这里插入图片描述

  1. 创建一个没有网络连接的伪客户端。由于Redis命令只能在客户端上下文中执行,并且AOF文件在本地而不是网络。
  2. 解析AOF文件并取出一条写命令。
  3. 使用伪客户端执行被读出的写命令
  4. 持续执行2和3,直到所有写命令都已经执行完毕

对过期键的处理:
如果数据库中的某个键已经过期且没有被删除,AOF文件不会因为这个对过期键产生影响。当过期间被惰性删除或定期删除后,AOF文件追加一条DEL命令来显式删除。

AOF重写时,程序会对数据库的键检查,已过期的不会保存到AOF文件中。

AOF重写

因为AOF持久化会将所有的写命令都记录,所以会有冗余情况,比如频繁地创建删除键值对,或者对同一个键的值频繁更新,都会导致文件的内容越来越多。为了解决AOF文件体积膨胀问题,Redis提供AOF文件重写功能,让服务器创建一个新的AOF文件,替代现有的AOF文件,减少冗余命令。

实现

在新的AOF文件的重写过程中,不会读取旧AOF文件,而是通过读取数据库状态来实现的。首先从数据库中读取键现在的值,然后用一条命令记录键值对,代替之前记录的多条命令。

比如服务器对list键执行了一系列RPUSH和LPOP操作,使得list包含"F",“G”,Redis最后只用一条命令来代替保存在AOF文件中的一系列操作:

RPUSH list “F” “G”

在重写时会先检查键所包含的元素数量,因为多元素的键在命令转换时可能会导致客户端输入缓冲区溢出。因此读取配置中对应的常量,默认超过64个就用多条指令记录。

AOF后台重写

Redis服务器使用单个线程来处理命令请求,服务器调用aof_rewrite重写会进行大量写入操作,因此调用这个函数的线程会被长时间阻塞,重新AOF期间服务器无法处理客户端发来的命令请求
因此Redis将AOF重写放到子进程中执行,好处是:

  • 重写期间,父进程仍然可继续处理请求。
  • 子进程有自己的数据副本,而非子线程,可以避免一些线程安全性问题的出现。

子进程在执行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,导致当前数据库状态与重写后的AOF文件保存状态不一致。为解决这个问题,设置了AOF重写缓冲区。
在这里插入图片描述
当重写子进程创建后,Redis服务器执行完写命令就会将其写入AOF缓冲区和AOF重写缓冲区,子进程执行重写期间,服务器进程要执行3个工作:

  1. 执行客户端发来的命令。
  2. 将执行后的写命令追加到AOF缓冲区。
  3. 将执行后的写命令追加到AOF重写缓冲区。

当子进程完成重写后,会向父进程发送一个信号,父进程接收并调用信号处理函数,将重写缓冲区的所有内容写到新AOF文件中,原子地覆盖现有的AOF文件。因此整个AOF文件重写的过程中,只有信号处理函数执行时,才会阻塞,将性能损耗降到最低。

事件

Redis服务器是一个事件驱动程序,需要处理两类事件:

  • 文件事件
    Redis通过socket与客户端连接,文件事件就是服务器对socket操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件来完成一系列网络通信操作
  • 时间事件
    Redis服务器中的一些操作需要在给定时间点执行,时间事件是这类定时操作的抽象

文件事件

文件事件处理器的四个组成部分如下图所示
在这里插入图片描述

  • 套接字
    每当一个套接字准备好执行应答、写入、读取、关闭等操作,就会产生一个文件事件,多个文件事件可能会并发出现
  • I/O多路复用程序
    负责监听多个套接字,将产生事件的套接字通过队列有序、同步地向分派器传送,并且每次传送一个,当上一个套接字产生的事件处理完毕之后才传送下一个
  • 文件事件分派器
    接收I/O多路复用程序传来的套接字,根据事件类型调用相应的处理器
  • 事件处理器
    是处理函数,定义了某事件发生时,服务器应执行的动作

连接事件示例

一次完整的客户端与服务器连接事件示例如下
在这里插入图片描述
首先Redis的监听套接字的AE_READBLE事件处于监听状态下,对应处理器为连接应答处理器
若此时有客户端向服务器发起连接,产生该事件,触发处理器执行。处理器会对客户端连接请求进行应答,然后创建客户端套接字以及状态,并将客户端套接字的AE_READBLE事件与命令请求处理器相关联,使客户端可以向主服务器发送命令请求
假设客户端向主服务器发送一个命令请求,客户端套接字产生AE_READBLE事件,引发命令请求处理器执行,处理器读取客户端命令内容,传给相关程序执行
执行命令将产生相关命令回复,服务器会将客户端套接字的AE_WRITABLE事件与命令回复处理器相关联
客户端尝试读取命令回复时,客户端套接字产生AE_WRITABLE事件,触发命令回复处理器执行,处理器将命令回复写入到套接字
处理器写完后,服务器解除客户端套接字的事件与命令回复处理器的关联

时间事件

时间事件分类:

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

事件的分类取决于处理器的返回值:

  • 返回ae.h/AE_NOMORE为定时事件,到达一次后就删除
  • 返回非AE_NOMORE为周期性事件。当时间事件到达后,根据返回值更新when属性,让该时间在一段时间后再次到达

时间事件的三个主要属性:

  • id
    服务器为时间事件创建的全局唯一ID,从小到大递增。新事件ID比旧事件ID大
  • when
    毫秒精度的UNIX时间戳,记录时间事件的到达时间
  • timeProc
    时间事件处理器,即函数。时间事件到达时,服务器调用相应处理器来处理事件

时间事件实现

所有时间事件都是在一个无序链表中,当时间事件执行器运行时,遍历整个链表,查找所有已到达的时间事件,调用相应处理器。
新时间事件总是插入到链表表头,所以时间事件按ID逆序排序。

应用实例

Redis需要定期对自身的资源和状态进行检查和调整,这些定期操作由redis.c/serverCron函数负责执行,主要工作包括

  • 更新服务器的各类统计信息,如时间、内存占用、数据库占用情况
  • 清理数据库中过期键值对
  • 关闭和清理连接失效的客户端
  • 尝试进行AOF/RDB持久化操作
  • 主服务器对从服务器进行定期同步
  • 若处于集群模式,对集群进行定期同步和连接测试

事件的调度与执行

事件的调度与执行由aeProcessEvens函数负责,伪代码如下:

def aeProcessEvens():
  time_event = aeSearchNearestTimer() #获取到达时间离当前时间最接近的时间事件
  remaind_ms = time_event.when - unix_ts_now() #计算距离到达时间
  if remaind_ms < 0: #如果事件已到达,设定为0
    remaind_ms = 0
  timeval = create_timeval_with_ms(remaind_ms) #创建timeval结构
  aeApiPoll(timeval) #阻塞并等待文件事件产生
  processFileEvents() #先处理已产生的文件事件
  processTimeEvents() #再处理已到达的时间事件

Redis服务器的运行流程如图所示
在这里插入图片描述
事件的调度与执行规则

  • aeApiPoll函数(redis封装的多路复用函数)的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保aeApiPoll函数不会阻塞过长时间。
  • 因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。
  • 对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性。

客户端

服务器

(待补充)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值