在 redis
工做流程中,过时的数据并不须要立刻就要执行删除操做。由于这些删不删除只是一种状态表示,能够异步
的去处理,在不忙的时候去把这些不紧急的删除操做作了,从而保证 redis
的高效redis
在redis中数据的存储不只仅须要保存数据自己还要保存数据的生命周期,也就是过时时间。在redis 中 数据的存储结构以下图:算法
Redis是一种内存级数据库,全部数据均存放在内存中,内存中的数据能够经过TTL指令获取其状态数据库
在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会形成总体redis性能的降低,甚至引起服务器宕机或内存泄漏。缓存
建立一个定时器,当key设置过时时间,且过时时间到达时,由定时器任务当即执行对键的删除操做服务器
节约内存,到时就删除,快速释放掉没必要要的内存占用网络
CPU压力很大,不管CPU此时负载多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量数据结构
用处理器性能换取存储空间并发
数据到达过时时间,不作处理。等下次访问该数据,若是未过时,返回数据。发现已通过期,删除,返回不存在。这样每次读写数据都须要检测数据是否已经到达过时时间。也就是惰性删除老是在数据的读写时发生的。dom
对全部的读写命令进行检查,检查操做的对象是否过时。过时就删除返回过时,不过时就什么也不作~。异步
执行数据写入过程当中,首先经过expireIfNeeded函数对写入的key进行过时判断。
/* * 为执行写入操做而取出键 key 在数据库 db 中的值。 * * 和 lookupKeyRead 不一样,这个函数不会更新服务器的命中/不命中信息。 * * 找到时返回值对象,没找到返回 NULL 。 */ robj *lookupKeyWrite(redisDb *db, robj *key) { // 删除过时键 expireIfNeeded(db,key); // 查找并返回 key 的值对象 return lookupKey(db,key); }
执行数据读取过程当中,首先经过expireIfNeeded函数对写入的key进行过时判断。
/* * 为执行读取操做而取出键 key 在数据库 db 中的值。 * * 并根据是否成功找到值,更新服务器的命中/不命中信息。 * * 找到时返回值对象,没找到返回 NULL 。 */ robj *lookupKeyRead(redisDb *db, robj *key) { robj *val; // 检查 key 释放已通过期 expireIfNeeded(db,key); // 从数据库中取出键的值 val = lookupKey(db,key); // 更新命中/不命中信息 if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; // 返回值 return val; }
执行过时动做expireIfNeeded其实内部作了三件事情,分别是:
/* * 检查 key 是否已通过期,若是是的话,将它从数据库中删除。 * * 返回 0 表示键没有过时时间,或者键未过时。 * * 返回 1 表示键已经由于过时而被删除了。 */ int expireIfNeeded(redisDb *db, robj *key) { // 取出键的过时时间 mstime_t when = getExpire(db,key); mstime_t now; // 没有过时时间 if (when < 0) return 0; /* No expire for this key */ /* Don't expire anything while loading. It will be done later. */ // 若是服务器正在进行载入,那么不进行任何过时检查 if (server.loading) return 0; // 当服务器运行在 replication 模式时 // 附属节点并不主动删除 key // 它只返回一个逻辑上正确的返回值 // 真正的删除操做要等待主节点发来删除命令时才执行 // 从而保证数据的同步 if (server.masterhost != NULL) return now > when; // 运行到这里,表示键带有过时时间,而且服务器为主节点 /* Return when this key has not expired */ // 若是未过时,返回 0 if (now <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; // 向 AOF 文件和附属节点传播过时信息 propagateExpire(db,key); // 发送事件通知 notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED, "expired",key,db->id); // 将过时键从数据库中删除 return dbDelete(db,key); }
判断key是否过时的数据结构是db->expires,也就是经过expires的数据结构判断数据是否过时。
内部获取过时时间并返回。
/* * 返回字典中包含键 key 的节点 * * 找到返回节点,找不到返回 NULL * * T = O(1) */ dictEntry *dictFind(dict *d, const void *key) { dictEntry *he; unsigned int h, idx, table; // 字典(的哈希表)为空 if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */ // 若是条件容许的话,进行单步 rehash if (dictIsRehashing(d)) _dictRehashStep(d); // 计算键的哈希值 h = dictHashKey(d, key); // 在字典的哈希表中查找这个键 // T = O(1) for (table = 0; table <= 1; table++) { // 计算索引值 idx = h & d->ht[table].sizemask; // 遍历给定索引上的链表的全部节点,查找 key he = d->ht[table].table[idx]; // T = O(1) while(he) { if (dictCompareKeys(d, key, he->key)) return he; he = he->next; } // 若是程序遍历完 0 号哈希表,仍然没找到指定的键的节点 // 那么程序会检查字典是否在进行 rehash , // 而后才决定是直接返回 NULL ,仍是继续查找 1 号哈希表 if (!dictIsRehashing(d)) return NULL; } // 进行到这里时,说明两个哈希表都没找到 return NULL; }
节约CPU性能,发现必须删除的时候才删除。
内存压力很大,出现长期占用内存的数据。
用存储空间换取处理器性能
周期性轮询redis库中时效性数据,采用随机抽取的策略,利用过时数据占比的方式删除频度。
CPU性能占用设置有峰值,检测频度可自定义设置
内存压力不是很大,长期占用内存的冷数据会被持续清理
须要周期性抽查存储空间
redis的按期删除是经过定时任务实现的,也就是定时任务会循环调用serverCron
方法。而后定时检查过时数据的方法是databasesCron
。按期删除的一大特色就是考虑了定时删除过时数据会占用cpu时间,因此每次执行databasesCron
的时候会限制cpu的占用不超过25%。真正执行删除的是 activeExpireCycle
方法。
对于持续运行的服务器来讲, 服务器须要按期对自身的资源和状态进行必要的检查和整理, 从而让服务器维持在一个健康稳定的状态, 这类操做被统称为常规操做(cron job)
在 Redis 中, 常规操做由 redis.c/serverCron()
实现, 它主要执行如下操做
1 更新服务器的各种统计信息,好比时间、内存占用、数据库占用状况等。
2 清理数据库中的过时键值对。
3 对不合理的数据库进行大小调整。
4 关闭和清理链接失效的客户端。
5 尝试进行 AOF 或 RDB 持久化操做。
6 若是服务器是主节点的话,对附属节点进行按期同步。
7 若是处于集群模式的话,对集群进行按期同步和链接测试。
由于 serverCron()
须要在 Redis 服务器运行期间一直按期运行, 因此它是一个循环时间事件: serverCron()
会一直按期执行,直到服务器关闭为止。
在 Redis 2.6 版本中, 程序规定 serverCron()
每秒运行 10
次, 平均每 100
毫秒运行一次。 从 Redis 2.8 开始, 用户能够经过修改 hz
选项来调整 serverCron()
的每秒执行次数, 具体信息请参考 redis.conf
文件中关于 hz
选项的说明
way1 : config get hz # "hz" "10" way2 : info server # server.hz 10
serverCron()
会按期的执行,在serverCron()
执行中会调用databasesCron()
方法(serverCron()
还作了其余不少事情,可是如今不讨论,只谈删除策略)
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { // 略去多无关代码 /* We need to do a few operations on clients asynchronously. */ // 检查客户端,关闭超时客户端,并释放客户端多余的缓冲区 clientsCron(); /* Handle background operations on Redis databases. */ // 对数据库执行各类操做 databasesCron(); /* !咱们关注的方法! */
在 databasesCron()
中 调用了 activeExpireCycle()
方法,来对过时的数据进行处理。(在这里还会作一些其余操做~ 调整数据库大小,主动和渐进式rehash)
// 对数据库执行删除过时键,调整大小,以及主动和渐进式 rehash void databasesCron(void) { // 判断是不是主服务器 若是是 执行主动过时键清除 if (server.active_expire_enabled && server.masterhost == NULL) // 清除模式为 CYCLE_SLOW ,这个模式会尽可能多清除过时键 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); // 在没有 BGSAVE 或者 BGREWRITEAOF 执行时,对哈希表进行 rehash if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) { static unsigned int resize_db = 0; static unsigned int rehash_db = 0; unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL; unsigned int j; /* Don't test more DBs than we have. */ // 设定要测试的数据库数量 if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum; /* Resize */ // 调整字典的大小 for (j = 0; j < dbs_per_call; j++) { tryResizeHashTables(resize_db % server.dbnum); resize_db++; } /* Rehash */ // 对字典进行渐进式 rehash if (server.activerehashing) { for (j = 0; j < dbs_per_call; j++) { int work_done = incrementallyRehash(rehash_db % server.dbnum); rehash_db++; if (work_done) { /* If the function did some work, stop here, we'll do * more at the next cron loop. */ break; } } } } }
大体流程以下
1 遍历指定个数的db(默认的 16 )进行删除操做
2 针对每一个db随机获取过时数据每次遍历不超过指定数量(如20),发现过时数据并进行删除。
3 若是有多于25%的keys过时,重复步骤 2
除了主动淘汰的频率外,Redis对每次淘汰任务执行的最大时长也有一个限定,这样保证了每次主动淘汰不会过多阻塞应用请求,如下是这个限定计算公式:
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */ ``... ``timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
也就是每次执行时间的25%用于过时数据删除。
void activeExpireCycle(int type) { // 静态变量,用来累积函数连续执行时的数据 static unsigned int current_db = 0; /* Last DB tested. */ static int timelimit_exit = 0; /* Time limit hit in previous call? */ static long long last_fast_cycle = 0; /* When last fast cycle ran. */ unsigned int j, iteration = 0; // 默认每次处理的数据库数量 unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL; // 函数开始的时间 long long start = ustime(), timelimit; // 快速模式 if (type == ACTIVE_EXPIRE_CYCLE_FAST) { // 若是上次函数没有触发 timelimit_exit ,那么不执行处理 if (!timelimit_exit) return; // 若是距离上次执行未够必定时间,那么不执行处理 if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return; // 运行到这里,说明执行快速处理,记录当前时间 last_fast_cycle = start; } /* * 通常状况下,函数只处理 REDIS_DBCRON_DBS_PER_CALL 个数据库, * 除非: * * 1) 当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL * 2) 若是上次处理遇到了时间上限,那么此次须要对全部数据库进行扫描, * 这能够避免过多的过时键占用空间 */ if (dbs_per_call > server.dbnum || timelimit_exit) dbs_per_call = server.dbnum; // 函数处理的微秒时间上限 // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也便是 25 % 的 CPU 时间 timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; timelimit_exit = 0; if (timelimit <= 0) timelimit = 1; // 若是是运行在快速模式之下 // 那么最多只能运行 FAST_DURATION 微秒 // 默认值为 1000 (微秒) if (type == ACTIVE_EXPIRE_CYCLE_FAST) timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */ // 遍历数据库 for (j = 0; j < dbs_per_call; j++) { int expired; // 指向要处理的数据库 redisDb *db = server.db+(current_db % server.dbnum); // 为 DB 计数器加一,若是进入 do 循环以后由于超时而跳出 // 那么下次会直接从下个 DB 开始处理 current_db++; do { unsigned long num, slots; long long now, ttl_sum; int ttl_samples; /* If there is nothing to expire try next DB ASAP. */ // 获取数据库中带过时时间的键的数量 // 若是该数量为 0 ,直接跳过这个数据库 if ((num = dictSize(db->expires)) == 0) { db->avg_ttl = 0; break; } // 获取数据库中键值对的数量 slots = dictSlots(db->expires); // 当前时间 now = mstime(); // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS) // 跳过,等待字典收缩程序运行 if (num && slots > DICT_HT_INITIAL_SIZE && (num*100/slots < 1)) break; /* * 样本计数器 */ // 已处理过时键计数器 expired = 0; // 键的总 TTL 计数器 ttl_sum = 0; // 总共处理的键计数器 ttl_samples = 0; // 每次最多只能检查 LOOKUPS_PER_LOOP 个键 if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; // 开始遍历数据库 while (num--) { dictEntry *de; long long ttl; // 从 expires 中随机取出一个带过时时间的键 if ((de = dictGetRandomKey(db->expires)) == NULL) break; // 计算 TTL ttl = dictGetSignedIntegerVal(de)-now; // 若是键已通过期,那么删除它,并将 expired 计数器增一 if (activeExpireCycleTryExpire(db,de,now)) expired++; if (ttl < 0) ttl = 0; // 累积键的 TTL ttl_sum += ttl; // 累积处理键的个数 ttl_samples++; } /* Update the average TTL stats for this database. */ // 为这个数据库更新平均 TTL 统计数据 if (ttl_samples) { // 计算当前平均值 long long avg_ttl = ttl_sum/ttl_samples; // 若是这是第一次设置数据库平均 TTL ,那么进行初始化 if (db->avg_ttl == 0) db->avg_ttl = avg_ttl; /* Smooth the value averaging with the previous one. */ // 取数据库的上次平均 TTL 和今次平均 TTL 的平均值 db->avg_ttl = (db->avg_ttl+avg_ttl)/2; } // 咱们不能用太长时间处理过时键, // 因此这个函数执行必定时间以后就要返回 // 更新遍历次数 iteration++; // 每遍历 16 次执行一次 if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */ (ustime()-start) > timelimit) { // 若是遍历次数正好是 16 的倍数 // 而且遍历的时间超过了 timelimit // 那么断开 timelimit_exit timelimit_exit = 1; } // 已经超时了,返回 if (timelimit_exit) return; // 若是已删除的过时键占当前总数据库带过时时间的键数量的 25 % // 那么再也不遍历 } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } }
hz调大将会提升Redis主动淘汰的频率,若是你的Redis存储中包含不少冷数据占用内存过大的话,能够考虑将这个值调大,但Redis做者建议这个值不要超过100。咱们实际线上将这个值调大到100,观察到CPU会增长2%左右,但对冷数据的内存释放速度确实有明显的提升(经过观察keyspace个数和used_memory大小)。
能够看出timelimit和server.hz是一个倒数的关系,也就是说hz配置越大,timelimit就越小。换句话说是每秒钟指望的主动淘汰频率越高,则每次淘汰最长占用时间就越短。这里每秒钟的最长淘汰占用时间是固定的250ms(1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100),而淘汰频率和每次淘汰的最长时间是经过hz参数控制的。
所以当redis中的过时key比率没有超过25%以前,提升hz能够明显提升扫描key的最小个数。假设hz为10,则一秒内最少扫描200个key(一秒调用10次*每次最少随机取出20个key),若是hz改成100,则一秒内最少扫描2000个key;另外一方面,若是过时key比率超过25%,则扫描key的个数无上限,可是cpu时间每秒钟最多占用250ms。
当REDIS运行在主从模式时,只有主结点才会执行上述这两种过时删除策略,而后把删除操做”del key”同步到从结点。
if (server.active_expire_enabled && server.masterhost == NULL) // 判断是不是主节点 从节点不须要执行activeExpireCycle()函数。 // 清除模式为 CYCLE_SLOW ,这个模式会尽可能多清除过时键 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
redis.config.ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 决定每次循环从数据库 expire中随机挑选值的个数
若是不限制 reids 对内存使用的限制,它将会使用所有的内存。能够经过 config.memory
来指定redis 对内存的使用量 。
下面是redis 配置文件中的说明
543 # Set a memory usage limit to the specified amount of bytes. 544 # When the memory limit is reached Redis will try to remove keys 545 # according to the eviction policy selected (see maxmemory-policy). 546 # 547 # If Redis can't remove keys according to the policy, or if the policy is 548 # set to 'noeviction', Redis will start to reply with errors to commands 549 # that would use more memory, like SET, LPUSH, and so on, and will continue 550 # to reply to read-only commands like GET. 551 # 552 # This option is usually useful when using Redis as an LRU or LFU cache, or to 553 # set a hard memory limit for an instance (using the 'noeviction' policy). 554 # 555 # WARNING: If you have replicas attached to an instance with maxmemory on, 556 # the size of the output buffers needed to feed the replicas are subtracted 557 # from the used memory count, so that network problems / resyncs will 558 # not trigger a loop where keys are evicted, and in turn the output 559 # buffer of replicas is full with DELs of keys evicted triggering the deletion 560 # of more keys, and so forth until the database is completely emptied. 561 # 562 # In short... if you have replicas attached it is suggested that you set a lower 563 # limit for maxmemory so that there is some free RAM on the system for replica 564 # output buffers (but this is not needed if the policy is 'noeviction'). 将内存使用限制设置为指定的字节。当已达到内存限制Redis将根据所选的逐出策略(请参阅maxmemory策略)尝试删除数据。 若是Redis没法根据逐出策略移除密钥,或者策略设置为“noeviction”,Redis将开始对使用更多内存的命令(如set、LPUSH等)进行错误回复,并将继续回复只读命令,如GET。 当将Redis用做LRU或LFU缓存或设置实例的硬内存限制(使用“noeviction”策略)时,此选项一般颇有用。 警告:若是将副本附加到启用maxmemory的实例,则将从已用内存计数中减去馈送副本所需的输出缓冲区的大小,这样,网络问题/从新同步将不会触发收回密钥的循环,而副本的输出缓冲区将充满收回的密钥增量,从而触发删除更多键,依此类推,直到数据库彻底清空。 简而言之。。。若是附加了副本,建议您设置maxmemory的下限,以便系统上有一些空闲RAM用于副本输出缓冲区(但若是策略为“noeviction”,则不须要此限制)。
Maxmemery-policy volatile-lru
当前已用内存超过 maxmemory
限定时,触发主动清理策略
volatile-lru:只对设置了过时时间的key进行LRU(默认值)
volatile-random:随机删除即将过时key
volatile-ttl : 删除即将过时的
volatile-lfu:挑选最近使用次数最少的数据淘汰
allkeys-lru : 删除lru算法的key
allkeys-lfu:挑选最近使用次数最少的数据淘汰
allkeys-random:随机删除
(Redis 4.0 默认策略)
noeviction : 永不过时,返回错误当mem_used内存已经超过maxmemory的设定,对于全部的读写请求都会触发redis.c/freeMemoryIfNeeded(void)
函数以清理超出的内存。注意这个清理过程是阻塞的,直到清理出足够的内存空间。因此若是在达到maxmemory而且调用方还在不断写入的状况下,可能会反复触发主动清理策略,致使请求会有必定的延迟。
清理时会根据用户配置的maxmemory-policy来作适当的清理(通常是LRU或TTL),这里的LRU或TTL策略并非针对redis的全部key,而是以配置文件中的maxmemory-samples个key做为样本池进行抽样清理。
maxmemory-samples在redis-3.0.0中的默认配置为5,若是增长,会提升LRU或TTL的精准度,redis做者测试的结果是当这个配置为10时已经很是接近全量LRU的精准度了,而且增长maxmemory-samples会致使在主动清理时消耗更多的CPU时间,建议:
1 尽可能不要触发maxmemory,最好在mem_used内存占用达到maxmemory的必定比例后,须要考虑调大hz以加快淘汰,或者进行集群扩容。
2 若是可以控制住内存,则能够不用修改maxmemory-samples配置;若是Redis自己就做为LRU cache服务(这种服务通常长时间处于maxmemory状态,由Redis自动作LRU淘汰),能够适当调大maxmemory-samples。
这里提一句,实际上redis根本就不会准确的将整个数据库中最久未被使用的键删除,而是每次从数据库中随机取5个键并删除这5个键里最久未被使用的键。上面提到的全部的随机的操做实际上都是这样的,这个5能够用过redis的配置文件中的maxmemeory-samples参数配置。
使用INFO命令输出监控信息,查询缓存int和miss的次数,根据业务需求调优Redis配置。