# Redis过期策略

在redis中,我们可以在保存数据时指定过期时间,比如通过set命令的EX/PX等选项参数或者通过expire/expireat等命令设定某个key的过期时间。

过期机制主要用于实现几个目的

  1. 利用过期机制实现一些时间相关的功能,比如限速器、时间段计数器等
  2. redis的内存容量有限,通过设置过期时间,不经常使用的数据能够自动清理,为其他数据留出可用内存。

# 过期机制的实现

那么redis是如何实现过期的呢?

# 过期数据存储

如果保存数据时设置了过期参数或者通过调用expire命令, redis中除了数据的dict(即map),还有一个expires表保存过期信息,key是对应要过期的key,value是要过期的绝对时间。

/* 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Set an expire to the specified key. If the expire is set in the context
 * of an user calling a command 'c' is the client, otherwise 'c' is set
 * to NULL. The 'when' parameter is the absolute unix time in milliseconds
 * after which the key will no longer be considered valid. */
void setExpire(client *c, redisDb *db, robj *key, long long when) {
    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    kde = dictFind(db->dict,key->ptr);
    serverAssertWithInfo(NULL,key,kde != NULL);
    de = dictAddOrFind(db->expires,dictGetKey(kde));
    dictSetSignedIntegerVal(de,when);

    int writable_slave = server.masterhost && server.repl_slave_ro == 0;
    if (c && writable_slave && !(c->flags & CLIENT_MASTER))
        rememberSlaveKeyWithExpire(db,key);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 过期删除

redis删除过期key有两种时机 第一个时机是在访问这个key的时候(比如查询、要修改等情况),如果发现过期时间已过,则会进行删除。这也称为lazy惰性删除。 如下代码中expireIfNeeded会判断key的过期时间,如果过期会进行删除,删除分为同步删除和异步删除。

robj *lookupKey(redisDb *db, robj *key, int flags) {
    dictEntry *de = dictFind(db->dict,key->ptr);
    robj *val = NULL;
    if (de) {
        val = dictGetVal(de);
        /* Forcing deletion of expired keys on a replica makes the replica
         * inconsistent with the master. We forbid it on readonly replicas, but
         * we have to allow it on writable replicas to make write commands
         * behave consistently.
         *
         * It's possible that the WRITE flag is set even during a readonly
         * command, since the command may trigger events that cause modules to
         * perform additional writes. */
        int is_ro_replica = server.masterhost && server.repl_slave_ro;
        int force_delete_expired = flags & LOOKUP_WRITE && !is_ro_replica;
        if (expireIfNeeded(db, key, force_delete_expired)) {
            /* The key is no longer valid. */
            val = NULL;
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第二个时机是redis会定时扫描expires表,清理过期的key,这个会运行在redis的主线程中。

  1. 每秒进行10次扫描
  2. 每次从过期字典中随机20个key
  3. 删除20个key钟已经过期的key
  4. 如果过期的key比例超过1/4,重复步骤2

为了避免扫描任务过多导致redis线程卡主不能响应用户请求,redis对扫描时间做了25ms的限制。

/* Check the set of keys created by the master with an expire set in order to
 * check if they should be evicted. */
void expireSlaveKeys(void) {
    if (slaveKeysWithExpire == NULL ||
        dictSize(slaveKeysWithExpire) == 0) return;

    int cycles = 0, noexpire = 0;
    mstime_t start = mstime();
    while(1) {
        dictEntry *de = dictGetRandomKey(slaveKeysWithExpire);
        sds keyname = dictGetKey(de);
        uint64_t dbids = dictGetUnsignedIntegerVal(de);
        uint64_t new_dbids = 0;

        /* Check the key against every database corresponding to the
         * bits set in the value bitmap. */
        int dbid = 0;
        while(dbids && dbid < server.dbnum) {
            if ((dbids & 1) != 0) {
                redisDb *db = server.db+dbid;
                dictEntry *expire = dictFind(db->expires,keyname);
                int expired = 0;

                if (expire &&
                    activeExpireCycleTryExpire(server.db+dbid,expire,start))
                {
                    expired = 1;
                }


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 大量key同一时间过期的风险

虽然上面的扫描过期key做了时间限制,但是也只是限制单次的时间,而不是整体redis一段时间内扫描过期key的时间,所以 如果同一时间出现大量的key过期,还是会影响用户的redis请求。 所以在业务系统设计时,对于key的过期时间如果可能有大量key同时过期,需要在key的过期时间加上一定的随机,避免同时过期的风险。

# master slave同步如何处理过期

从库不会进行过期扫描,是通过主库key过期时的删除命令同步到从库实现从库数据过期的。 如果主从切换时有删除命令没有同步到从库,则可能导致主从数据不一致。

# set修改key后会清理过期时间

在调用set修改key的值时,如果没有传入过期时间,则会清理掉之前的过期时间(如果有)