环境
window10
前言
《Redis 设计与实现》读书笔记;
服务器结构
Redis服务器默认会创建16个数据库,编号从0开始;
服务器结构如下:
struct redisServer{
...
// 一个数组,保存着服务器中的所有数据库
redisDb *db;
// 服务器数据库的数量
int dbnum;
...
}
数据库结构
typedef struct redisDb{
...
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
...
} redisDb;
设置键的生存时间或过期时间
Redis有四个命令:
① expire <key> <ttl>
ttl是键的生存时间,单位秒;
② pexpire <key> <ttl>
ttl是键的生存时间,单位毫秒;
③ expireat <key> <timestamp>
timestamp 键的过期时间,单位秒
④ pexpireat <key> <timestamp>
timestamp 键的过期时间,单位毫秒
虽然是四个键,但是底层实现上,其实都是pexpireat
命令来实现的;
过期时间是如何保存在Redis里面的呢?
typedef struct redisDb{
...
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
// 过期字典,保存着键的过期时间
dict *expires;
...
} redisDb;
也就是说,底层使用的是字典
结构,来记录键的过期时间;
查询键的剩余生存时间
// 返回生存时间 单位秒
TTL <key>
// 返回生存时间 单位毫秒
PTTL <key>
RDB
Redis是内存数据库服务器,假设断电了,数据不就丢失了?
针对上面的问题,Redis提供了持久化的功能;
其中RDB持久化就是其中之一;
将某个时间点上的数据库状态保存到一个RDB文件中。
数据库状态:服务器中非空数据库以及它们的键值对统称为数据库状态。
当执行:
redis> SAVE // 等待直到RDB文件创建完毕
OK
也可以后台执行:
redis> BGSAVE // 派生子进程,并由子进程创建RDB文件
Background saving started
BGSAVE是如何工作的
SAVE
命令执行时,会产生阻塞,直到RDB文件创建完毕;
那BGSAVE
是如何工作的呢?
在此之前,我们先了解一个概念cow = copy on write
;
这是一种简单的读写分离思想,适用于读多写少的并发场景。比如黑白名单,热点文章等等。
正常情况下我们说cow,指的是修改共享资源时,将共享资源copy一份,加锁后修改,再将原容器的引用指向新的容器。
对于java来说,是有线程的cow容器的,比如CopyOnWriteArrayList。
另外就是cow保证的是最终一致性而不是强一致。
copy on write 在Redis中使用细节
BGSAVE
命令底层就会用到copy on write
技术;
但是Redis并不会直立马接copy一份副本出来,因为那样会立马造成可用内存减少了一半。
Redis的copy on write 具体做法:
1、Redis创建子进程后,不会进行数据复制copy,主进程和子进程是共享数据的。主进程继续对外提供读写服务。
2、虽然不copy数据,但是kernel
(内核)会把主进程中所有内存页的权限都设为read-only,主进程和子进程访问数据的指针都指向同一内存地址。
3、主进程发生写操作时,因为权限已经设置为read-only
,所以会触发页异常中断(page-fault
),在中断处理中,需要被修改的内存页面会复制一份,复制出来的旧数据交给子进程使用;并且会把异常页权限修改为可写,这样,主进程就可以执行写操作,而子进程也可以继续它持久化操作;
在使用bgsave命令生成RDB文件的过程中,发生了写操作时,这会引起内核异常,此时就会触发copy on write
参考地址:
Redis 中 bgsave 方式持久化的细节问题
设置保存条件
struct redisServer{
...
// 一个数组,保存着服务器中的所有数据库
redisDb *db;
// 服务器数据库的数量
int dbnum;
// 记录了保存条件的数组
struct saveparam *saveparams;
// 修改计数器
long long dirty;
// 上一次执行保存的时间
time_t lastsave;
...
}
RDB压缩功能
RDB的压缩功能是可以通过配置文件来开启和关闭的;
开启压缩功能后,保存字符串对象时:
① 如果字符串长度小于等于20个字节,那么这个字符串会直接被原样保存。
② 如果字符串的长度大于20字节,那么这个字符串会被压缩之后再保存。
如果服务器关闭了RDB文件的压缩功能,那么RDB程序总以无压缩的方式保存字符串的值。
AOF
作用:和RDB
一样,都是用来持久化的,保存数据库状态的;
AOF
持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的;
如何记录呢?当然是写入到aof
的文件中,过程如下:
① 先写入到aof_buf缓冲区中;
② 根据服务器的配置,来觉得是aof文件的写入和同步时机;
上图中,aof文件的写入和同步时机,是可以通过服务器配置来设定的;
又因为Redis服务器进程就是一个事件循环(loop
):
def eventLoop():
while True:
# 处理文件事件,接收命令请求以及发送命令回复
# 处理命令请求时可能会有新内容被追加到aof_bug缓冲区中
processFileEvents()
# 处理时间事件
processTimeEvents()
# 考虑是否要将aof_buf中的内容写入和保存到aof文件里面
flushAppendOnlyFile()
当调用flushAppendOnlyFile()
方法时,其具体的行为由appendfsync
选项的值来决定,各个不同的值产生的行为如下:
appendfsync 选项的值 | flushAppendOnlyFile 函数的行为 |
---|---|
always | 将aof_buf缓冲区中的所有内容写入并同步到aof文件中 |
everysec | 将aof_buf缓冲区中的所有内容写入aof文件中,如果上次同步aof文件的时间距离现在超过1秒,那么再次对aof文件进行同步,并且这个同步操作是由一个线程专门负责执行的 |
no | 将aof_bug缓冲区中的所有内容写入到aof文件中,但并不对aof文件进行同步,何时同步由操作系统来决定 |
这里要特别说明,写入到文件中,并不等于写入到磁盘中。
为了提高文件的写入效率,现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面。等到缓冲区的空间被填满、或者超时后,才会真正地将缓冲区中数据写入到磁盘里面。— 这个就是同步(落盘)。
随着时间的流逝,AOF文件会越来越大,为了解决这个问题,Redis提供了AOF文件重写功能。
虽然是AOF文件的重写,但是并不需要对现有的AOF文件进行任何读取、分析或者写入操作。
这个功能是通过读取服务器当前的数据库状态来实现的。
AOF文件重写
Redis实际做法是:遍历数据库中的所有键,根据键的类型,调用相应的重写方法来记录到新AOF文件中。
redis> sadd aaa "yutao"
redis> sadd aaa "cat"
旧AOF文件会记录上面两条记录;
新AOF文件会记录一条:
redis> sadd aaa "yutao" "cat"
当你解决了一个坑时,又会掉入另一个坑
虽然AOF
重写解决了文件过大的问题,但是重写AOF
程序aof_rewrite
函数会进行大量的写入操作。
因此,AOF
文件重写,Redis
是创建子进程
来执行。(服务器进程为父进程)
创建子进程的好处:
① 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免使用锁的情况下,保证数据的安全性。
② 子进程进行重写期间,服务器进程(父进程)可以继续处理命令请求。
下面是AOF
整体的流程图:
说明:
① AOF重写缓冲区
的作用:为了记录
子进程开始进行文件重写时,服务器进程的数据库中又进来的新数据。
② 当子进程重写完成后,会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,会将AOF重写缓冲区
中的所有内存追加到新AOF文件的末尾,并重命名,然后原子地覆盖现有AOF文件
,完成新旧文件的替换。
③ AOF使用的是伪客户端
(没有网络连接的客户端):因为Redis命令只能在客户端上下文中执行,而载入AOF文件时包含了所有所需的命令,所以不需要网络连接。