前言
Redis 和 MySQL 是面试绕不过的两座大山,他们一个是关系型数据库的代表(MySQL),一个是键值数据库以及缓存中间件的一哥。尤其 Redis 简直是所有互联网公司都在用的技术,比方国内的 BATJ、新浪、360、小米等公司;国外的微软、Twitter、Stack Overflow、GitHub、暴雪等公司。我从业了十几年,就任过 4、5 家公司,有的公司用 MySQL、有的用 SQL Server、甚至还有的用 Oracle 和 DB2,但缓存无一例外应用的都是 Redis,从某种程度上来讲 Redis 是普及率最高的技术,没有之一。
Redis 是如何解决过期数据的?当内存不够用时 Redis 又是如何解决的?
典型答复
咱们在新增 Redis 缓存时能够设置缓存的过期工夫,该工夫保障了数据在规定的工夫内生效,能够借助这个个性来实现很多性能。比方,存储肯定天数的用户(登录)会话信息,这样在肯定范畴内用户不必反复登录了,但为了安全性,须要在肯定工夫之后从新验证用户的信息。因而,咱们能够应用 Redis 设置过期工夫来存储用户的会话信息。
对于曾经过期的数据,Redis 将应用两种策略来删除这些过期键,它们别离是惰性删除和定期删除。
惰性删除是指 Redis 服务器不被动删除过期的键值,而是当拜访键值时,再查看以后的键值是否过期,如果过期则执行删除并返回 null 给客户端;如果没过期则失常返回值信息给客户端。
它的长处是不会节约太多的系统资源,只是在每次拜访时才查看键值是否过期。毛病是删除过期键不及时,造成了肯定的空间节约。
惰性删除的源码位于 src/db.c 文件的 expireIfNeeded 办法中,如下所示:
int expireIfNeeded(redisDb *db, robj *key) {
// 判断键是否过期
if (!keyIsExpired(db,key)) return 0;
if (server.masterhost != NULL) return 1;
/* 删除过期键 */
// 减少过期键个数
server.stat_expiredkeys++;
// 流传键过期的音讯
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
// server.lazyfree_lazy_expire 为 1 示意异步删除,否则则为同步删除
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
// 判断键是否过期
int keyIsExpired(redisDb *db, robj *key) {mstime_t when = getExpire(db,key);
if (when < 0) return 0;
if (server.loading) return 0;
mstime_t now = server.lua_caller ? server.lua_time_start : mstime();
return now > when;
}
// 获取键的过期工夫
long long getExpire(redisDb *db, robj *key) {
dictEntry *de;
if (dictSize(db->expires) == 0 ||
(de = dictFind(db->expires,key->ptr)) == NULL) return -1;
serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
return dictGetSignedIntegerVal(de);
}
惰性删除的执行流程如下图所示:
除了惰性删除之外,Redis 还提供了定期删除性能以补救惰性删除的有余。
定期删除是指 Redis 服务器每隔一段时间会检查一下数据库,看看是否有过期键能够被革除。
默认状况下 Redis 定期检查的频率是每秒扫描 10 次,用于定期革除过期键。当然此值还能够通过配置文件进行设置,在 redis.conf 中批改配置“hz”即可,默认的值为“hz 10”。
小贴士:定期删除的扫描并不是遍历所有的键值对,这样的话比拟费时且太耗费系统资源。Redis 服务器采纳的是随机抽取模式,每次从过期字典中,取出 20 个键进行过期检测,过期字典中存储的是所有设置了过期工夫的键值对。如果这批随机查看的数据中有 25% 的比例过期,那么会再抽取 20 个随机键值进行检测和删除,并且会循环执行这个流程,直到抽取的这批数据中过期键值小于 25%,此次检测才算实现。
定期删除的源码在 expire.c 文件的 activeExpireCycle 办法中,如下所示:
void activeExpireCycle(int type) {
static unsigned int current_db = 0; /* 上次定期删除遍历到的数据库 ID */
static int timelimit_exit = 0;
static long long last_fast_cycle = 0; /* 上次执行定期删除的工夫点 */
int j, iteration = 0;
int dbs_per_call = CRON_DBS_PER_CALL; // 须要遍历数据库的数量
long long start = ustime(), timelimit, elapsed;
if (clientsArePaused()) return;
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {if (!timelimit_exit) return;
// ACTIVE_EXPIRE_CYCLE_FAST_DURATION 疾速定期删除的执行时长
if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
last_fast_cycle = start;
}
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
// 慢速定期删除的执行时长
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; /* 删除操作破费的工夫 */
long total_sampled = 0;
long total_expired = 0;
for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
current_db++;
do {
// .......
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
// 每个数据库中查看的键的数量
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
// 从数据库中随机选取 num 个键进行查看
while (num--) {
dictEntry *de;
long long ttl;
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedInteger
// 过期查看,并对过期键进行删除
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl > 0) {
ttl_sum += ttl;
ttl_samples++;
}
total_sampled++;
}
total_expired += expired;
if (ttl_samples) {
long long avg_ttl = ttl_sum/ttl_samples;
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
}
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
elapsed = ustime()-start;
if (elapsed > timelimit) {
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break;
}
}
/* 判断过期键删除数量是否超过 25% */
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
// .......
}
定期删除的执行流程,如下图所示:
小贴士:Redis 服务器为了保障过期删除策略不会导致线程卡死,会给过期扫描减少了最大执行工夫为 25ms。
以上是 Redis 服务器看待过期键的解决计划,当 Redis 的内存超过最大容许的内存之后,Redis 会触发内存淘汰策略,这和过期策略是齐全不同的两个概念,常常有人把二者搞混,这两者一个是在失常状况下革除过期键,一个是在非正常状况下为了保障 Redis 顺利运行的爱护策略。
当 Redis 内存不够用时,Redis 服务器会依据服务器设置的淘汰策略,删除一些不罕用的数据,以保障 Redis 服务器的顺利运行。
考点剖析
本课时的面试题并非 Redis 的入门级面试题,须要面试者对 Redis 有肯定的理解能力对答如流,并且 Redis 的过期淘汰策略和内存淘汰策略的概念比拟相似,都是用于淘汰数据的。因而很多人会把二者当成一回事,但其实并不是,这个面试者特地留神一下,和此知识点相干的面试题还有以下这些:
- Redis 内存淘汰策略有哪些?
- Redis 有哪些内存淘汰算法?
常识扩大
Redis 内存淘汰策略
咱们能够应用 config get maxmemory-policy 命令,来查看以后 Redis 的内存淘汰策略,示例代码如下:
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
从下面的后果能够看出,以后 Redis 服务器设置的是“noeviction”类型的内存淘汰策略,那么这示意什么含意呢?Redis 又有几种内存淘汰策略呢?
在 4.0 版本之前 Redis 的内存淘汰策略有以下 6 种。
- noeviction:不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,它是 Redis 默认内存淘汰策略。
- allkeys-lru:淘汰整个键值中最久未应用的键值。
- allkeys-random:随机淘汰任意键值。
- volatile-lru:淘汰所有设置了过期工夫的键值中最久未应用的键值。
- volatile-random:随机淘汰设置了过期工夫的任意键值。
- volatile-ttl:优先淘汰更早过期的键值。
能够看出咱们下面示例应用的是 Redis 默认的内存淘汰策略“noeviction”。
而在 Redis 4.0 版本中又新增了 2 种淘汰策略:
- volatile-lfu,淘汰所有设置了过期工夫的键值中起码应用的键值;
- allkeys-lfu,淘汰整个键值中起码应用的键值。
小贴士:从以上内存淘汰策略中能够看出,allkeys-xxx 示意从所有的键值中淘汰数据,而 volatile-xxx 示意从设置了过期键的键值中淘汰数据。
这个内存淘汰策略咱们能够通过配置文件来批改,redis.conf 对应的配置项是“maxmemory-policy noeviction”,只须要把它批改成咱们须要设置的类型即可。
须要留神的是,如果应用批改 redis.conf 的形式,当设置实现之后须要重启 Redis 服务器能力失效。
还有另一种简略的批改内存淘汰策略的形式,咱们能够应用命令行工具输出“config set maxmemory-policy noeviction”来批改内存淘汰的策略,这种批改形式的益处是执行胜利之后就会失效,无需重启 Redis 服务器。但它的害处是不能长久化内存淘汰策略,每次重启 Redis 服务器之后设置的内存淘汰策略就会失落。
Redis 内存淘汰算法
内存淘汰算法次要蕴含两种:LRU 淘汰算法和 LFU 淘汰算法。
LRU(Least Recently Used,最近起码应用)淘汰算法:是一种罕用的页面置换算法,也就是说最久没有应用的缓存将会被淘汰。
LRU 是基于链表构造实现的,链表中的元素依照操作程序从前往后排列,最新操作的键会被挪动到表头,当须要进行内存淘汰时,只须要删除链表尾部的元素即可。
Redis 应用的是一种近似 LRU 算法,目标是为了更好的节约内存,它的实现形式是给现有的数据结构增加一个额定的字段,用于记录此键值的最初一次拜访工夫。Redis 内存淘汰时,会应用随机采样的形式来淘汰数据,它是随机取 5 个值 (此值可配置),而后淘汰最久没有应用的数据。
LFU(Least Frequently Used,最不罕用的)淘汰算法:最不罕用的算法是依据总拜访次数来淘汰数据的,它的核心思想是“如果数据过来被拜访屡次,那么未来被拜访的频率也更高”。
LFU 相对来说比 LRU 更“智能”,因为它解决了应用频率很低的缓存,只是最近被拜访了一次就不会被删除的问题。如果是应用 LRU 相似这种状况数据是不会被删除的,而应用 LFU 的话,这个数据就会被删除。
Redis 内存淘汰策略应用了 LFU 和近 LRU 的淘汰算法,具体应用哪种淘汰算法,要看服务器是如何设置内存淘汰策略的,也就是要看“maxmemory-policy”的值是如何设置的。
总结
本文讲了 Redis 的过期删除策略:惰性删除 + 定期删除;还讲了 Redis 的内存淘汰策略,它和过期策略是齐全不同的两个概念,内存淘汰策略是当内存不够用时才会触发的一种机制,它在 Redis 4.0 之后提供了 8 种内存淘汰策略,这些淘汰策略次要应用了近 LRU 淘汰算法和 LFU 淘汰算法。
本文由 mdnice 多平台公布