Redis的缓存策略和主键失效机制

简介:

作为缓存系统都要定期清理无效数据,就需要一个主键失效和淘汰策略。

>>EXPIRE主键失效机制

在Redis当中,有生存期的key被称为volatile,
在创建缓存时,要为给定的key设置生存期,当key过期的时候(生存期为0),它可能会被删除。

(1)影响生存时间的一些操作

生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 和 GETSET 命令覆盖原来的数据,

也就是说,修改key对应的value和使用另外相同的key和value来覆盖以后,当前数据的生存时间不同。

比如说,对一个 key 执行INCR命令,对一个列表进行LPUSH命令,或者对一个哈希表执行HSET命令,这类操作都不会修改 key 本身的生存时间。

另一方面,如果使用RENAME对一个 key 进行改名,那么改名后的 key 的生存时间和改名前一样。

RENAME命令的另一种可能是,尝试将一个带生存时间的 key 改名成另一个带生存时间的 another_key ,这时旧的 another_key (以及它的生存时间)会被删除,然后旧的 key 会改名为 another_key ,因此,新的 another_key 的生存时间也和原本的 key 一样。
使用PERSIST命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个persistent key 。

(2)如何更新生存时间

可以对一个已经带有生存时间的 key 执行EXPIRE命令,新指定的生存时间会取代旧的生存时间。
过期时间的精度已经被控制在1ms之内,主键失效的时间复杂度是O(1),
EXPIRE和TTL命令搭配使用,TTL可以查看key的当前生存时间
设置成功返回 1;当 key 不存在或者不能为 key 设置生存时间时,返回 0 。

>>最大缓存配置

在 redis 中,允许用户设置最大使用内存大小

1
server.maxmemory

默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。
redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。

redis 提供 6种数据淘汰策略:

volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据

注意这里的6种机制,volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,
后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。

使用策略规则:

(1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru。
(2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random。

三种数据淘汰策略:

ttl和random比较容易理解,实现也会比较简单。主要是Lru最近最少使用淘汰策略,设计上会对key 按失效时间排序,然后取最先失效的key进行淘汰。

>>失效的内部实现

Redis 删除失效主键的方法主要有两种:

消极方法(passive way),在主键被访问时如果发现它已经失效,那么就删除它
积极方法(active way),周期性地从设置了失效时间的主键中选择一部分失效的主键删除
主键具体的失效时间全部都维护在expires这个字典表中。

1
2
3
4
5
6
7
8
typedef struct redisDb {
     dict *dict;  //key-value
     dict *expires;   //维护过期key
     dict *blocking_keys;
     dict *ready_keys;
     dict *watched_keys;
     int  id;
} redisDb;

 

(1)passive way 消极方法

在passive way 中, redis在实现GET、MGET、HGET、LRANGE等所有涉及到读取数据的命令时都会调用 expireIfNeeded,它存在的意义就是在读取数据之前先检查一下它有没有失效,如果失效了就删除它。
expireIfNeeded函数中调用的另外一个函数propagateExpire,这个函数用来在正式删除失效主键之前广播这个主键已经失效的信息,这个信息会传播到两个目的地:
一个是发送到AOF文件,将删除失效主键的这一操作以DEL Key的标准命令格式记录下来;
另一个就是发送到当前Redis服务器的所有Slave,同样将删除失效主键的这一操作以DEL Key的标准命令格式告知这些Slave删除各自的失效主键。从中我们可以知道,所有作为Slave来运行的Redis服务器并不需要通过消极方法来删除失效主键,它们只需要执行Master的删除指令即可。

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
31
32
33
34
35
36
37
38
39
40
41
int  expireIfNeeded(redisDb *db, robj *key) {
    // 获取主键的失效时间
     long  long  when = getExpire(db,key);
     //假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1),直接返回0
     if  (when < 0)  return  0;
    // 假如Redis服务器正在从RDB文件中加载数据,暂时不进行失效主键的删除,直接返回0
     if  (server.loading)  return  0;
    // 假如当前的Redis服务器是作为Slave运行的,那么不进行失效主键的删除,因为Slave
   //  上失效主键的删除是由Master来控制的,但是这里会将主键的失效时间与当前时间进行
    // 一下对比,以告知调用者指定的主键是否已经失效了
     if  (server.masterhost != NULL) {
         return  mstime() > when;
     }
     //如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的主键
    // 还未失效就直接返回0
     if  (mstime() <= when)  return  0;
    // 如果发现主键确实已经失效了,那么首先更新关于失效主键的统计个数,然后将该主键失
    // 效的信息进行广播,最后将该主键从数据库中删除
     server.stat_expiredkeys++;
     propagateExpire(db,key);
     return  dbDelete(db,key);
}
 
void  propagateExpire(redisDb *db, robj *key) {
     robj *argv[2];
    // shared.del是在Redis服务器启动之初就已经初始化好的一个常用Redis对象,即DEL命令
     argv[0] = shared.del;
     argv[1] = key;
     incrRefCount(argv[0]);
     incrRefCount(argv[1]);
   //  检查Redis服务器是否开启了AOF,如果开启了就为失效主键记录一条DEL日志
     if  (server.aof_state != REDIS_AOF_OFF)
         feedAppendOnlyFile(server.delCommand,db->id,argv,2);
     //检查Redis服务器是否拥有Slave,如果是就向所有Slave发送DEL失效主键的命令,这就是
    // 上面expireIfNeeded函数中发现自己是Slave时无需主动删除失效主键的原因了,因为它
   //  只需听从Master发送过来的命令就OK了
     if  (listLength(server.slaves))
         replicationFeedSlaves(server.slaves,db->id,argv,2);
     decrRefCount(argv[0]);
     decrRefCount(argv[1]);
}

 

(2)Active Way 积极方法

消极方法的缺点是,如果key 迟迟不被访问,就会占用很多内存空间,所以就出现了积极的方式(Active Way),

此方法利用了redis的时间事件,即每隔一段时间就中断一下完成一些指定操作,其中就包括检查并删除失效主键。

A.时间事件

创建时间事件, 回调函数就是serverCron,它在Redis服务器启动时创建,每秒的执行次数由宏定义REDIS_DEFAULT_HZ来指定,默认每秒钟执行10次。

1
2
3
4
5
//该代码在redis.c文件的initServer函数中。实际上,serverCron这个回调函数不仅要进行失效主键的检查与删除,还要进行统计信息的更新、客户端连接超时的控制、BGSAVE和AOF的触发等等,这里我们仅关注删除失效主键的实现,也就是函数activeExpireCycle。
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
         redisPanic( "create time event failed" );
         exit (1);
}

B.使用activeExpireCycle 清除失效key

其实现原理是从Redis中每个数据库的expires字典表中,随机抽样REDIS_EXPIRELOOKUPS_PER_CRON(默认值为10)个设置了失效时间的主键,检查它们是否已经失效并删除掉失效的主键,如果失效主键个数占本次抽样个数的比例超过25%,它会继续进行下一轮的随机抽样和删除,直到刚才的比例低于25%才停止对当前数据库的处理,转向下一个数据库。

注意,activeExpireCycle函数不会试图一次性处理Redis中的所有数据库,而是最多只处理REDIS_DBCRON_DBS_PER_CALL(默认值为16),此外activeExpireCycle函数还有处理时间上的限制,不是想执行多久就执行多久,凡此种种都只有一个目的,那就是避免失效主键删除占用过多的CPU资源。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
void  activeExpireCycle( void ) {
     /*因为每次调用activeExpireCycle函数不会一次性检查所有Redis数据库,所以需要记录下
         每次函数调用处理的最后一个Redis数据库的编号,这样下次调用activeExpireCycle函数
         还可以从这个数据库开始继续处理,这就是current_db被声明为static的原因,而另外一
         个变量timelimit_exit是为了记录上一次调用activeExpireCycle函数的执行时间是否达
         到时间限制了,所以也需要声明为static
     */
     static  unsigned  int  current_db = 0;
     static  int  timelimit_exit = 0;
     unsigned  int  j, iteration = 0;
 
     /**
         每次调用activeExpireCycle函数处理的Redis数据库个数为REDIS_DBCRON_DBS_PER_CALL
         unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
         long long start = ustime(), timelimit;
         如果当前Redis服务器中的数据库个数小于REDIS_DBCRON_DBS_PER_CALL,则处理全部数据库,
         如果上一次调用activeExpireCycle函数的执行时间达到了时间限制,说明失效主键较多,也
         会选择处理全部数据库
     */
     if  (dbs_per_call > server.dbnum || timelimit_exit)
         dbs_per_call = server.dbnum;
 
     /*
         执行activeExpireCycle函数的最长时间(以微秒计),其中REDIS_EXPIRELOOKUPS_TIME_PERC
         是单位时间内能够分配给activeExpireCycle函数执行的CPU时间比例,默认值为25,server.hz
         即为一秒内activeExpireCycle的调用次数,所以这个计算公式更明白的写法应该是这样的,即
             (1000000 * (REDIS_EXPIRELOOKUPS_TIME_PERC / 100)) / server.hz
     */
     timelimit = 1000000*REDIS_EXPIRELOOKUPS_TIME_PERC/server.hz/100;
     timelimit_exit = 0;
     if  (timelimit <= 0) timelimit = 1;
 
     //遍历处理每个Redis数据库中的失效数据
     for  (j = 0; j < dbs_per_call; j++) {
         int  expired;
         redisDb *db = server.db+(current_db % server.dbnum);
       // 此处立刻就将current_db加一,这样可以保证即使这次无法在时间限制内删除完所有当前
       // 数据库中的失效主键,下一次调用activeExpireCycle一样会从下一个数据库开始处理,
        //从而保证每个数据库都有被处理的机会
         current_db++;
        // 开始处理当前数据库中的失效主键
         do  {
             unsigned  long  num, slots;
             long  long  now;
            // 如果expires字典表大小为0,说明该数据库中没有设置失效时间的主键,直接检查下
           // 一数据库
             if  ((num = dictSize(db->expires)) == 0)  break ;
             slots = dictSlots(db->expires);
             now = mstime();
           //  如果expires字典表不为空,但是其填充率不足1%,那么随机选择主键进行检查的代价
            //会很高,所以这里直接检查下一数据库
             if  (num && slots > DICT_HT_INITIAL_SIZE &&
                 (num*100/slots < 1))  break ;
             expired = 0;
             //如果expires字典表中的entry个数不足以达到抽样个数,则选择全部key作为抽样样本
             if  (num > REDIS_EXPIRELOOKUPS_PER_CRON)
                 num = REDIS_EXPIRELOOKUPS_PER_CRON;
             while  (num--) {
                 dictEntry *de;
                 long  long  t;
               //  随机获取一个设置了失效时间的主键,检查其是否已经失效
                 if  ((de = dictGetRandomKey(db->expires)) == NULL)  break ;
                 t = dictGetSignedIntegerVal(de);
                 if  (now > t) {
            // 发现该主键确实已经失效,删除该主键
                     sds key = dictGetKey(de);
                     robj *keyobj = createStringObject(key,sdslen(key));
                     //同样要在删除前广播该主键的失效信息
                     propagateExpire(db,keyobj);
                     dbDelete(db,keyobj);
                     decrRefCount(keyobj);
                     expired++;
                     server.stat_expiredkeys++;
                 }
             }
            // 每进行一次抽样删除后对iteration加一,每16次抽样删除后检查本次执行时间是否
           // 已经达到时间限制,如果已达到时间限制,则记录本次执行达到时间限制并退出
             iteration++;
             if  ((iteration & 0xf) == 0 &&
                 (ustime()-start) > timelimit)
             {
                 timelimit_exit = 1;
                 return ;
             }
         //如果失效的主键数占抽样数的百分比大于25%,则继续抽样删除过程
         while  (expired > REDIS_EXPIRELOOKUPS_PER_CRON/4);
     }
}

  

>>Redis 的主键失效机制对系统性能的影响

Redis 会定期地检查设置了失效时间的主键并删除已经失效的主键,但是通过对每次处理数据库个数的限制、activeExpireCycle 函数在一秒钟内执行次数的限制、分配给 activeExpireCycle 函数CPU时间的限制、继续删除主键的失效主键数百分比的限制,Redis 已经大大降低了主键失效机制对系统整体性能的影响,但是如果在实际应用中出现大量主键在短时间内同时失效的情况还是会产生很多问题,
也就是缓存穿透的情况。

>>如何避免大量主键在同一时间同时失效造成数据库压力过大

合理的配置缓存可以增加系统的健壮性,避免缓存失效造成的事故。
1.在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2.可以通过缓存reload机制,预先去更新缓存.
2.不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
3.做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

>>Memcached删除失效主键的方法与Redis有何异同?

Memcached 在删除失效主键时采用的消极方法,即 Memcached 内部不会监视主键是否失效,而是在通过 Get 访问主键时才会检查其是否已经失效。
其次,Memcached 与 Redis 在主键失效机制上的最大不同是,Memcached 不会像 Redis 那样真正地去删除失效的主键,而只是简单地将失效主键占用的空间回收。

这样当有新的数据写入到系统中时,Memcached 会优先使用那些失效主键的空间。
如果失效主键的空间用光了,Memcached 还可以通过 LRU 机制来回收那些长期得不到访问的空间,因此 Memcached 并不需要像 Redis 中那样的周期性删除操作,这也是由 Memcached 使用的内存管理机制决定的。
同时, Redis 在出现 OOM时同样可以通过配置 maxmemory-policy 这个参数来决定是否采用 LRU 机制来回收内存空间。


目录
相关文章
|
7月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
2月前
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
|
3月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
215 1
Redis专题-实战篇二-商户查询缓存
|
2月前
|
缓存 运维 监控
Redis 7.0 高性能缓存架构设计与优化
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Redis 7.0高性能缓存架构,探索函数化编程、多层缓存、集群优化与分片消息系统,用代码在二进制星河中谱写极客诗篇。
|
3月前
|
存储 缓存 NoSQL
工作 10 年!Redis 内存淘汰策略 LRU 和传统 LRU 差异,还傻傻分不清
小富带你深入解析Redis内存淘汰机制:LRU与LFU算法原理、实现方式及核心区别。揭秘Redis为何采用“近似LRU”,LFU如何解决频率老化问题,并结合实际场景教你如何选择合适策略,提升缓存命中率。
430 4
|
3月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
4月前
|
存储 缓存 人工智能
Redis六大常见命令详解:从set/get到过期策略的全方位解析
本文将通过结构化学习路径,帮助读者实现从命令语法掌握到工程化实践落地的能力跃迁,系统性提升 Redis 技术栈的应用水平。
|
4月前
|
存储 NoSQL 算法
应对Redis中的并发冲突:有效解决策略
以上策略各有优劣:乐观锁和悲观锁控制得当时可以很好地解决并发问题;发布/订阅模式提高了实时响应能力;Lua脚本和Redis事务保证了命令序列的原子性;分布式锁适合跨节点的并发控制;限流措施和持久化配置从系统设计层面减少并发风险;数据分片通过架构上的优化减轻单个Redis节点的负担。正确选择适合自己应用场景的策略,是解决Redis并发冲突的关键。
298 0
|
6月前
|
存储 监控 NoSQL
流量洪峰应对术:Redis持久化策略与内存压测避坑指南
本文深入解析Redis持久化策略与内存优化技巧,涵盖RDB快照机制、AOF重写原理及混合持久化实践。通过实测数据揭示bgsave内存翻倍风险、Hash结构内存节省方案,并提供高并发场景下的主从复制冲突解决策略。结合压测工具链构建与故障恢复演练,总结出生产环境最佳实践清单。
199 9
|
6月前
|
缓存 负载均衡 网络协议
电商API接口性能优化技术揭秘:缓存策略与负载均衡详解
电商API接口性能优化是提升系统稳定性和用户体验的关键。本文聚焦缓存策略与负载均衡两大核心,详解其在电商业务中的实践。缓存策略涵盖本地、分布式及CDN缓存,通过全量或部分缓存设计和一致性维护,减少后端压力;负载均衡则利用反向代理、DNS轮询等技术,结合动态调整与冗余部署,提高吞吐量与可用性。文中引用大型及跨境电商平台案例,展示优化效果,强调持续监控与迭代的重要性,为电商企业提供了切实可行的性能优化路径。