强可用的Redis多线程实现高效的过期管理(redis过期 多线程)
在大规模分布式系统中,Redis已经成为了最流行的缓存和数据存储方案之一。然而,为了保证Redis系统的高可用性,运维人员需要采取一系列措施,其中最重要的就是对Redis键的过期时间进行管理。
传统的Redis采用单线程方式对键的过期时间进行管理,这种方式虽然保证了数据一致性,但是在高并发场景下会造成性能瓶颈。为了解决这个问题,Redis 4.0引入了多线程方式实现过期管理,大大提升了Redis的性能。
多线程方式实现高效的过期管理
Redis的多线程方式实现过期管理的原理是将Redis键值对按照过期时间进行排序,将快要过期的数据优先淘汰。Redis将过期时间轮(Expire wheel)划分为多个时间槽,每个时间槽对应一个过期时间,Redis在每个时间槽中维护一个链表结构,存储在这个时间槽内过期的键值对。Redis通过多个线程定时移动时间槽,将过期时间较短的键值对优先淘汰,从而达到高效的过期管理。
下面是Redis多线程方式实现过期管理的代码:
#define REDIS_EXPIRELOOKUPS_PER_LOOP 20
void activeExpireCycle(int type) { /* 定义时钟的处理周期 */
long long start = ustime(), timelimit_exit = 0, timelimit_us = 0; unsigned int expired = 0;
if (server.masterhost && server.repl_slave_ro == 1) return;
/* 如果Redis是开启写保护的,直接返回 */ if (server.activerehashing) return;
if (type == ACTIVE_EXPIRE_CYCLE_FAST) timelimit_us = 1000;
else timelimit_us = server.maxmemory_samples ? 0 : 100000;
/* 如果Redis有设置最大循环时间,则使用它 */ if (server.maxmemory_samples != 0) {
timelimit_exit = start + (1000000/server.hz)*server.maxmemory_samples; }
/* 停止keys迭代器 */ signalFlushedDb(-1);
while(1) { long long now;
int j;
/* 如果达到了我们的定义的过期处理扫描次数,需要退出循环。 */ if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit_exit = start + 1000; if (server.maxmemory_samples != 0) {
if (ustime() > timelimit_exit) break; }
/* 记录当前的时间戳 */ now = mstime();
/* 减少计数器的值 */ if (server.lazyfree_lazy_expire) lazyfreeTryExpire(now);
/* 需要分配候选键数组,该数组用来记录到期的键 */ if (expired_entries_pool_size
populateExpiredArray();
/* 遍历所有哈希表,查找到期键 */ for (j = 0; j
int expired_this_loop = 0;
/* 获得当前数据库 */ redisDb *db = server.db+j;
/* 因为执行过期操作时,需要修改键而且Redis是事件驱动的, 如果在过期操作时有其它操作正在进行,可能会产生冲突
所以,该库必须被标记 */ if (dictSize(db->expires)) {
dictEntry *de; de = dictFind(db->expires,dictGetSomeKeys(db->expires));
if (de) { long long t = dictGetSignedIntegerVal(de)-now;
/* 如果该键已经过期,将其淘汰 */ if (t
flushdbAsync(j); expired+=dictSize(db->expires);
continue; }
/* 计算出该时间槽所属的槽位 */ int i = (t/1000)
(t/1000) : (REDIS_EXPIRELOOKUPS_PER_LOOP-1); /* 将该键存储到该时间槽所属的链表中 */
expired_candidates[i][expired_count[i]] = de; expired_count[i]++;
} }
}
/* 遍历所有的时隙,逐个处理过期键值 */ for (j = 0; j
dictEntry *de; if (expired_count[j] == 0) continue;
/* 遍历当前时间槽的所有键值对 */ while(expired_count[j]--) {
de = expired_candidates[j][expired_count[j]]; dictDelete(server.db[de->v.val].dict,dictGetKey(de));
dictDelete(server.db[de->v.val].expires,dictGetKey(de)); notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",dictGetKey(de), dictGetKeyLength(de));
decrRefCount(dictGetKey(de)); server.dirty++;
if (++expired >= REDIS_EXPIREDLOOKUPS_PER_CRON) break; }
/* 如果达到最大过期键扫描次数,需要退出循环 */ if (expired >= REDIS_EXPIREDLOOKUPS_PER_CRON) {
goto end; }
} }
end: /* 重置过期键候选数组 */
expired_entries_pool_size = 0; memset(expired_candidates,0,sizeof(expired_candidates));
memset(expired_count,0,sizeof(expired_count));
/* 重置keys迭代器 */ signalFlushedDb(-1);
}
如上面的代码所示,Redis在以上循环中处理过期键值对。具体而言,Redis将键值对存储在过期时间槽所属的链表中,每次清理的是快要过期的键,避免因为并发性能瓶颈而使Redis出现性能问题。
结语
采用多线程方式实现高效的过期管理方式是实现Redis高可用性的关键之一。通过优化Redis的过期管理方式,我们可以更好地保证Redis的性能和稳定性,为用户带来更好的使用体验。