Redis 设计与实现:单机数据库的实现
1 数据库
Redis服务器将所有数据库都保存在server.h/redisServer结构的db中,db数组中每个元素都是一个server.h/redisDb结构,其代表着一个数据库。Redis服务器初始化会根据dbnum创建默认的16个数据库。
/* src/server.h */
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;
struct redisServer {
/* General */
pid_t pid; /* Main process pid. */
pthread_t main_thread_id; /* Main thread id */
char *configfile; /* Absolute config file path, or NULL */
char *executable; /* Absolute executable file path. */
char **exec_argv; /* Executable argv vector (copy). */
int dynamic_hz; /* Change hz value depending on # of clients. */
int config_hz; /* Configured HZ value. May be different than
the actual 'hz' field value if dynamic-hz
is enabled. */
mode_t umask; /* The umask value of the process on startup */
int hz; /* serverCron() calls frequency in hertz */
int in_fork_child; /* indication that this is a fork child */
redisDb *db;
int dbnum; /* Total number of configured DBs */
};
默认为0号数据库,切换数据库可使用select x语句,其原理是改变server.h/client.db的指向,指向在server.h/redisServer结构db数组中的某个元素(其中每个元素代表一个数据库)。
/* src/server.h */
typedef struct client {
redisDb *db; /* Pointer to currently SELECTed DB. */
} client;
在server.h/redisDb结构中的字典dict,保存了该数据库的所有键值对,称为键空间(keyspace)。对数据库的键值对操作,实际上都是对键空间字典进行操作,如增删改查等。
1.1 键的生存时间或过期时间
所有命令最终都转换成pexpireat执行,ttl表示生存时间(秒/毫秒),timestamp表示过期的时间戳(秒/毫秒时间戳)。在server.h/redisDb结构中的字典expires保存了所有键的过期时间,称为过期字典。
expire key ttl
pexpire key ttl
expireat key timestamp
pexpireat key timestamp
# 删除键的过期时间
persist key
# 查询键的剩余生存时间(秒/毫秒)
ttl key
pttl key
1.2 过期键的删除
Redis结合惰性删除和定期删除,来对过期键进行管理。
- 定时删除:对内存友好,尽早删除过期键释放内存;但是对CPU时间不友好,会影响服务器的响应时间和吞吐量以及数据库性能。
- 惰性删除:对CPU时间非常友好,只在访问键时才会检查过期时间;对内存最不友好,过期键可能会一直占用着内存,有内存泄露的风险。
- 定期删除:综合了以上两种策略,对内存和CPU都相对友好;难点在于需要根据服务器性能,确定删除操作的执行时长和执行频率。
2 RDB 持久化
Redis将某时刻的所有非空数据库及其所有键值对数据,从内存保存到RDB磁盘文件中,从而达到持久化和备份目的。有两个命令可以生成RDB文件,其本质都是调用rdb.c/rdbSave函数。Redis服务器启动时会自动加载RDB文件,本质是调用rdb.c/rdbLoad函数,服务器在载入RDB文件期间会一直阻塞,直到完成。
# Redis服务器会阻塞
save
# 由子进程执行,Redis服务器不会阻塞
bgsave
2.1 自动间隔性保存
设置Redis配置文件的save选项,可以按条件自动间隔性的执行bgsave命令(本质是间隔性调用server.c/serverCron函数),保存数据到RDB文件。
2.2 RDB 文件结构
RDB文件是经过压缩的二进制文件,对于不同类型的键值对数据,RDB文件会使用不同的编码方式来保存。
| REDIS | db_version | databases | EOF | check_sum |
|---|---|---|---|---|
| 5字节 | 4字节,RDB文件版本号 | 任意多个数据库及其数据 | 1字节 | 8字节,校验和 |
| "REDIS" | "0006" | database0, database1... | EOF | xxxxxxxx |
3 AOF 持久化(Append Only File)
AOF持久化是通过保存Redis服务器中修改数据库的写命令到磁盘文件中,来记录数据库状态。AOF持久化实现步骤:
- 命令追加:将被执行的写命令追加到服务器的aof_buf缓冲区末尾
- 文件写入:调用
aof.c/flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区内容写入和保存到AOF文件 - 文件同步:同步策略受
default.conf/appendfsync配置选项影响,默认everysec,还有always和no可选。选择不同的default.conf/appendfsync配置项会影响AOF持久化的效率和安全性
3.1 AOF 文件重写
实现原理:从数据库中读取键现在的值,用一条命令去记录键值对,来代替之前记录这个键值对的多条命令,从而使记录的数据量大大减少。也说明了AOF文件重写并不需要对现有的AOF文件进行任何操作,而是直接读取服务器当前数据库状态。
3.2 AOF 后台重写
为了不阻塞服务器主进程,一般采用AOF后台重写,即创建一个子进程来进行AOF文件重写。在此期间,为了保证数据一致性,会创建一个AOF重写缓冲区,这里面记录了开始AOF重写后服务器新接收到的所有命令,当子进程完成新AOF文件重写后,主进程会将AOF重写缓冲区的所有命令追加到新AOF文件中,然后用新AOF文件覆盖旧AOF文件,完成AOF后台重写。这就是bgrewriteaof命令的实原理。
/* src/aof.c */
void bgrewriteaofCommand(client *c) {
if (server.child_type == CHILD_TYPE_AOF) {
addReplyError(c,"Background append only file rewriting already in progress");
} else if (hasActiveChildProcess() || server.in_exec) {
server.aof_rewrite_scheduled = 1;
/* When manually triggering AOFRW we reset the count
* so that it can be executed immediately. */
server.stat_aofrw_consecutive_failures = 0;
addReplyStatus(c,"Background append only file rewriting scheduled");
} else if (rewriteAppendOnlyFileBackground() == C_OK) {
addReplyStatus(c,"Background append only file rewriting started");
} else {
addReplyError(c,"Can't execute an AOF background rewriting. "
"Please check the server logs for more information.");
}
}
4 事件
Redis服务器是一个事件驱动程序,其处理的事件分文件事件和时间事件,文件事件是服务器对套接字操作的抽象,时间事件是服务器对定时操作的抽象。
4.1 文件事件
文件事件分为可读和可写事件,文件事件处理器由4部分构成:
- 套接字
- I/O多路复用程序,其底层有多个I/O多路复用函数库,如select、epoll、evport、kqueue等,编译时会自动选择系统性能最高的I/O多路复用函数库来作为I/O多路复用程序的底层实现
- 文件事件分派器(dispatcher)
- 事件处理器,常见如:连接应答处理器、命令请求处理器、命令回复处理器
4.2 时间事件
时间事件分为定时事件和周期性事件,定时事件就是一次性事件,执行后就删除;周期性事件是重复事件,每间隔一定时间就会执行。如Redis服务器需要定期对自身资源和状态进行检查更新,就是调用server.c/serverCron函数以周期性事件的方式运行。
5 客户端
每个连接到服务器的客户端,服务器都使用一个server.h/client结构来保存该客户端的状态信息,服务器使用clients链表连接多个客户端状态信息,新加入的客户端会被放到链表末尾。
/* src/server.h */
typedef struct client {
uint64_t id; /* Client incremental unique ID. */
uint64_t flags; /* Client flags: CLIENT_* macros. */
connection *conn;
int resp; /* RESP protocol version. Can be 2 or 3. */
redisDb *db; /* Pointer to currently SELECTed DB. */
robj *name; /* As set by CLIENT SETNAME. */
...
} client;
5.1 客户端分类:
- 普通客户端,通过网络连接到Redis服务器的普通客户端
- Lua脚本的伪客户端,服务器初始化时创建,负责执行Lua脚本中的Redis命令,只有服务器关闭时才会关闭
- AOF文件的伪客户端,服务器载入AOF文件时创建,用于执行AOF文件中的Redis命令,AOF载入完成后关闭
6 服务器
Redis服务器负责与多个客户端建立网络连接,处理各个客户端的命令请求,并维持服务器自身运转。
6.1 服务器启动初始化步骤
1. Redis服务器状态参数初始化
/* src/server.c */
void initServerConfig(void) {
int j;
char *default_bindaddr[CONFIG_DEFAULT_BINDADDR_COUNT] = CONFIG_DEFAULT_BINDADDR;
initConfigValues();
updateCachedTime(1);
getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
server.runid[CONFIG_RUN_ID_SIZE] = '\0';
changeReplicationId();
clearReplicationId2();
server.active_expire_enabled = 1;
server.skip_checksum_validation = 0;
server.loading = 0;
server.async_loading = 0;
server.loading_rdb_used_mem = 0;
server.aof_state = AOF_OFF;
server.aof_rewrite_base_size = 0;
server.aof_rewrite_scheduled = 0;
unsigned int lruclock = getLRUClock();
atomicSet(server.lruclock,lruclock);
resetServerSaveParams();
appendServerSaveParams(60*60,1); /* save after 1 hour and 1 change */
appendServerSaveParams(300,100); /* save after 5 minutes and 100 changes */
appendServerSaveParams(60,10000); /* save after 1 minute and 10000 changes */
/* Replication related */
server.masterhost = NULL;
server.masterport = 6379;
server.master = NULL;
server.cached_master = NULL;
server.master_initial_offset = -1;
...
}
2. 载入用户配置参数
服务器载入用户指定的配置项,更新对应的服务器状态参数,即由初始化参数更新为用户配置参数。
3. 初始化服务器数据结构
server.c/initServerConfig函数主要负责初始化一般状态属性,而server.c/initServer函数主要负责初始化数据结构(当然除此之外,还会进行一些非常重要的设置操作)。
/* src/server.c */
void initServer(void) {
int j;
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
makeThreadKillable();
/* Initialization after setting defaults from the config system. */
server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
server.hz = server.config_hz;
server.pid = getpid();
server.in_fork_child = CHILD_TYPE_NONE;
server.main_thread_id = pthread_self();
server.current_client = NULL;
server.errors = raxNew();
resetReplicationBuffer();
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
/* Create the Redis databases, and initialize other internal state. */
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType);
server.db[j].expires = dictCreate(&dbExpiresDictType);
server.db[j].expires_cursor = 0;
server.db[j].blocking_keys = dictCreate(&keylistDictType);
server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType);
server.db[j].watched_keys = dictCreate(&keylistDictType);
server.db[j].id = j;
server.db[j].avg_ttl = 0;
server.db[j].defrag_later = listCreate();
server.db[j].slots_to_keys = NULL; /* Set by clusterInit later on if necessary. */
listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
}
evictionPoolAlloc(); /* Initialize the LRU keys pool. */
server.pubsub_channels = dictCreate(&keylistDictType);
server.pubsub_patterns = dictCreate(&keylistDictType);
server.pubsubshard_channels = dictCreate(&keylistDictType);
/* Create the timer callback, this is our way to process many background
* operations incrementally, like clients timeout, eviction of unaccessed
* expired keys and so forth. */
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
/* Register a readable event for the pipe used to awake the event loop
* from module threads. */
if (aeCreateFileEvent(server.el, server.module_pipe[0], AE_READABLE,
modulePipeReadable,NULL) == AE_ERR) {
serverPanic(
"Error registering the readable event for the module pipe.");
}
/* Register before and after sleep handlers (note this needs to be done
* before loading persistence since it is used by processEventsWhileBlocked. */
aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep);
/* 32 bit instances are limited to 4GB of address space, so if there is
* no explicit limit in the user provided configuration we set a limit
* at 3 GB using maxmemory with 'noeviction' policy'. This avoids
* useless crashes of the Redis instance for out of memory. */
if (server.arch_bits == 32 && server.maxmemory == 0) {
serverLog(LL_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
}
scriptingInit(1);
functionsInit();
slowlogInit();
latencyMonitorInit();
...
}
当server.c/initServer函数执行完成后,服务器就会打印出大家熟悉的画面:
[8600] 08 Sep 11:37:41.960 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
[8600] 08 Sep 11:37:41.960 # Redis version=5.0.10, bits=64, commit=1c047b68, modified=0, pid=8600, just started
[8600] 08 Sep 11:37:41.961 # Configuration loaded
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.10 (1c047b68/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 8600
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
[8600] 08 Sep 11:37:41.969 # Server initialized
4. 还原数据库状态
经过以上步骤,服务器状态信息和数据结构初始化完成了,但还需要载入数据库状态信息。有两种情况:
- 服务器开启了AOF持久化功能,那就会载入AOF文件来初始化数据库
- 服务器没有开启AOF持久化功能,那就会载入RDB文件来初始化数据库
数据库状态还原之后,服务器会打印如下耗时信息:
[8600] 08 Sep 11:37:41.976 * DB loaded from disk: 0.006 seconds
5. 执行事件循环
服务器开启事件循环,并打印如下信息:
[8600] 08 Sep 11:37:41.976 * Ready to accept connections
至此,服务器初始化工作完成,准备好接收客户端连接。