# Redis过期策略
在redis中,我们可以在保存数据时指定过期时间,比如通过set命令的EX/PX等选项参数或者通过expire/expireat等命令设定某个key的过期时间。
过期机制主要用于实现几个目的
- 利用过期机制实现一些时间相关的功能,比如限速器、时间段计数器等
- 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;
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);
}
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第二个时机是redis会定时扫描expires表,清理过期的key,这个会运行在redis的主线程中。
- 每秒进行10次扫描
- 每次从过期字典中随机20个key
- 删除20个key钟已经过期的key
- 如果过期的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;
}
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的值时,如果没有传入过期时间,则会清理掉之前的过期时间(如果有)
← 字符串使用和内部原理 内存逐出淘汰机制 →