第二部分 单机数据库的实现——《Redis设计与实现》

读《Redis设计与实现》黄键宏著,笔记,第二部分。

目录

第二部分 单机数据库的实现

2.1 数据库

Redis 服务器的数据库实现详细介绍:

  • 服务器报错数据库的方法
  • 客户端切换数据库的方法
  • 数据库保存键值对的方法
  • 数据库的添加、删除、查看、更新操作的实现方法

另外

  • 服务器保存键的过期时间的方法
  • 服务器自动删除过期键的方法
  • 数据库通知功能
服务器中得数据库
struct redisServer{
    // ...
    
    // 一个数组,保存着服务器中得所有数据库
    redisDb *db;
    
    // ...
    
    // 服务器的数据库数量 默认16,可以配置
    int dbnum;
    
    // ...
};
切换数据库

默认进入 0 号数据库,通过 select index来切换。

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

通过修改 *db 指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能—— 这就是 SELECT 命令的实现原理。

数据库键空间

Redis 是一个键值对(key-value pair)数据库服务器。

服务器中的每个数据库都由一个 redisDb 结构表示。

其中,redisDb 结构的 dict 字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)。

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 */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    int id;                     /* Database ID */
    PORT_LONGLONG avg_ttl;          /* Average TTL, just for stats */
} redisDb;

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

  • 键空间的键也就是数据库的键,每个键都是一个字符串对象。
  • 键空间的值也就是数据库的值,每个值也可以是字符串对象、列表对象、哈希表对象、集合对象、有序集合对象中的任一种。

因为数据库的键空间是一个字典,所以所有针对数据库的操作,比如添加一个键值对到数据库,或者从数据库种删除一个键值对,又或者在数据库种获取某个键值对等,实际上都是通过对键空间字典进行操作来实现的。

读写键空间时的维护操作

当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间指向指定的读写操作,还会有一些额外的维护操作:

  • 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间未命中(miss)次数,这两个值可以在 INFO state 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看。
  • 在读取一个键之后,服务器会更新键的 LRU(最后一次使用)时间,这个值可以用于计数键的闲置时间,使用 Object idletime key 命令可以查看键 key 的闲置时间。
  • 如果服务器在读取一个键时发现该键已经过期,那么服务器会删除这个过期键,然后才执行余下的其他操作。
  • 如果服务器使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
  • 服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作。
  • 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。
设置键的生存时间或过期时间

通过 EXPIRE 命令或者 PEXPIRE 命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为 0 的键。

通过 EXPIREAT 命令或 PEXPIREAT 命令,以秒或者毫秒精度给数据库中的某个键设置过期时间(expire time)。

过期时间是一个 UNIX 时间戳,当键的过期时间来临时,服务器就会自动从数据库中删除这个键。

TTL 命令和 PTTL 命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间。

过期时间

Redis 有四个不同的命令用于设置键的生存时间(键可以存在多久)或者过期时间(键什么时候会被删除):

  • EXPIRE <key> <ttl> 命令用于将键 key 的生存时间设置为 ttl 秒。
  • PEXPIRE <key> <ttl> 命令用于将键 key 的生存时间设置为 ttl 毫秒。
  • EXPIREAT <key> <timestamp> 命令用于将键 key 的过期时间设置为 timestamp 所指定的秒数时间戳。
  • PEXPIREAT <key> <timestamp> 命令用于将键 key 的过期时间设置为 timestamp 所指定的毫秒时间戳。

实际上 EXPIRE 、PEXPIRE 、EXPIREAT 都是使用 PEXPIREAT 来实现的。

EXPIRE => PEXPIRE

def EXPIRE(key,ttl_in_sec):
	// 将 TTL 从秒转换成毫秒 
	ttl_in_ms = sec_to_ms(ttl_in_sec)
    PEXPIRE(key,ttl_in_ms)

PEXPIRE => PEXPIREAT

def PEXPIRE(key,ttl_in_ms):
	// 获取以毫秒计数的当前 UNIX 时间戳
	now_ms = get_current_unix_temstamp_in_ms()
    // 当前时间加上 TTL,得出毫秒格式的键过期时间
    PEXPIREAT(key, now_ms + ttl_in_ms)

EXPIREAT => PEXPIREAT

def EXPIREAT(key,expire_time_in_sec):
	// 将过期时间从秒转换为毫秒
	expire_time_in_ms = sec_to_ms(expire_time_in_sec)
    PEXPIREAT(key,expire_time_in_ms)    
保存过期时间

redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,我们称为过期字典。

  • 过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键)。
  • 过期字典的值是一个 long 类型的整数,这个整数保存了键所指向的数据库的键的过期时间——一个毫秒精度的 UNIX 时间戳。

键空间的键和过期字典的键公用一个键对象。

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 */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    int id;                     /* Database ID */
    PORT_LONGLONG avg_ttl;          /* Average TTL, just for stats */
} redisDb;

PEXPIREAT 命令的伪代码定义:

def PEXPIREAT(key, expire_time_in_ms):
    # 如果给定的键不存在于键空间,那么不能设置过期时间
	if key not in redisDb.dict:
		return 0
    # 在过期字典中关联键和过期时间
    redisDb.expires[key] = expire_time_in_ms
    # 过期时间设置成功
    return 1
删除过期时间

PERSIST 命令可以删除一个键的过期时间,PEXPIREAT 命令的反操作。

PERSIST 命令的伪代码定义:

def PERSIST (key):
    # 如果给定的键不存在,或者键没有设置过期时间,那么直接返回
	if key not in redisDb.expires:
		return 0
    # 删除过期字典中给定的键
    redisDb.expires.remove(key)
    # 键的过期时间删除成功
    return 1
过期键的删除策略

如果一个键过期了,那么它什么时候会被删除呢?

三种不同的删除策略:

  • 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
  • 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
Redis 的过期键删除策略

Redis 服务器实际使用的是惰性删除定期删除

通过配合使用这两种删除策略,服务器可以很好地在合理使用 CPU 时间和避免浪费内存空间之间取得平衡。

惰性删除策略的实现

由 db.c/expireIfNeeded 函数实现。

(3.2版本)

int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;

    if (when < 0) return 0; /* No expire for this key */

    /* Don't expire anything while loading. It will be done later. */
    if (server.loading) return 0;

    /* If we are in the context of a Lua script, we claim that time is
     * blocked to when the Lua script started. This way a key can expire
     * only the first time it is accessed and not in the middle of the
     * script execution, making propagation to slaves / AOF consistent.
     * See issue #1525 on Github for more information. */
    now = server.lua_caller ? server.lua_time_start : mstime();

    /* If we are running in the context of a slave, return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller,
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
    if (server.masterhost != NULL) return now > when;

    /* Return when this key has not expired */
    if (now <= when) return 0;

    /* Delete the key */
    server.stat_expiredkeys++;
    propagateExpire(db,key);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    return dbDelete(db,key);
}

定期删除策略的实现

由 redis.c(server.c)/activeExpireCycle 函数实现的。每当周期性操作 serverCron 函数时被调用。

在规定时间内,分多次遍历服务器中的各个数据库,从数据库中的 expires 字典中随机检查一部分键的过期时间。

随着多次执行就可以将整个服务器的所有数据库都检查一般。

(3.2版本)

/* Try to expire a few timed out keys. The algorithm used is adaptive and
 * will use few CPU cycles if there are few expiring keys, otherwise
 * it will get more aggressive to avoid that too much memory is used by
 * keys that can be removed from the keyspace.
 *
 * No more than CRON_DBS_PER_CALL databases are tested at every
 * iteration.
 *
 * This kind of call is used when Redis detects that timelimit_exit is
 * true, so there is more work to do, and we do it more incrementally from
 * the beforeSleep() function of the event loop.
 *
 * Expire cycle type:
 *
 * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
 * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
 * microseconds, and is not repeated again before the same amount of time.
 *
 * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
 * executed, where the time limit is a percentage of the REDIS_HZ period
 * as specified by the REDIS_EXPIRELOOKUPS_TIME_PERC define. */
void activeExpireCycle(int type) {
    /* This function has some global state in order to continue the work
     * incrementally across calls. */
    static unsigned int current_db = 0; /* Last DB tested. */
    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    static PORT_LONGLONG last_fast_cycle = 0; /* When last fast cycle ran. */

    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL;
    PORT_LONGLONG start = ustime(), timelimit;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Don't start a fast cycle if the previous cycle did not exited
         * for time limt. Also don't repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        if (!timelimit_exit) return;
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        last_fast_cycle = start;
    }

    /* We usually should test CRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     *
     * 1) Don't test more DBs than we have.
     * 2) If last time we hit the time limit, we want to scan all DBs
     * in this iteration, as there is work to do in some DB and we don't want
     * expired keys to use memory for too much time. */
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     * per iteration. Since this function gets called with a frequency of
     * server.hz times per second, the following is the max amount of
     * microseconds we can spend in this function. */
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);

        /* Increment the DB now so we are sure if we run out of time
         * in the current DB we'll restart from the next. This allows to
         * distribute the time evenly across DBs. */
        current_db++;

        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
            PORT_ULONG num, slots;
            PORT_LONGLONG now, ttl_sum;
            int ttl_samples;

            /* If there is nothing to expire try next DB ASAP. */
            if ((num = (PORT_ULONG) dictSize(db->expires)) == 0) {              WIN_PORT_FIX /* cast (PORT_ULONG) */
                db->avg_ttl = 0;
                break;
            }
            slots = (PORT_ULONG) dictSlots(db->expires);                        WIN_PORT_FIX /* cast (PORT_ULONG) */
            now = mstime();

            /* When there are less than 1% filled slots getting random
             * keys is expensive, so stop here waiting for better times...
             * The dictionary will be resized asap. */
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;

            /* The main collection cycle. Sample random keys among keys
             * with an expire set, checking for expired ones. */
            expired = 0;
            ttl_sum = 0;
            ttl_samples = 0;

            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            while (num--) {
                dictEntry *de;
                PORT_LONGLONG ttl;

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl > 0) {
                    /* We want the average TTL of keys yet not expired. */
                    ttl_sum += ttl;
                    ttl_samples++;
                }
            }

            /* Update the average TTL stats for this database. */
            if (ttl_samples) {
                PORT_LONGLONG avg_ttl = ttl_sum/ttl_samples;

                /* Do a simple running average with a few samples.
                 * We just use the current estimate with a weight of 2%
                 * and the previous estimate with a weight of 98%. */
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
            }

            /* We can't block forever here even if there are many keys to
             * expire. So after a given amount of milliseconds return to the
             * caller waiting for the other active expire cycle. */
            iteration++;
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                PORT_LONGLONG elapsed = ustime()-start;

                latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
                if (elapsed > timelimit) timelimit_exit = 1;
            }
            if (timelimit_exit) return;
            /* We don't repeat the cycle if there are less than 25% of keys
             * found expired in the current DB. */
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}
AOF、RDB 和复制功能对过期键的处理

对过期键的:

  • RDB 持久化功能
  • AOF 持久化功能
  • 复制功能
RDB 持久化

生成 RDB 文件:

在执行 SAVE 命令或者 BGSAVE 命令创建一个新的 RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中。

载入 RDB 文件:

  • 服务器以主服务器模式运行,程序会对文件保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略。
  • 服务器以从服务器模式运行,文件中所有的键都会被载入到数据库中。(主从同步时,从服务器的数据库会被清空)
AOF 持久化

AOF 文件写入:

当服务器以 AOF 持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么 AOF 文件不会因为整个过期键而产生任何影响。

当过期键被惰性删除或者定期删除之后,程序会向 AOF 文件追加(append)一条 DEL 命令,来显式地记录该键已被删除。

AOF 文件重写:

和生成 RDB 文件时类似,在执行 AOF 重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的 AOF 文件中。

复制

当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:

  • 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除整个过期键。
  • 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
  • 从服务器只有在接到主服务器发来的 DEL 命令之后,才会删除过期键。

通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性,也正是因为这个原因,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器里的复制品也会继续存在。

数据库通知

Redis数据库的通知主要用来获取数据库中的键的变化以及数据库中命令的执行情况

要想使用redis数据库中的通知的功能则需要在redis.conf 配置文件中进行相应的配置

键的变化通知用官方的语句称为键空间通知(key-space notification)
命令的执行情况通知用官方的语句称为键事件通知(key-event notification)

配置文件redis.conf中的notify-keyspace-events选项决定了服务器发送通知的类型

以下列举一些常见的配置

notify-keyspace-events AKE 服务器发送所有的键空间和键事件
notify-keyspace-events AK 服务器发送所有的键空间
notify-keyspace-events AE 服务器发送所有的键事件
notify-keyspace-events K$ 服务器只发送所有的有关字符串键有关的键空间通知
notify-keyspace-events Elg 服务器只发送所有的有关列表键有关的基础命令通知

  • K Keyspace events, published with __keyspace@<db>__ prefix.
  • E Keyevent events, published with __keyevent@<db>__ prefix.
  • g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, …
  • $ String commands
  • l List commands
  • s Set commands
  • h Hash commands
  • z Sorted set commands
  • x Expired events (events generated every time a key expires)
  • e Evicted events (events generated when a key is evicted for maxmemory)
  • A Alias for g$lshzxe, so that the “AKE” string means all the events.

配置为notify-keyspace-events AKE下进行有关的测试

以下为有关键空间通知的相关示例

127.0.0.1:6379> SUBSCRIBE __keyspace@0__:msg
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__keyspace@0__:msg"
3) (integer) 1

我们再开启一个客户端执行相应的命令

127.0.0.1:6379> set msg kkkk
OK

则相应的服务器的输出依次如下

1) "message"
2) "__keyspace@0__:msg"
3) "set"
发送通知实现

代码由 notify.c/notifyKeyspaceEvent 函数实现。

看代码很清楚

(3.2版本)

/* The API provided to the rest of the Redis core is a simple function:
 *
 * notifyKeyspaceEvent(char *event, robj *key, int dbid);
 *
 * 'event' is a C string representing the event name.事件名称
 * 'key' is a Redis object representing the key name.产生事件的键
 * 'dbid' is the database ID where the key lives.  产生事件的数据库号码*/
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
    sds chan;
    robj *chanobj, *eventobj;
    int len = -1;
    char buf[24];

    /* If notifications for this class of events are off, return ASAP. */
    if (!(server.notify_keyspace_events & type)) return;

    eventobj = createStringObject(event,strlen(event));

    /* __keyspace@<db>__:<key> <event> notifications. */
    if (server.notify_keyspace_events & NOTIFY_KEYSPACE) {
        chan = sdsnewlen("__keyspace@",11);
        len = ll2string(buf,sizeof(buf),dbid);
        chan = sdscatlen(chan, buf, len);
        chan = sdscatlen(chan, "__:", 3);
        chan = sdscatsds(chan, key->ptr);
        chanobj = createObject(OBJ_STRING, chan);
        // 发送通知
        pubsubPublishMessage(chanobj, eventobj);
        decrRefCount(chanobj);
    }

    /* __keyevente@<db>__:<event> <key> notifications. */
    if (server.notify_keyspace_events & NOTIFY_KEYEVENT) {
        chan = sdsnewlen("__keyevent@",11);
        if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
        chan = sdscatlen(chan, buf, len);
        chan = sdscatlen(chan, "__:", 3);
        chan = sdscatsds(chan, eventobj->ptr);
        chanobj = createObject(OBJ_STRING, chan);
        pubsubPublishMessage(chanobj, key);
        decrRefCount(chanobj);
    }
    decrRefCount(eventobj);
}

DEL 命令成调用的事件函数:

void delCommand(client *c) {
    int deleted = 0, j;

    for (j = 1; j < c->argc; j++) {
        expireIfNeeded(c->db,c->argv[j]);
        if (dbDelete(c->db,c->argv[j])) {
            signalModifiedKey(c->db,c->argv[j]);
            // 发送通知
            notifyKeyspaceEvent(NOTIFY_GENERIC,
                "del",c->argv[j],c->db->id);
            server.dirty++;
            deleted++;
        }
    }
    addReplyLongLong(c,deleted);
}

2.2 RDB 持久化

因为 Redis 是内存数据库,它将自己的数据库状态(非空数据库以及它的键值对)存储在内存里面,所以如果不想办法将储存在内存中的数据库状态保存到磁盘里面,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见。

为了解决这个问题,Redis 提供了 RDB 持久化功能,这个功能可以将 Redis 在内存中的数据库状态保存到磁盘中,避免数据意外丢失。

RDB 持久化,既可以手动执行,也可以定期执行(配置)。

RDB 持久化会将数据库状态保存到一个 RDB 文件中,RDB 文件是一个经过压缩的二进制文件,通过该文件可以还原数据库状态。

RDB 文件的创建与载入

Redis 有两个命令生成 RDB 文件。

  • SAVE 命令,阻塞服务器,服务器不处理任何命令请求。
  • BGSAVE 命令,不阻塞服务器,派生出一个子进程创建 RDB 文件,父进程继续处理命令请求。

创建 RDB 文件的源码在 rdb.c/rdbSave 函数

服务器启动时自动加载 RDB 文件。当 AOF 持久化功能开启时,优先加载 AOF 文件。

加载 RDB 文件的源码在 rdb.c/rdbLoad 函数

BGSAVE 命令期间,会拒绝 BGSAVE 命令请求,BGREWRITEAOF 请求会在 BGSAVE 执行完后执行。但是,BGREWRITEAOF 命令期间会拒绝 BGSAVE 命令。

RDB 文件加载期间,阻塞,拒绝任何请求。

自动隔间性保存

因为 BGSAVE 命令可以在不阻塞服务器进程的情况下执行,所以 Redis 允许用户通过设置服务器配置的 save 选项,让服务器每隔一段事件自动执行一个 BGSAVE 命令。

默认配置如下:

save 900 1
save 300 10
save 60 10000

意思为满足三个条件中任意一个时,执行 BGSAVE 命令。

  • 服务器在 900 秒之内,对数据库进行了至少 1 次修改。
  • 服务器在 300 秒之内,对数据库进行了至少 10 次修改。
  • 服务器在 60 秒之内,对数据库进行了至少 10000 次修改。
设置保存条件

设置服务器状态 redisServer结构中的 saveparams 属性

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

saveparams 结构如下:

struct saveparam {
    // 秒数
    time_t seconds;
    // 修改数
    int changes;
};
dirty 计数器和 lastsave 属性
  • dirty 计数器记录距离上一次成功执行 SAVE 命令或者 BGSAVE 命令之后,服务器对数据库状态(服务器中的所有的数据库)进行了多少次修改(包括了写入、删除、更新等操作)。
  • lastsave 属性是一个 UNIX 时间戳,记录了服务器上次成功执行 SAVE 命令或者 BGSAVE 命令的时间。
struct redisServer{
    // ...
    
    PORT_LONGLONG dirty;            /* Changes to DB from the last save */
    time_t lastsave;                /* Unix time of last successful save */
    time_t lastbgsave_try;          /* Unix time of last attempted bgsave */
    time_t rdb_save_time_last;      /* Time used by last RDB save run. */
    time_t rdb_save_time_start;     /* Current RDB save start time. */    
    // ...
};

检查保存条件是否满足

Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查 save 选项所实则的保存条件是否满足,如果满足的话,就执行 BGSAVE 命令。

server.c/serverCron 部分代码:

/* If there is not a background saving/rewrite in progress check if
         * we have to save/rewrite now */
for (j = 0; j < server.saveparamslen; j++) {
    struct saveparam *sp = server.saveparams+j;

    /* Save if we reached the given amount of changes,
             * the given amount of seconds, and if the latest bgsave was
             * successful or if, in case of an error, at least
             * CONFIG_BGSAVE_RETRY_DELAY seconds already elapsed. */
    if (server.dirty >= sp->changes &&
        server.unixtime-server.lastsave > sp->seconds &&
        (server.unixtime-server.lastbgsave_try >
         CONFIG_BGSAVE_RETRY_DELAY ||
         server.lastbgsave_status == C_OK))
    {
        serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
                  sp->changes, (int)sp->seconds);
        rdbSaveBackground(server.rdb_filename);
        break;
    }
}
RDB 文件结构

一个完整的 RDB 文件所包含的各个部分

REDISdb_versiondatabasesEOFcheck_sum
Magic Number 文件识别4 字节版本号数据库1字节文件结束8字节无符号整数校验和

官方定义标准 https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format

2.3 AOF 持久化

配置选项 appendonly 默认是 no 改为 yes 启动 AOF 持久化

除了 RDB 持久化功能之外,Redis 还提供了 AOF (Append Only File)持久化功能。

  • RDB 持久化通过保存数据库中的键值对来记录数据库状态;

  • AOF 持久化通过保存 Redis 服务器所执行的写命令来记录数据库状态的。

被写入 AOF 文件的所有命令都是以 Redis 的命令请求协议格式保存的,Redis 的命令请求协议是纯文本格式。

在这个 AOF 文件里面,除了用于指定数据库的 SELECT 命令是服务器自动添加的之外,其他都是我们之前通过客户端发送的命令。

AOF 持久化的实现

AOF 持久化的实现可以分为:

  • 命令追加(append)
  • 文件写入
  • 文件同步(sync)
命令追加

当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾。

struct redisServer{
    // ....
    
    // AOF缓冲区
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
    // ....
};
AOF 文件的写入与同步

Redis 服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令回复,而时间时间则负责执行像 serverCron 函数这样需要定时运行的函数。

因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 aof.c/flushAppendOnlyFile 函数,考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面。

python 伪代码

def eventLoop():
    while Ture:
        # 处理文件事件,接收命令请求以及发送命令回复
        # 处理命令请求时可能会有新内容被追加到 aof_buf 缓存区
        processFileEvents()
        
        # 处理时间事件
        processTimeEvents()
        
        # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件
        flushAppendOnlyFile()

不同 appendfsync 选项值产生不同的持久化行为

appendfsync 选项的值行为和效率
always服务器在每个事件循环都要讲 aof_buf 缓冲区中的所有内容都写入到 AOF 文件,并且同步 AOF 文件,效率最慢,但是最安全,最多丢失一个事件循环命令数据。
everysec(默认)服务器在每个事件循环都要讲 aof_buf 缓冲区中的所有内容都写入到 AOF 文件,并且每隔一秒就要在子线程中对 AOF 文件进行一次同步。效率相对足够,安全性,最多丢失一秒钟命令数据。
no服务器在每个事件循环都要讲 aof_buf 缓冲区中的所有内容都写入到 AOF 文件,至于何时同步,由操作系统决定。效率最高,单次同步时长,丢失时失去到上次同步 AOF 文件之后的全部命令数据。
AOF 文件的载入与数据还原

因为 AOF 文件里面包含了重建数据库状态所需的所有写命令,服务器通过创建一个不带网络连接的伪客户端(fake client)来读取和执行写命令。完成数据的还原。

AOF 重写

为了解决 AOF 文件提交膨胀的问题,Redis 提供了 AOF 文件重写(rewrite)功能。

通过该功能,Redis 服务器可以创建一个新的 AOF 文件来替代现有的 AOF 文件。

新旧两个 AOF 文件锁保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令。

所以新 AOF 文件的体积通常会比旧 AOF 文件的体积要小得多。

AOF 文件重写得实现

虽然叫重写,但是 AOF 文件得重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,这个功能通过读取服务器当前的数据库状态来实现。

重写的 python 伪代码:

def aof_rewrite(new_aof_file_name):
    # 创建新 AOF 文件
    f = creat_file(new_aof_flie_name)
    # 遍历数据库
    for db in redisServer.db:
        # 忽略空数据库
        if db.is_empty():continue
            
        # 写入 SELECT 命令,指定数据库号码
        f.write_command("SELECT"+db.id)
        
        # 遍历数据库中所有的键
        for key in db:
            
            # 忽略已过期的键
            if key.is_expired():continue
                
            # 根据键的类型对键进行重写
            if key.type == String:
                rewrite_string(key)
            elif key.type == List:
                rewrite_list(key)
            elif key.type == Hash:
                rewrite_hash(key)
            elif key.type == Set:
                rewrite_set(key)
            elif key.type == SortedSet:
                rewrite_sorted_set(key)
                
            # 如果键带有过期时间,那么过期时间也要被重写
            if key.have_expire_time():
                rewrite_exprie_time(key)
                
    # 写入完毕,关闭文件
    f.close()
    
def rewrite_string(key):
    # 使用 GET 命令获取字符串键的值
    value = GET(key)
    # 使用 SET 命令重写字符串键
    f.write_command(SET,key,value)
    
def rewrite_list(key):
    # 使用 LRANGE 命令获取列表键包含的所有元素
    itme1,item2,...,itemN = LRANGE(key,0,-1)
    # 使用 RPUSh 命令重写列表键
    f.write_command(PRUSH,key,item1,item2,...,itemN)
    
def rewrite_hash(key):
    # 使用 HGETALL 命令获取哈希键包含的所有键值对
    field1,value1,field2,value2,...,fieldN,valueN = HGETALL(key)
    # 使用 HSET 命令重写哈希键
    f.write_command(HSET,key,field1,value1,field2,value2,...,fieldN,valueN)
    
def rewrite_set(key):
    # 使用 SMEMBERS 命令获取集合键包含的所有元素
    elem1,elem2,...elemN = SMEMBERS(key)
    # 使用 SADD 命令重写集合键
    f.write_command(SADD,key,elem1,elem2,...elemN)
    
def rewrite_sorted_set(key):
    # 使用 ZRANGE 命令获取有序集合键包含的所有元素
    member1,score1,member2,score2,...,memberN,scoreN = ZRANGE(key,0,-1,"WITHSCORES")
    # 使用 ZADD 命令重写有序集合键
    f.write_command(ZADD,key,member1,score1,member2,score2,...,memberN,scoreN)
    
def rewrite_expire_time(key):
    # 获取毫秒精度的键过期时间戳
    timestemp = get_expire_time_in_unixstamp(key)
    # 使用 PEXPIREAT 命令重写键的过期时间
    f.write_command(PEXPIREAT,key,timestemp)
    

为了避免在执行命令时造成客户端输入缓冲区溢出,

当元素的数据超过 server.h/AOF_REWRITE_ITEMS_PER_CMD (64),时使用多条命令

同理,列表键超过 64 时,也会用多条命令。

AOF 后台重写

在执行 BGREWRITEAOF 命令时,

Redis 服务器会维护一个 AOF 重写缓冲区,该缓存区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。

当子进程完成创建新 AOF 文件的工作之后,

服务器会将重写缓冲区中的所有内存追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件的所保存的数据库状态一致。

最后,服务器用新的AOF文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。

替换 AOF 文件时,使用原子地(atomic)覆盖。

子进程完成后,通知父进程,父进程调用信号处理函数会阻塞父进程。

2.4 事件

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

  • 文件事件(file event):Redis 服务器通过套接字与客户端(或者其他 Redis 服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
  • 时间事件(time event):Redis 服务器中的一些操作(比如 serverCron 函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
文件事件

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler):

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,也可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

文件事件处理器的构成

文件事件是对套接字操作的抽象:每次套接字变成可应答(acceptable)、可写入(writable)或者可读(readable)时,相应的文件事件就会产生。

四个组成部分:

  • 套接字
  • I/O 多路复用程序
  • 文件事件分派器(dispatcher)
  • 事件处理器

I/O 多路复用程序复杂监听多个套接字,并向文件事件分派器发送安歇产生了事件的套接字。

尽管多个文件事件可能会并发地出现,但 I/O 多路复用程序总是会将所有产生事件的套接字都放在一个队列里面,然后通过这个队列,以有序(sequential)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后,I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字。

文件事件分派器接收 I/O 多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。

事件处理器,就是一个个函数,他们定义了某个事件发生时,服务器应该执行的动作。

I/O 多路复用程序的实现

I/O 多路复用是什么意思?

查看日志了解 https://blog.csdn.net/sehanlingfeng/article/details/78920423

Redis 为每个 I/O 多路复用函数库实现了相同的 API ,所以底层可以互换。

Redis 根据在源码中用宏定义相应的规则,使得程序在编译时自动选择该系统中性能最高的 I/O 多路复用函数库。

宏定义规则如下:

/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif
事件的类型

I/O 多路复用程序可以监听多个套接字的 ae.h/AE_READABLE 事件和 ae.h/AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下:

  • 当套接字变的可读时(客户端对套接字执行 write 操作,或者执行 close 操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 操作),套接字产生 AE_READABLE 事件。
  • 当套接字变得可写时(客户端对套接字执行 read 操作),套接字产生 AE_WRITABLE 事件。

额可以同时监听,如果一个套接字又可读又可写的话,那么服务器将先读套接字,后写套接字。

API

ae.c 内:

函数说明
aeCreateFileEvent将给定套接字的给定事件假如到 I/O 多路复用程序的监听范围之内,并对事件和事件处理器进行关联
aeDeleteFileEvent让 I/O 多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联
aeGetFileEvents接受一个套接字描述符,返回该套接字正在被监听的事件类型
aeWait在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生,或者等待超时之后,函数返回
aeApiPoll在指定的时间内,阻塞并等待所有被 aeCreateFileEvent 函数设置为监听状态的套接字产生文件事件,当有至少一个事件产生,或者等待超时后,函数返回
aeProcessEvents文件事件分派器,它先调用 aeApiPoll 函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的事件处理器来处理这些事件
aeGetApiName返回使用的 I/O 多路复用程序的名称

事件结构:

/* File event structure */
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;
文件处理器

多个文件处理器:(networking.c)

  • 连接应答处理器 acceptTcpHandler 函数:负责用于对连接服务器的客户端进行应答。
  • 命令请求处理器 readQueryFromClient 函数:负责从套接字中读入客户端发送的命令请求内容。
  • 命令回复处理器 sendReplyToClient 函数:负责客户端返回命令的执行结果。
  • 复制处理器 : 当主服务器和从服务器进行负责操作。
时间事件

Redis 的时间事件分为两类:

  • 定时事件:让一段程序在指定的时间之后执行一次。
  • 周期性事件:然过一段程序每隔指定时间就执行一次。

一个时间任务主要的三个属性:

  • id:服务器为时间事件创建的全局唯一 ID (识别号)。递增,新的比旧的大。
  • when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间。
  • timeProc:时间事件处理器,一个函数。对应的处理器。

一个时间事件是定时事件,还是周期性事件取决于返回值:

  • 返回 ae.h/AE_NOMORE 是定时事件。
  • 返回 不是 ae.h/AE_NOMORE 是周期性事件。
实现

服务器将所有时间事件都放在一个无需链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

API

ae.c:

函数说明
aeCreateTimeEvent将一个新的时间事件添加到服务器
aeDeleteTimeEvent从服务器中删除该指定ID所对应的时间事件
aeSearchNearestTimer函数返回到达时间距离当前时间最接近的那个时间事件
processTimeEvents执行时间事件

事件结构:

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *prev;
    struct aeTimeEvent *next;
    int refcount; /* refcount to prevent timer events from being
  		   * freed in recursive time event calls. */
} aeTimeEvent;
时间事件应用实例:serverCron 函数

持续运行的 Redis 服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,就是由 serverCron 函数负责。主要功能:

  • 更新服务器的各类统计信息
  • 清理数据库中的过期键值对
  • 关闭和清理连接失效的客户端
  • 尝试进行 AOF 或者 RDB 持久化操作
  • 如果服务器是主服务器,那么对从服务器进行定期同步
  • 如果是hi处于集群模式,对集群进行定期同步和连接测试

默认 hz (10)次每秒。

源码说明:

/* This is our timer interrupt, called server.hz times per second.
 * Here is where we do a number of things that need to be done asynchronously.
 * For instance:
 *
 * - Active expired keys collection (it is also performed in a lazy way on
 *   lookup).
 * - Software watchdog.
 * - Update some statistic.
 * - Incremental rehashing of the DBs hash tables.
 * - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
 * - Clients timeout of different kinds.
 * - Replication reconnection.
 * - Many more...
 *
 * Everything directly called here will be called server.hz times per second,
 * so in order to throttle execution of things we want to do less frequently
 * a macro is used: run_with_period(milliseconds) { .... }
 */

int serverCron(struct aeEventLoop *eventLoop, PORT_LONGLONG id, void *clientData) 

hz 选项说明:

# Redis calls an internal function to perform many background tasks, like
# closing connections of clients in timeot, purging expired keys that are
# never requested, and so forth.
#
# Not all tasks are perforemd with the same frequency, but Redis checks for
# tasks to perform according to the specified "hz" value.
#
# By default "hz" is set to 10. Raising the value will use more CPU when
# Redis is idle, but at the same time will make Redis more responsive when
# there are many keys expiring at the same time, and timeouts may be
# handled with more precision.
#
# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10
事件的调度与执行

由 ae.c/aeProcessEvents 函数负责。

伪代码如下:

def aeProcessEvents():
    
    # 获取到达时间离当前时间最接近的时间事件
    time_event = aeSearchNearestTimer()
    
    # 计算最接近的时间事件距离到达还有多少毫秒
    remaind_ms = time_event.when - unix_ts_now()
    
    # 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设定为 0
    if remaind_ms < 0:
        remaind_ms = 0
        
    # 根据 remaind_ms 的值,创建 timeval 结构
    timeval = create_timeval_with_ms(remaind_ms)
    
    # 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 结构决定
    # 如果 remaind_ms 的值为 0,那么 aeApiPoll 调用之后马上返回,不阻塞
    aeApiPoll(timeval)
    
    # 处理所有已产生的文件事件,函数为了理解虚构,实际上是一大段代码块
    processFileEvents()
    
    # 处理所有已到达的时间事件
    processTimeEvents()

将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,这就构成了 Redis 服务器的主函数,伪代码如下:

def main():
    
    # 初始化服务器
    init_server()
    
    # 一直处理事件,直到服务器关闭为止
    while server_is_not_shutdown():
        aeProcessEvents()
        
    # 服务器关闭,执行清理操作
    clean_server()

2.5 客户端

Redis 服务器是典型的一对多服务器程序,通过使用由 I/O 多路复用技术实现的文件事件处理器,Redis 服务器使用单线程进程的方式来处理命令请求,并与多个客户端进行网络通信。

服务器为每个客户端建立了一个对应的结构 server.c/client (使用的redis 3.2 版本)

源码如下:

/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct client {
    uint64_t id;            /* Client incremental unique ID. */
    int fd;                 /* Client socket. */
    redisDb *db;            /* Pointer to currently SELECTed DB. */
    int dictid;             /* ID of the currently SELECTed DB. */
    robj *name;             /* As set by CLIENT SETNAME. */
    sds querybuf;           /* Buffer we use to accumulate client queries. */
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size. */
    int argc;               /* Num of arguments of current command. */
    robj **argv;            /* Arguments of current command. */
    struct redisCommand *cmd, *lastcmd;  /* Last command executed. */
    int reqtype;            /* Request protocol type: PROTO_REQ_* */
    int multibulklen;       /* Number of multi bulk arguments left to read. */
    PORT_LONG bulklen;           /* Length of bulk argument in multi bulk request. */
    list *reply;            /* List of reply objects to send to the client. */
    PORT_ULONGLONG reply_bytes; /* Tot bytes of objects in reply list. */
    size_t sentlen;         /* Amount of bytes already sent in the current
                               buffer or object being sent. */
    time_t ctime;           /* Client creation time. */
    time_t lastinteraction; /* Time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
    int flags;              /* Client flags: CLIENT_* macros. */
    int authenticated;      /* When requirepass is non-NULL. */
    int replstate;          /* Replication state if this is a slave. */
    int repl_put_online_on_ack; /* Install slave write handler on ACK. */
    int repldbfd;           /* Replication DB file descriptor. */
    off_t repldboff;        /* Replication DB file offset. */
    off_t repldbsize;       /* Replication DB file size. */
    sds replpreamble;       /* Replication DB preamble. */
    PORT_LONGLONG reploff;      /* Replication offset if this is our master. */
    PORT_LONGLONG repl_ack_off; /* Replication ack offset, if this is a slave. */
    PORT_LONGLONG repl_ack_time;/* Replication ack time, if this is a slave. */
    PORT_LONGLONG psync_initial_offset; /* FULLRESYNC reply offset other slaves
                                       copying this slave output buffer
                                       should use. */
    char replrunid[CONFIG_RUN_ID_SIZE+1]; /* Master run id if is a master. */
    int slave_listening_port; /* As configured with: SLAVECONF listening-port. */
    int slave_capa;         /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
    multiState mstate;      /* MULTI/EXEC state */
    int btype;              /* Type of blocking op if CLIENT_BLOCKED. */
    blockingState bpop;     /* blocking state */
    PORT_LONGLONG woff;         /* Last write global replication offset. */
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */
    WIN32_ONLY(char replFileCopy[_MAX_PATH];)

    /* Response buffer */
    int bufpos;
    char buf[PROTO_REPLY_CHUNK_BYTES];
} client;

Redis 服务器状态结构的 clients 属性是一个链表,保持了连接的客户端。

struct redisServer{
    // ...
    list *clients;              /* List of active clients */
    // ...
};

客户端结构主要包括如下属性:

  • 客户端的套接字描述符
  • 客户端的名字
  • 客户端的标志值(flag)
  • 指向客户端正在使用的数据库的指针,以及该数据库的号码
  • 客户端当前要执行的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针
  • 客户端的输入缓冲区和输出缓冲区
  • 客户端的复制状态信息,以及进行复制所需的数据结构
  • 客户端执行 BRPOP、BLPOP 等列表阻塞命令时使用的数据结构
  • 客户端的事务状态,以及执行 WATCH 命令时用到的数据结构
  • 客户端执行发布与订阅功能时用到的数据结构
  • 客户端的身份验证标志
  • 客户端的创建事件,客户端和服务器最后一次通信的时间,以及客户端的输出缓冲区大小超出软性限制(soft limit)的时间
客户端属性

客户端状态包含的属性可以分为两类:

  • 通用属性:与特定功能无关,一般都用到这些属性。
  • 特定功能相关属性:数据库相关 db属性和 dictid 属性,执行事务的 mstate 属性,等。
套接字描述符
int fd;                 /* Client socket. */

客户端状态的 fd 属性记录了客户端正在使用的套接字描述符。

  • 伪客户端(fake client):值为-1。用到的地方,载入 AOF 文件;执行 Lua 脚本。
  • 普通服务端:值大于-1。正常的套接字状态不会为-1。
名字
robj *name;             /* As set by CLIENT SETNAME. */

在默认情况下,一个连接到服务器的客户端是没有名字的。

client list 查看。

client setname 设置名字。

标识
int flags;              /* Client flags: CLIENT_* macros. */

记录了客户端的角色(role),以及客户端目前所处的状态。

由多种状态 | 运算后的。

状态如下:

/* Client flags */
#define CLIENT_SLAVE (1<<0)   /* This client is a slave server */
#define CLIENT_MASTER (1<<1)  /* This client is a master server */
#define CLIENT_MONITOR (1<<2) /* This client is a slave monitor, see MONITOR */
#define CLIENT_MULTI (1<<3)   /* This client is in a MULTI context */
#define CLIENT_BLOCKED (1<<4) /* The client is waiting in a blocking operation */
#define CLIENT_DIRTY_CAS (1<<5) /* Watched keys modified. EXEC will fail. */
#define CLIENT_CLOSE_AFTER_REPLY (1<<6) /* Close after writing entire reply. */
#define CLIENT_UNBLOCKED (1<<7) /* This client was unblocked and is stored in
                                  server.unblocked_clients */
#define CLIENT_LUA (1<<8) /* This is a non connected client used by Lua */
#define CLIENT_ASKING (1<<9)     /* Client issued the ASKING command */
#define CLIENT_CLOSE_ASAP (1<<10)/* Close this client ASAP */
#define CLIENT_UNIX_SOCKET (1<<11) /* Client connected via Unix domain socket */
#define CLIENT_DIRTY_EXEC (1<<12)  /* EXEC will fail for errors while queueing */
#define CLIENT_MASTER_FORCE_REPLY (1<<13)  /* Queue replies even if is master */
#define CLIENT_FORCE_AOF (1<<14)   /* Force AOF propagation of current cmd. */
#define CLIENT_FORCE_REPL (1<<15)  /* Force replication of current cmd. */
#define CLIENT_PRE_PSYNC (1<<16)   /* Instance don't understand PSYNC. */
#define CLIENT_READONLY (1<<17)    /* Cluster client is in read-only state. */
#define CLIENT_PUBSUB (1<<18)      /* Client is in Pub/Sub mode. */
#define CLIENT_PREVENT_AOF_PROP (1<<19)  /* Don't propagate to AOF. */
#define CLIENT_PREVENT_REPL_PROP (1<<20)  /* Don't propagate to slaves. */
#define CLIENT_PREVENT_PROP (CLIENT_PREVENT_AOF_PROP|CLIENT_PREVENT_REPL_PROP)
#define CLIENT_PENDING_WRITE (1<<21) /* Client has output to send but a write
                                        handler is yet not installed. */
#define CLIENT_REPLY_OFF (1<<22)   /* Don't send replies to client. */
#define CLIENT_REPLY_SKIP_NEXT (1<<23)  /* Set CLIENT_REPLY_SKIP for next cmd */
#define CLIENT_REPLY_SKIP (1<<24)  /* Don't send just this reply. */
#define CLIENT_LUA_DEBUG (1<<25)  /* Run EVAL in debug mode. */
#define CLIENT_LUA_DEBUG_SYNC (1<<26)  /* EVAL debugging without fork() */
输入缓冲区
 sds querybuf;           /* Buffer we use to accumulate client queries. */

客户端状态得输入缓冲区用于保存客户端发送的命令请求。

动态的缩小和扩大,不超过1G。

命令与命令参数
int argc;               /* Num of arguments of current command. */
robj **argv;            /* Arguments of current command. */

在服务器将客户端发送的命令请求保存到客户端状态的 querybuf 属性之后,服务器将对命令请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的 argv 属性和 argc 属性。

argv 属性是一个数组,数组中每项都是一个字符串对象,argv[0] 是执行的命令,后面是参数。

argc 属性是记录 argv 数组的长度。

命令的实现函数
struct redisCommand *cmd, *lastcmd;  /* Last command executed. */

命令指向的 redisCommand 结构,该结构保存了命令的实现函数、命令的标志、命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息。

输出缓冲区

执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面。

有两个缓冲区可用:

固定大小的,用于保存长度比较小的回复。

#define PROTO_REPLY_CHUNK_BYTES (16*1024) /* 16k output buffer */
/* Response buffer */
int bufpos;
char buf[PROTO_REPLY_CHUNK_BYTES];

可变大小的,用于保存长度比较大的回复。

list *reply;            /* List of reply objects to send to the client. */

当 buf 数组用完,或者回复太大不能放入 buf 数组时,就开始使用可变缓冲区。

身份验证
int authenticated;      /* When requirepass is non-NULL. */

记录客户端是否通过了身份验证。

值为 0 ,未通过,除了 AUTH 命令,拒绝所有命令。

值为 1 ,通过。

requirepass 选项设置 验证密码。

时间

和时间相关的属性。

time_t ctime;           /* Client creation time. 创建客户端的时间 */
time_t lastinteraction; /* Time of the last interaction, used for timeout 最后一次交互的时间,用于timeout*/
time_t obuf_soft_limit_reached_time;/* 属性记录了输出缓冲区第一次到达软性限制的时间*/
客户端的创建与关闭

服务器使用不同的方式来创建和关闭不同类的客户端。

创建普通的客户端

如果客户端是通过网络连接与服务器进行连接的普通客户端

那么在客户端使用 connect 函数连接到服务器时,

服务器就会调用连接事件处理器

为客户端创建相应的客户端状态

并将整个新的客户端状态添加到服务器状态结构 clients 链表的末尾

关闭普通客户端

有多种关闭普通客户端的原因:

  • 如果客户端进程退出或者被杀死,那么客户端和服务器之间的网络连接将被关闭,从而造成客户端被关闭
  • 如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭
  • 如果客户端成为了 CLIENT KILl 命令的目标,那么它也会被关闭
  • 如果用户为服务器设置了 timeout 配置选项,那么当客户端的空转事件超过 timeout 选项设置的值时,客户端将被关闭(从主时BLPOP、SUBSCRIBE、PSUBSCRIBE命令时不会 timeout)
  • 如果客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为 1 GB),那么这个客户端会被服务器关闭
  • 如果要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭

服务器使用两种模式限制客户端输出缓冲区的大小:

  • 硬性限制(hard limit):超过指定大小就关闭。
  • 软性限制(soft limit):超过指定大小,一段时间以后,就关闭。

通过配置 client-output-buffer-limit 选项设置。

# The limit can be set differently for the three different classes of clients:
#
# normal -> normal clients including MONITOR clients
# slave  -> slave clients
# pubsub -> clients subscribed to at least one pubsub channel or pattern
#
# The syntax of every client-output-buffer-limit directive is the following:
#
# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
  • 第一行表示普通客户端,不限制
  • 第二行表示从服务器客户端,硬性限制 256mb,软性限制 64mb,时长60秒
  • 第三行表示将执行发布与订阅功能的客户端,硬性限制 32mb,软性限制 8mb,时长60秒
Lua 脚本的伪客户端

在服务器结构的 lua_client 属性中:

struct redisServer{
    // ...
    client *lua_client;   /* The "fake client" to query Redis from Lua */
    // ...
};

在服务器生命周期中一直存在。

AOF 文件的伪客户端

服务器在载入 AOF 文件时,会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端,并在载入完成之后,关闭这个伪客户端。

2.6 服务器

Redis 服务器负载与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令锁产生的数据,并通过资源管理来维持服务器自身的原子。

命令请求的执行过程

一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。

以下面命令为例子:

redis> SET KEY VALUE
OK

操作如下:

  1. 客户端向服务器发送命令请求 SET KEY VALUE
  2. 服务器接收并处理客户端发来的命令请求 SET KEY VALUE,在数据库中进行设置操作,并产生命令回复 OK
  3. 服务器将命令回复 OK 发送给客户端。
  4. 客户端接收服务器返回的命令回复 OK ,并将这个回复打印给用户观看。

具体细节如下:

发送命令请求

Redis 服务器的命令请求来自 Redis 客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发给服务器。

读取命令请求

当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

  1. 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
  2. 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,分别保存到 argv 属性和 argc 属性。
  3. 调用命令执行器,执行客户端指定的命令。
命令执行器(1):查找命令实现

根据客户端的 argv[0]参数,在命令表(command table)中查找指定命令,并保存到客户端状态的 cmd 属性里面。

命令表是一个字典,键是命令名字,值是一个 redisCommand 结构。

redisCommand 结构如下:

struct redisCommand {
    char *name;
    redisCommandProc *proc;
    int arity;
    char *sflags; /* Flags as string representation, one char per flag. */
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    PORT_LONGLONG microseconds, calls;
};

主要属性说明:

属性名类型作用
namechar *命令的名字,如:SET
procredisCommandProc *函数指针,指向命令的指向函数
arityint命令参数的个数,用于检查命令的格式是否正确,负数-N ,个数大于等于N,名字本身也是一个参数
sflagschar *字符串形式的标识值,这个值记录了命令的属性,如,读命令,写命令
flagsint对sflags 标识进行分析得出的二进制标识,自动生成,服务器对命令标识检查使用flags属性,二进制标识方便通过位运算符。
callsPORT_LONGLONG服务器总共执行了多少次这个命令
microsecondsPORT_LONGLONG服务器执行这个命令所耗费的总时长

sflags 属性的标识:

标识意义带有这个标识的命令
w这是一个写入命令,可能会修改数据库SET、RPUSH、DEL等
r这是一个只读命令,不会修改数据库GET、STRLEN、EXISTS等
m这个命令可能会占用大量内存,执行之前需要先检查服务器的内存使用情况,如果内存紧缺的话就禁止执行这个命令SET、APPEND、RPUSH、LPUSH、SADD、SINTERSTORE等
a这是一个管理命令SAVE、BASAVE、SHUTDOWN等
p这是一个发布与订阅功能方面的命令PUBLISH、SUBSCRIBE等
s这个命令不可以在 Lua 脚本中使用BRPOP、BLPOP等
R这是一个随机命令,对于相同的数据集和相同的参数,命令返回的结果可能不同SPOP、SSCAN等
S当在 Lua 脚本中使用这个命令时,对这个命令的输出结果进行一次排序,使得命令的结果有序SINTER、SUNION等
l这个命令可以在服务器载入数据的过程中使用INFO、SHUTDOWN等
t这是一个允许从服务器在带有过期数据时使用的命令SLAVEOF、PING、INFO等
M这个命令在监视器(monitor)模式下不会自动被传播EXEC
命令执行器(2):执行预备操作

执行命令需要的:

  • 命令实现函数
  • 参数
  • 参数个数

都集齐了,在真正执行之前还需要做一些预备操作:

  • 检查客户端状态的 cmd 指针是否指向 NULL,是,返回一个错误
  • 根据客户端 cmd 指向的 redisCommand 结构中的 arity 属性,检查命令请求所给定的参数个数是否正确。
  • 检查客户端是否已经通过了身份验证
  • 如果服务器打开了 maxmemory 功能,那么在执行命令之前,先检查服务器的内存占用情况,并在需要时进行内存回收,从而使得接下来的命令可以顺利执行
  • 如果服务器上一次执行 BGSAVE 命令时出错,并且服务器打开了 stop-writes-on-bgsave-error 功能(默认yes),那么执行写命令时返回一个错误
  • 如果客户端当前正在用 SUBSCRIBE 命令订阅频道,或者正在用 PSUBSCRIBE 命令订阅模式,那么服务器就只会执行 SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE 四个命令,其他拒绝
  • 如果服务器正在进行数据载入,那么客户端发送的命令必须带有 1 标识才会被服务器执行
  • 如果服务器因为执行 Lua 脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的 SHUTDWON nosave 命令和 SCRIPT KILL 命令
  • 如果客户端正在执行事务,那么服务器只会执行客户端发送的 EXEC、DISCARD、MULTI、WATCH四个命令
  • 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器
命令执行器(3):调用命令的实现函数

在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的 cmd 属性中,参数和参数个数保存在 argv 属性和 argc 属性里面,当服务器要执行命令时,它只要执行以下语句就可以了:

// client 是指向客户端状态的指针
client -> cmd -> proc(client);

产生的命令回复,保存在输出缓存区里面,之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。

命令执行器(4):执行后续工作

执行完实现函数后的后续工作:

  • 如果开启慢查询日志功能,检查是否需要添加一个新的慢查询日志
  • 根据刚刚执行命令所耗费的时长,更新 microseconds, calls 赠一
  • 如果开启 AOF 持久化功能,写入 AOF 缓存区里面
  • 如果有其他从服务器正在复制当前服务器,会将这个命令发送给所有从服务器。

之后,就会处理下一个命令请求。

将命令回复发送给客户端

服务器会执行命令回复处理器,将保存在客户端输出缓存区中的命令回复发送给客户端,并清空输出缓存区。

客户端接收并打印命令回复

redis-cli 打印在命令行

serverCron 函数

Redis 服务器中的 serverCron 函数默认每隔 100 毫秒执行一次,这个函数复制管理服务器的资源,并保持服务器自身的良好运转。

更新服务器时间缓存

Redis 服务器中有不少功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的 unixtime 属性和 mstime 属性被用作当前时间的缓存:

struct redisServer{
    // ...
    /* time cache */
    time_t unixtime;        /* Unix time sampled every cron cycle. */
    PORT_LONGLONG mstime;       /* Like 'unixtime' but with milliseconds resolution. */
    // ... 
};

每隔100 毫秒执行一次所以精度不高:

  • 服务器只会在执行日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精度要求不高的功能上 “ 使用 unixtime 属性和 mstime 属性 ”。
  • 对于为键设置过期时间、添加慢查询日志这种需要高精度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间。
更新 LRU 时钟

服务器状态中的 lruclock 属性保存了服务器的 LRU 属性,也是系统时间缓存的一种。

struct redisServer{
    // ...
    unsigned lruclock:LRU_BITS; /* Clock for LRU eviction */
    // ... 
};

每个 Redis 对象都会有一个 lru 属性,保存了对象最后一次被命令访问的时间:

typedef struct redisObject{
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    // LRU
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    // 引用计数
    int refcount;
} robj;

当服务器计算一个数据库键的空转时间(也即是数据库键对应的值对象的空转时间),程序会用服务器的 lruclock 属性记录的时间减去对象的 lru 属性记录的时间,得出的计算结果就是这个对象的空转时间。

lruclock 时钟的当前值可以通过 INFO server 命令的 lru_clock 域查看。

更新服务器每秒执行命令次数

估算并记录服务器在最近一秒钟处理的命令请求数量。

struct redisServer{
    // ...
    /* The following two are used to track instantaneous metrics, like
     * number of operations per second, network traffic. */
    struct {
        PORT_LONGLONG last_sample_time; /* Timestamp of last sample in ms */
        PORT_LONGLONG last_sample_count;/* Count in last sample */
        PORT_LONGLONG samples[STATS_METRIC_SAMPLES];
        int idx;
    } inst_metric[STATS_METRIC_COUNT];
    // ... 
};
更新服务器内存峰值记录
struct redisServer{
    // ...
    size_t stat_peak_memory;        /* Max used memory record */
    // ... 
};

通过命令 info memory 查看

used_memory_peak:710944
used_memory_peak_human:694.28K
处理 SIGTERM 信号

在启动服务器时,Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数,这个信号处理器负责在服务器连到 SIGTERM 信号时,打开服务器状态的 shutdwon_asap 标识

struct redisServer{
    // ...
    // 关闭服务器的标识
    // 值为 1 时,关闭服务器
    // 值为 0 时,不做动作
    int shutdown_asap;          /* SHUTDOWN needed ASAP */
    // ... 
};

每次 serverCron 函数运行时,程序都会对服务器状态的 shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器。

管理客户端资源

调用函数:

/* We need to do a few operations on clients asynchronously. */
clientsCron();

对一定数量的客户端进行以下两个检查:

  • 如果客户端与服务器之间的连接已经超时,那么程序释放这个客户端。
  • 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存。
管理数据库资源

调用函数:

/* Handle background operations on Redis databases. */
databasesCron();

这个函数会对服务器钟的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作。

执行被延迟的 BGREWRITEAOF

在服务器执行 BGSAVE 命令的期间,如果客户端向服务器发来 BGREWRITEAOF 命令,那么服务器会将其延迟到 BGSAVE 命令执行完之后。

服务器的 aof_rewrite_scheduled 标识

struct redisServer{
    // ...
    // 如果值为 1,那么表示有 BGREWRITEAOF 命令被延迟了
    int aof_rewrite_scheduled;      /* Rewrite once BGSAVE terminates. */
    // ... 
};
检查持久化操作的运行状态

检查以下两个属性:

struct redisServer{
    // ...
    // 记录执行 BGSAVE 命令的子进程的ID:
    // 如果服务器没有在执行 BGSAVE
    // 那么这个属性的值为-1
    pid_t rdb_child_pid;            /* PID of RDB saving child */
    
    // 记录执行 BGREWRITEAOF 命令的子进程的ID
    // 如果服务器没有执行 BGREWRITEAOF
    // 那么这个属性的值为 -1
    pid_t aof_child_pid;            /* PID if rewriting process */
    // ... 
};

当:其中一个属性的值不为 -1,程序就会执行一次 wait3 函数,检查子进程是否有信号发来服务器进程:

  • 如果有信号到达,那么表示新的 RDB 文件已经生成完毕,或者 AOF 文件已经重写完毕,服务器需要进行相应命令的后续操作。比如替换。
  • 如果没有到达,那么表示持久化操作未完成,程序不做动作。

当:都为 -1 ,程序执行以下三个检查:

  1. 查看是否有 BGREWRITEAOF 被延迟了,如果有的话,那么开始一个新的 BGREWRITEAOF 操作。
  2. 检查服务器的自动保存条件是否已经被满足,如果条件被满足,并且服务器没有在执行其他持久化操作,那么服务器开始一个新的 BGSAVE 操作。
  3. 检查服务器设置的 AOF 重写条件是否满足,如果条件满足,并且服务器没有在执行其他持久化,就开始一个新的 BGREWRITEAOF 操作。
将 AOF 缓存区钟的内容写入 AOF 文件

如果服务器开启了 AOF 持久化功能,并且 AOF 缓存区里面还有待写入的数据,那么 serverCron 函数会调用相应的程序,将 AOF 缓存区钟的内容写入到 AOF 文件里面。

关闭异步客户端

这一步,服务器会关闭那些输出缓存区大小超出了限制的客户端。

添加 cronloops 计数器的值

cronloops 属性记录了 serverCron 函数执行的次数

struct redisServer{
    // ...
    // 函数每次执行一次,增一
    int cronloops;              /* Number of times the cron function run */
    // ... 
};

cronloops 属性目前在服务器的为一个作用,就是在负责模块中实现 “ 每执行 serverCron 函数 N 次就执行一次指定代码 ” 的功能。

初始化服务器

一个 Redis 服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如,初始化服务器状态,接受用户指定的服务器配置,创建相应的数据结构和网络连接等等。

初始化服务器状态结构

初始化服务器的第一步就是创建一个 struct redisServer 结构的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值。

初始化工作位于 server.c/initServerConfig 函数。以下是部分代码:

void initServerConfig(void) {
    int j;
	// 设置服务器的运行 id
    getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
    // 设置默认配置文件路径
    server.configfile = NULL;
	// 设置默认服务器评论
    server.hz = CONFIG_DEFAULT_HZ;
    // 为运行 id 加上结尾字符
    server.runid[CONFIG_RUN_ID_SIZE] = '\0';
    // 设置服务器的运行架构
    server.arch_bits = (sizeof(PORT_LONG) == 8) ? 64 : 32;
    // 设置默认服务器端口号
    server.port = CONFIG_DEFAULT_SERVER_PORT;
 
    // ...
}

void initServerConfig(void)函数的主要工作:

  • 设置服务器的运行ID
  • 设置服务器的默认运行频率
  • 设置服务器的默认配置文件路径
  • 设置服务器的运行架构
  • 设置服务器的默认端口号
  • 设置服务器的默认 RDB 持久化条件和 AOF 持久化条件
  • 初始化服务器的 LRU 时钟
  • 创建命令表

设置了一些服务器属性都是一些整数、浮点数、或者字符串属性,除了命令表之外,其他数据结构(数据库、慢查询日志、Lua 环境、共享对象等)都是在后面被创建。

载入配置选项

启动服务器时,用户可以通过给定配置参数或者指定配置文件来配置服务器的默认配置。

初始化服务器数据结构

除了命令表之外,服务器还包括其他数据结构:

  • server.clients 链表,这个链表记录了所有与服务器相连的客户端的状结构,链表的每个节点都包含了一个 redisClient 结构实例
  • server.db 数组,数组中包含了服务器的所有数据库
  • 用于保存频道订阅信息的 server.pubsub_channels 字典,以及用于保存模式订阅信息的 server.pubsub_patterns 链表
  • 用于执行 Lua 脚本的 Lua 环境 server.lua
  • 用于保存慢查询日志的 server.slowlog 属性

调用 void initServer(void) 函数,为以上结构分配内存。除此还进行了一些非常重要的设置操作:

  • 为服务器设置进程信号处理器。
  • 创建共享对象,服务器通过重用这些共享对象来避免反复创建相同的对象。
  • 打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正在运行时接受客户端的连接。
  • 为 serverCron 函数创建时间事件,等待服务器正在运行时执行 serverCron 函数。
  • 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件,为 AOF 写入做好准备。
  • 初始化服务器的后台 I/O 模块(bio),为将来的 I/O 操作做好准备。
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 3.2.100 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 16804
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

[16804] 31 Oct 03:22:29.551 # Server started, Redis version 3.2.100
还原数据库状态

服务器载入 RDB 文件或者 AOF 文件,并根据文件记录的内容来还原服务器的数据库状态。

  • 如果启动了 AOF 持久化功能,服务器使用 AOF 文件来还原数据库状态。
  • 如果没有启动了 AOF 持久化功能,服务器使用 RDB 文件来还原数据库状态。
[16804] 31 Oct 03:22:29.552 * DB loaded from disk: 0.000 seconds
执行事件循环

在初始化的最后一步,服务器将打印一下日志:

[16804] 31 Oct 03:22:29.552 * The server is now ready to accept connections on port 6379

并开始执行服务器的事件循环(loop)。

服务器初始化工作圆满完成,开始接受客户端的连接请求,并处理客户端发来的命令请求。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值