Redis概览

Redis的优点

支持众多的数据类型 支持持久化 支持”事件” 支持多机、集群分布式 支持发布与订阅 支持事务 支持嵌入Lua脚本

5种数据类型

Redis支持5种数据类型,分别是字符串对象、列表对象、哈希对象、集合对象和有序集合对象。 127.0.0.1:6379> set name “xuwenzhi” OK 127.0.0.1:6379> get name “xuwenzhi” 127.0.0.1:6379> rpush numbers 1 2 3 4 5 (integer) 5 127.0.0.1:6379>

持久化

RDB持久化 AOF持久化

事件

文件事件 : 动态响应客户端请求 时间事件 : 用于常规的维护和管理操作保证Redis正常运行,包括一些定时的操作

多机、集群分布式

复制 : 服务器间的数据通信 Sentinel : 监视服务器的运行状态以及故障转移的方法 集群 : 节点之间的通信方法

发布与订阅

PUBLISH SUBSCRIBE PUBSUB

SDS

Redis中所有的字符串都使用了一个结构,叫做SDS(Simple Dynamic String),比如K-V中的键值,或者V中复杂对象中使用的字符串都使用了SDS。

sds.h/sdshdr
/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 数据空间
    char buf[];
};

那么问题来了,为什么Redis不直接使用C语言中的字符串操作方式,还要在此基础上封装了层SDS呢?

SDS的优势

  • 当要查询某个字符串中的某个字符时,无需像C中那样遍历,复杂度为O(1),且获取字符串长度的时候相当快
  • 保证不会出现缓冲区溢出,比如当进行修改操作时,可以立刻通过free知道当前剩余空间是否能够执行修改而不会发生溢出现象
  • 增加了SDS之后,可以保证在频繁的拼接字符串导致的内存重分配操作,且通过free的透明度,可以实现Redis的空间预分配和惰性释放等特点。
  • 二进制安全,保证SDS可以处理二进制数据。

空间预分配 : 当对SDS进行修改时

  • 当长度小于1M时,假如此时的len为13,那么会同时为free也申请13长度的大小,此时buf的长度为13+13+1
  • 当长度大于1M时,加入此时的len为30M,那么会同时为free申请1M,此时buf的长度为30M+1M+1byte(至于为什么为free申请1M,看这里#define SDS_MAX_PREALLOC (1024*1024))

Redis Object

Redis的对象系统并不是一个很新的东西,而是基于字符串对象、列表对象、哈希对象、集合对象和有序集合对象的基础上整合出的对象系统。比如创建一个简单的字符串对象时中的键值对,键和值都是一个Redis对象。

//redis.h/redisObject
typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    // 引用计数
    int refcount;
    // 指向实际值的指针
    void *ptr;
} robj;

type : 对象的类型,可选值为 REDIS_STRING、REDIS_LIST、REDIS_HASH、REDIS_SET和REDIS_ZSET等。可以通过TYPE命令来获得对象的类型。

127.0.0.1:6379> get name
"xuwenzhi"
127.0.0.1:6379> TYPE name
string
127.0.0.1:6379> RPUSH numbers 1 2 3
(integer) 8
127.0.0.1:6379> TYPE numbers
list
127.0.0.1:6379> HMSET profile name Tome age 25 career Programmer
OK
127.0.0.1:6379> TYPE profile
hash
127.0.0.1:6379> SADD fruits apple banana cherry
(integer) 3
127.0.0.1:6379> TYPE fruits
set
127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
127.0.0.1:6379> TYPE price
zset
127.0.0.1:6379>

http://img.xuwenzhi.com/redisbianmachangliang.jpg

ptr : 指向对象的底层实现数据结构,而由于Redis底层实现有很多种,还需要encoding成员来指定。下图为encoding的可选值及解释。 http://img.xuwenzhi.com/redistypeencoding.jpg

此时,Redis的对象结构体中,存在type和encoding两个元素来区分对象的不同,当这两个成员进行组合的时候,又可以组成更多种不同的对象,原因在于每种type的对象都至少使用了两种不同的encoding。

ListNode

链表在Redis中的使用非常广泛,比如列表键的底层实现之一就是链表,其次还有发布与订阅、慢查询、监视器和通过链表来保存多个客户端的状态信息。

Redis链表的实现

adlist.h/listNode
/*
 * 双端链表节点
 */
typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点的值
    void *value;
} listNode;

虽然listNode本身就可以实现链表的双端等功能,但Redis本身在使用链表时又对listNode进行了封装

/*
 * 双端链表结构
 */
typedef struct list {
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void (*free)(void *ptr);
    // 节点值对比函数
    int (*match)(void *ptr, void *key);
    // 链表所包含的节点数量
    unsigned long len;
} list;

特点

  • 双端:取头和尾都是O(1)
  • 无环:头尾节点均指向NULL
  • 获取链表长度只需list.len
  • 多态:链表节点使用(void*)来保存节点值,保证链表可以存储不同类型的值

Integer Set

当一个集合只包含整数值元素,并且数量不多时,Redis就会使用整数集合作为集合键的底层实现。

//intset.h/intset
typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} intset;

contents[] : 虽然content的类型定义成int8_t的,但是contents[]真正的类型是由encoding决定。并且contents[]中的数字都是按照从小到大顺序排列的。

整数集合的升级

当需要向contents[]中新增数字(<最小的数||>最大的数)时,需要为整数集合进行升级操作,升级操作不光包含contents[]长度的扩大还包括encoding编码类型的变更。

整数集合升级的步骤

根据新增数字的类型,扩展整数集合底层的内存空间,并未新增数据分配空间 将原有的整数集合都转换成新数字的相同的类型,并且原有的从小到大的顺序不变 将新数字添加到整数集合中

整数集合的降级

不支持。

整数集合的优点

  • 存储不同类型的整型,灵活
  • 节约内存空间

Map

字典在Redis中的实现相当广泛,比如Redis数据库中的增删改查底层就是通过字典实现的。

127.0.0.1:6379> set name "xuwenzhi"
OK

比如此例子中的name和”xuwenzhi”这一个键值对的映射就是通过字典实现的。

字典的实现

Redis字典的底层实现使用了Hash表。

dict.h/dictht
/*
 * 哈希表
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

再看其中的table,table的类型为一个dictEntry。

dict.h/dictEntry
/*
 * 哈希表节点
 */
typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

其中的key即为字典的Key,v则为字典的Value。

字典的结构

dict.h/dict
/*
 * 字典
 */
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */
} dict;

type:为dictType类型,表示该字典的类型,Redis会为不同类型的字典设置不同的字典操作函数等。而privdata保存了传递给不同操作函数的数据。 ht:ht指向了一个dictht hash表。大小为2,一般使用h[0],h[1]为备用,主要用于rehash。 rehashidx:为rehash的状态,当为-1时,表示当前没有进行rehash。

dict.h/dictType
/*
 * 字典类型特定函数
 */
typedef struct dictType {
    // 计算哈希值的函数
    unsigned int (*hashFunction)(const void *key);
    // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值的函数
    void *(*valDup)(void *privdata, const void *obj);
    // 对比键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);
    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

Hash算法 哈希值的计算: 确定是哪张hash表dictEntry //计算键key的hash值

hash = dict->type->hashFunction(key);

索引值的计算: 确定是dictEntry中的哪个索引值 index = hash & dict->ht[x].sizemask; x根据情况为0或1。

解决Hash冲突

Redis的hash冲突解决办法为链地址法,当新的key与已有的key冲突时,则在原有的key基础上通过dictEntry.next指针链接上。

Rehash

当然通过原有的解决冲突方案可能会导致dictEntry的链接越来越长,如果超过了哈希表的负载因子(load factor),那么就需要Rehash登场,也就是ht[1]要登场了。 load factor = ht[0].used / ht[0].size

如何进行Rehash

为ht[1]分配空间:至于分配多大取决于ht[0].used的大小 将保存在ht[0]上的所有的键值对重新计算hash值保存在ht[1]上 释放ht[0],将ht[1]设置为ht[0],然后重新生成ht1,为下一次rehash做准备

Hash表的收缩与扩展

什么时候扩展?

当服务器当前没有在执行BGSAVE或者BGREWRITEAOF时,并且负载因子>1 当服务器当前正在执行BGSAVE或者BGREWRITEAOF时,并且负载因子>5 什么时候收缩? 当Hash表的负载因子<0.1时,自动收缩

Rehash是渐进式的

也就是说在Rehash过程中,ht[0]并不会一下子就转移到ht[1]中,而是渐进式的。 在Rehash过程中,rehashidx起到了很重要的作用,详细的步骤为: 为ht[1]分配空间 将rehashidx置为0,代表rehash开始 随着渐进式的rehash过程中,会更改rehashidx为当前rehash的key值 不知不觉rehashidx变为-1,表示rehash完成。

Skip List

跳跃表是一种有序的数据结构,通过在每个节点中保存多个指向其他节点的指针以达到快速访问的目的。 时间复杂度 : 平均O(logN), 最坏O(N)

http://img.xuwenzhi.com/tiaoyuebiao.jpg

在Redis中,跳跃表主要用于实现有序集合键和集群节点中用作内部数据结构。

图中的第一个节点包含L(1->32),通过跳跃表节点zskiplistNode.level[0-32]标记。而水平方向的L1->L1->L1…则是通过跳跃表节点zskiplistNode中的前进指针(forward)实现的。

层 : 在这里的层的概念是,图中L1->L1->L1意为一层,L2->L2->L2->L2意为一层。 跨度(span) : 比如图中的L1->L1是相连的,跨度就为1;而L5->L5跨度为3。 长度(length) : 如图中,最长的L1->L1->L1->L1,最长的长度为3(忽略表头结点)。 BW(backward) : 指向上一个节点的指针。 score : 各个节点的分值,如1.0 2.0 3.0等,节点按分值从小到大排列。

zskiplistNode主要用于存储跳跃表节点。

//redis.h/zskiplistNode
/*
 * 跳跃表节点
 */
typedef struct zskiplistNode {
    // 成员对象,可见是一个redis对象
    robj *obj;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

zskiplist主要记录跳跃表节点的相关信息

//redis.h/zskiplist
/*
 * 跳跃表
 */
typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

redisServer

服务中的数据库

Redis的所有数据库信息都保存在redis.h/redisServer结构中,每一个数据库节点由redis.h/redisDb结构定义。

//redis.h/redisDb
typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    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 */
    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    // 数据库号码
    int id;                     /* Database ID */
    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

在服务器启动时,Redis通过redis.h/redisServer中定义的dbnum数量来初始化数据库的数量,默认为16个数据库。

/* Static server configuration */
// ...
#define REDIS_DEFAULT_DBNUM     16  /*默认的数据库数量*/
// ...
//redis.h/redisServer
struct redisServer {
    redisDb *db;
    int dbnum;
}

切换数据库 Redis默认使用第0个数据库,还可以通过 SELECT n,来切换数据库。

127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> get name
(nil)
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> get name
"xuwenzhi"

SELECT的实现原理 由于Redis中定义了客户端结构体,里面存储了当前正在使用的数据库节点的指针,所以通过SELECT进行切换数据库的时候,只需改变指针即可。

//redis.h/redisClient
typedef struct redisClient {
    //...
    // 当前正在使用的数据库的 id (号码)
    int dictid;
    //...
}

数据库的键空间

Redis将数据库中所有的键值对保存在了redis.h/redisDb.dict字典中,叫这个字典为”键空间”。键空间的键就是一个字符串对象,键空间的值都可以是任意的Redis对象(字符串、列表、哈希、集合和有序集合)。 所以所做的SET、GET、DEL的该命令实际上是在做数据库的增删改查而已。

其他数据库操作命令

FLUSHDB : 顾名思义,清空数据库,也就是通过删除键空间来实现 RANDOMKEY : 随机返回键空间的某个键 DBSIZE : 返回键空间键值对的数量 EXISTS : 是否存在某个键 RENAME : 对键重命名 KEYS : 返回所有的键 EXPIRE : 设置某个key的过期时间 TTL : 返回某个key的过期时间

127.0.0.1:6379> RANDOMKEY
"profile"
127.0.0.1:6379> DBSIZE
(integer) 5
127.0.0.1:6379> EXISTS name
(integer) 1
127.0.0.1:6379> EXISTS name1
(integer) 0
127.0.0.1:6379> KEYS *
1) "fruits"
2) "profile"
3) "price"
4) "numbers"
5) "name"
127.0.0.1:6379>

通常的过期键删除策略

定时删除 : 每个键都有个贴身timer,一过期的时候就删除。要为所有的键建立定时器,太占用CPU资源,不可取。 惰性删除 : 不做任何检查,当做GET操作时,发现键过期就删除。对内存来说不够友好,有可能长时间不用的键值对还长时间的保留在内存中,可能发生内存泄露。 定期删除 : 每隔一段时间,对键空间检查,过期就删除。难点在于定期这个周期是多少,可能无法保证数据的一致性。 ·

Redis的过期删除策略

  • 惰性删除+定期删除
惰性删除
//db.c/expireIfNeeded
/*
 * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。
 *
 * 返回 0 表示键没有过期时间,或者键未过期。
 *
 * 返回 1 表示键已经因为过期而被删除了。
 */
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. */
    // 当服务器运行在 replication 模式时
    // 附属节点并不主动删除 key
    // 它只返回一个逻辑上正确的返回值
    // 真正的删除操作要等待主节点发来删除命令时才执行
    // 从而保证数据的同步
    if (server.masterhost != NULL) return now > when;
    // 运行到这里,表示键带有过期时间,并且服务器为主节点
    /* Return when this key has not expired */
    // 如果未过期,返回 0
    if (now <= when) return 0;
    /* Delete the key */
    server.stat_expiredkeys++;
    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    // 将过期键从数据库中删除
    return dbDelete(db,key);
}
定期删除

Redis的服务器周期性操作redis.c/serverCron时,会调用activeExpireCycle。在规定的时间内,分多次遍历服务器中的各个数据库,从数据库中的expires字典中随机检查一部分键的过期键。

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 long long last_fast_cycle = 0; /* When last fast cycle ran. */
    unsigned int j, iteration = 0;
    // 默认每次处理的数据库数量
    unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
    // 函数开始的时间
    long long 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. */
        // 如果上次函数没有触发 timelimit_exit ,那么不执行处理
        if (!timelimit_exit) return;
        // 如果距离上次执行未够一定时间,那么不执行处理
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        // 运行到这里,说明执行快速处理,记录当前时间
        last_fast_cycle = start;
    }
    /* We usually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     *
     * 一般情况下,函数只处理 REDIS_DBCRON_DBS_PER_CALL 个数据库,
     * 除非:
     *
     * 1) Don't test more DBs than we have.
     *    当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL
     * 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. */
    // 函数处理的微秒时间上限
    // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也即是 25 % 的 CPU 时间
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;
    // 如果是运行在快速模式之下
    // 那么最多只能运行 FAST_DURATION 微秒
    // 默认值为 1000 (微秒)
    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. */
        // 为 DB 计数器加一,如果进入 do 循环之后因为超时而跳出
        // 那么下次会直接从下个 DB 开始处理
        current_db++;
        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
            /* If there is nothing to expire try next DB ASAP. */
            // 获取数据库中带过期时间的键的数量
            // 如果该数量为 0 ,直接跳过这个数据库
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            // 获取数据库中键值对的数量
            slots = dictSlots(db->expires);
            // 当前时间
            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. */
            // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS)
            // 跳过,等待字典收缩程序运行
            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 计数器
            ttl_sum = 0;
            // 总共处理的键计数器
            ttl_samples = 0;
            // 每次最多只能检查 LOOKUPS_PER_LOOP 个键
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
            // 开始遍历数据库
            while (num--) {
                dictEntry *de;
                long long ttl;
                // 从 expires 中随机取出一个带过期时间的键
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                // 计算 TTL
                ttl = dictGetSignedIntegerVal(de)-now;
                // 如果键已经过期,那么删除它,并将 expired 计数器增一
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl < 0) ttl = 0;
                // 累积键的 TTL
                ttl_sum += ttl;
                // 累积处理键的个数
                ttl_samples++;
            }
            /* Update the average TTL stats for this database. */
            // 为这个数据库更新平均 TTL 统计数据
            if (ttl_samples) {
                // 计算当前平均值
                long long avg_ttl = ttl_sum/ttl_samples;

                // 如果这是第一次设置数据库平均 TTL ,那么进行初始化
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                /* Smooth the value averaging with the previous one. */
                // 取数据库的上次平均 TTL 和今次平均 TTL 的平均值
                db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
            }
            /* 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++;
            // 每遍历 16 次执行一次
            if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
                (ustime()-start) > timelimit)
            {
                // 如果遍历次数正好是 16 的倍数
                // 并且遍历的时间超过了 timelimit
                // 那么断开 timelimit_exit
                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. */
            // 如果已删除的过期键占当前总数据库带过期时间的键数量的 25 %
            // 那么不再遍历
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}

AOF、RDB和复制功能对过期键的处理

  • 生成RDB文件 在使用SAVE或者BGSAVE生成RDB文件时,不会对过期键进行存储

  • 载入RDB文件 当服务器以主服务器的形式存在时,载入RDB时,不会对过期键进行载入 当服务器以从服务器的形式存在时,会将所有键载入

AOF

待定。

主从复制

  • 当主服务器删除键时,会向所有从服务器发送DEL命令
  • 当客户端在从服务器上读取数据时,即使发现键过期,也不删除
  • 从服务器只接收来自主服务器的命令