Back to Blogs
Redis
设计与实现

Redis 设计与实现:单机数据库的实现

Soloman
2022-09-18

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文件会使用不同的编码方式来保存。

REDISdb_versiondatabasesEOFcheck_sum
5字节4字节,RDB文件版本号任意多个数据库及其数据1字节8字节,校验和
"REDIS""0006"database0, database1...EOFxxxxxxxx

3 AOF 持久化(Append Only File)

AOF持久化是通过保存Redis服务器中修改数据库的写命令到磁盘文件中,来记录数据库状态。AOF持久化实现步骤:

  1. 命令追加:将被执行的写命令追加到服务器的aof_buf缓冲区末尾
  2. 文件写入:调用aof.c/flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区内容写入和保存到AOF文件
  3. 文件同步:同步策略受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部分构成:

  1. 套接字
  2. I/O多路复用程序,其底层有多个I/O多路复用函数库,如select、epoll、evport、kqueue等,编译时会自动选择系统性能最高的I/O多路复用函数库来作为I/O多路复用程序的底层实现
  3. 文件事件分派器(dispatcher)
  4. 事件处理器,常见如:连接应答处理器、命令请求处理器、命令回复处理器

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 客户端分类:

  1. 普通客户端,通过网络连接到Redis服务器的普通客户端
  2. Lua脚本的伪客户端,服务器初始化时创建,负责执行Lua脚本中的Redis命令,只有服务器关闭时才会关闭
  3. 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

至此,服务器初始化工作完成,准备好接收客户端连接。

7 Redis 相关技术文章

1.Redis 设计与实现:数据结构与对象

2.Redis 数据库基础知识:5大数据类型对象

3.如何在Python中使用Redis