环境

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命令来实现的;


转换成



expire

pexpire

pexpireat

expireat


过期时间是如何保存在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

设置保存条件

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文件的写入和同步时机;

redis 表名 redis建表命令_数据库

上图中,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整体的流程图:

redis 表名 redis建表命令_redis 表名_02

说明:

AOF重写缓冲区的作用:为了记录子进程开始进行文件重写时,服务器进程的数据库中又进来的新数据。
② 当子进程重写完成后,会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,会将AOF重写缓冲区中的所有内存追加到新AOF文件的末尾,并重命名,然后原子地覆盖现有AOF文件,完成新旧文件的替换。
③ AOF使用的是伪客户端(没有网络连接的客户端):因为Redis命令只能在客户端上下文中执行,而载入AOF文件时包含了所有所需的命令,所以不需要网络连接。