您现在的位置是:网站首页> 编程资料编程资料

详解Redis 缓存删除机制(源码解析)_Redis_

2023-05-27 761人已围观

简介 详解Redis 缓存删除机制(源码解析)_Redis_

删除的范围

  1. 过期的 key
  2. 在内存满了的情况下,如果继续执行 set 等命令,且所有 key 都没有过期,那么会按照缓存淘汰策略选中的 key

过期删除

redis 中设置了过期时间的 key 会单独存储一份

 typedef struct redisDb { dict *dict; // 所有的键值对 dict *expires; //设置了过期时间的键值对 // ... } redisDb; 

设置有效期

Redis 中有 4 个命令可以给 key 设置过期时间,分别是 expire pexpire expireat pexpireat 

设置相对时间

expire :将 key 值的过期时间设置为 ttl 秒。

 // src/expire.c /* EXPIRE key seconds */ void expireCommand(client *c) { expireGenericCommand(c,mstime(),UNIT_SECONDS); } 

pexpire :将 key 值的过期时间设置为 ttl 毫秒。

 // src/expire.c /* PEXPIRE key milliseconds */ void pexpireCommand(client *c) { expireGenericCommand(c,mstime(),UNIT_MILLISECONDS); } 

设置绝对时间

expireat :将 key 值的过期时间设置为指定的 timestamp 秒数。

 // src/expire.c /* EXPIREAT key time */ void expireatCommand(client *c) { expireGenericCommand(c,0,UNIT_SECONDS); } 

pexpireat :将 key 值的过期时间设置为指定的 timestamp 毫秒数。

 // src/expire.c /* PEXPIREAT key ms_time */ void pexpireatCommand(client *c) { expireGenericCommand(c,0,UNIT_MILLISECONDS); } 

以上 4 种方法最终都会调用下面的通用函数 expireGenericCommand :

 // src/expire.c void expireGenericCommand(client *c, long long basetime, int unit) { robj *key = c->argv[1], *param = c->argv[2]; // 获取数据对象 long long when; if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK) return; // 将时间转化成以 ms 为单位 if (unit == UNIT_SECONDS) when *= 1000; when += basetime; // 在 master 节点上,如果设置的过期时间小于当前时间,那么将命令转化成 DEL 指令 if (when <= mstime() && !server.loading && !server.masterhost) { robj *aux; int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) : dbSyncDelete(c->db,key); // ... // 将删除命令同步给 slave 和 AOF // ... } else { // 设置过期时间 setExpire(c,c->db,key,when); // ... // 构造返回值和发布对象更新消息 // ... return; } } 

设置过期时间的操作由 setExpire 执行,他将 dictEntry 的 union v 中的 s64 设为过期时间

 // src/db.c void setExpire(client *c, redisDb *db, robj *key, long long when) { dictEntry *kde, *de; // 找出 db->dict 中对应的存储对象,这里的查询和用 get 查询数据是逻辑一样,通过 hashFunc(key) & sizemask // 找到 bucket 后在链表中遍历 kde = dictFind(db->dict,key->ptr); // 找出 db->expires 中对应的存储对象,如果没有则新建一个 de = dictAddOrFind(db->expires,dictGetKey(kde)); // dictSetSignedIntegerVal(de,when); // ... } #define dictSetSignedIntegerVal(entry, _val_) \ do { (entry)->v.s64 = _val_; } while(0) 

db->expires 中存储的  dictEntry 表示的是过期 key 和过期时间,存储过期时间的 v 是一个 union ,可见在 redis 中不同使用场景或不同编码下 v 的意义不同

 typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry; 

查询过期时间

ttl key 返回 key 剩余过期秒数。

 // src/expire.c /* TTL key */ void ttlCommand(client *c) { ttlGenericCommand(c, 0); } 

pttl key 返回 key 剩余过期的毫秒数。

 // src/expire.c /* PTTL key */ void pttlCommand(client *c) { ttlGenericCommand(c, 1); } 

以上 2 种查看方式最终都会调用下面的通用函数 ttlGenericCommand :

 // src/expire.c /* Implements TTL and PTTL */ void ttlGenericCommand(client *c, int output_ms) { // ... // key 不存在时报错 // ... // 获取过期时间,如果没有过期时间则 expire = getExpire(c->db,c->argv[1]); if (expire != -1) { ttl = expire-mstime(); if (ttl < 0) ttl = 0; } if (ttl == -1) { addReplyLongLong(c,-1); } else { // 根据指定的单位返回结果,以秒为单位时向上取整 addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000)); } } 

获取过期时间的操作由 getExpire 执行,在 db->expires 中查询到对象后,获取 union v 中的成员 s64 

 // src/expire.c // 返回过期时间的绝对时间 long long getExpire(redisDb *db, robj *key) { dictEntry *de; // 查询对象 if (dictSize(db->expires) == 0 || // 如果返回为 NULL 表示没有设置过期时间,向上返回 -1 (de = dictFind(db->expires,key->ptr)) == NULL) return -1; // 获取 v.s64 return dictGetSignedIntegerVal(de); } #define dictGetSignedIntegerVal(he) ((he)->v.s64) 

过期策略

Redis 综合使用 惰性删除 和 定期扫描 实现

惰性删除

每次访问时会调用 expireIfNeeded 判断 key 是否过期,如果过期就删除该键,否则返回键对应的值。单独使用这种策略可能会浪费很多内存。

 // src/db.c int expireIfNeeded(redisDb *db, robj *key) { mstime_t when = getExpire(db,key); mstime_t now; // 没有设置过期时间,直接返回 if (when < 0) return 0; // 从硬盘中加载数据时不执行过期操作 if (server.loading) return 0; // 参考 GitHub Issue #1525 // 对于 master,在执行 Lua Script 的过程中,可能会用某个 key 是否存在当作判断条件 // 为了避免一个脚本中前后条件不一致,将当前时间强制设为脚本开始时间 now = server.lua_caller ? server.lua_time_start : mstime(); // 对于 slave,返回此时 key 是否已过期,但不执行后续删除操作 if (server.masterhost != NULL) return now > when; // key 未过期 if (now <= when) return 0; // 统计过期 key 的个数 server.stat_expiredkeys++; // 向所有的 slave 和 AOF 文件写入一条 DEL 指令 propagateExpire(db,key,server.lazyfree_lazy_expire); // 向 keyspace channel 中发布一条 key 过期的消息 notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",key,db->id); // 根据配置决定是同步删除还是异步删除(仅删除引用,由后台线程执行物理删除) return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key); } 

特殊处理

在 master 节点执行 Lua 脚本时

参考 GitHub Issue #1525,对于 master,在执行 Lua Script 的过程中,可能会用某个 key 是否存在当作判断条件。为了避免一个脚本中前后条件不一致,将当前时间强制设为脚本开始时间。
例如多次执行如下 Lua 脚本 /tmp/myscript.lua 出现的结果可能不一致

 -- /tmp/myscript.lua if redis.call("exists",KEYS[1]) == 1 then redis.call("incr","mycounter") end if redis.call("exists",KEYS[1]) == 1 then return redis.call("incr","mycounter") end 

具体复现操作可以参考下面的 bash 脚本:

 while [ 1 ] do redis-cli set x foo px 100 > /dev/null sleep 0.092 redis-cli --eval /tmp/myscript.lua x > /dev/null sleep 0.1 redis-cli get mycounter redis-cli -p 6380 get mycounter done 

对于 slave 节点

在 slave 节点上,key 的删除操作由 master 发来的 DEL 执行,因此这里只返回是否过期的结果给客户端,而不执行删除操作

正在从 RDB 和 AOF 读取数据时跳过这个步骤

定期扫描

系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。单独使用这种策略可能出现键已经过期但没有删除的情况
Redis 默认每 100ms 执行一次(通过 hz 参数配置,执行周期为 1s/hz)过期扫描。由于 redisDb 中设置了过期时间的 key 会单独存储,所以不会出现扫描所有 key 的情况
具体步骤由 activeExpireCycle 函数执行

activeExpireCycle、incrementallyRehash 等后台操作都是由 databasesCron 触发的

 void activeExpireCycle(int type) { // ... // 依次遍历各个 db for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { int expired; redisDb *db = server.db+(current_db % server.dbnum); // 记录下一个执行的 db,这样如果因为超时意外退出,下次可以继续从这个 db 开始, // 从而在所有 db 上均匀执行清除操作 current_db++; do { // ... // 跳过没有设置过期时间的 key 等不需要执行的情况 // ... // 抽样个数,默认为 20 if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; // 从设置了过期时间的 key 中随机抽取 20 个 while (num--) { dictEntry *de; long long ttl; // 随机挑选 dict 中的一个 key if ((de = dictGetRandomKey(db->expires)) == NULL) break; ttl = dictGetSignedIntegerVal(de)-now; // 执行删除,具体删除操作和惰性删除中类似 if (activeExpireCycleTryExpire(db,de,now)) expired++; // ... } // ... // 更新统计数据等操作 // ... // 如果每次删除的 key 超过了样本数的 25%,说明过期键占的比例较高,需要再重复执行依次 } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } // ... } 

随机抽样由 dictGetRandomKey 执行

 // src/dict.c /* Return a random entry from the hash table. Useful to * implement randomized algorithms */ dictEntry *dictGetRandomKey(dict *d) { dictEntry *he, *orighe; unsigned long h; int listlen, listele; // 没有数据,返回为 NULL,外层函数接收到 NULL 后会中断过期操作的执行 if (dictSize(d) == 0) return NULL; // 根据 rehashidx 参数判断是否正在执行 rehash,如果正在执行, // 则先执行 rehash 中的一个步骤 if (dictIsRehashing(d)) _dictRehashStep(d); if (dictIsRehashing(d)) { do { // 正在执行 rehash,所以两个 ht 中的对象都要考虑 // // 由于正在执行 rehash,所以可以肯定 ht[0] 中下标小于等于 rehashidx 的 bucket // 肯定没有数据,所以只从 ht[0] 中大于 rehashidx 的 bucket 和 ht[1] 中抽取 h = d->rehashidx + (random() % (d->ht[0].size + d->ht[1].size - d->rehashidx)); he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] : d->ht[0].table[h]; // 取到空 bucket 时重试 } while(he == NULL); } else { do { // 参考写入 ht 时计算下标的规则 hashFunc(key) & sizemake // 这里 random() & sizemask 是随机取一个下标 h = random() & d->ht[0].sizemask; he = d->ht[0].table[h]; // 取到空 bucket 时重试 } while(he == NULL); } // 到这一步 he 是 ht[n] 中某个 bucket 中完整的链表 // 所以还要从这个链表中随机取一个对象 // 遍历计算整个链表的长度 listlen = 0; orighe = he; while(he) { he = he->next; listlen++; } // 随机取链表中某个对象的下标 listele = random() % listlen; he = orighe; // 重新遍历链表获取指定下标的对象 while(listele--) he = he->next; return he; } 

缓存淘汰

配置最大内存限制

在 redis.conf 中配置

redis server 启动时加载配置文件和命令行参数中的 maxmemory ,存入 Server 对象的 maxmemory 字段
main 中在 redis server 启动时执行初始化等操作,其中会执行加载配置文件的 loadServerConfig 函数

 // src/server.c int main(int argc, ch
                
                

-六神源码网