Redis“垃圾”过期死键管理与优化
mhr18 2024-10-24 11:10 31 浏览 0 评论
Redis死键的定义不尽相同,通常有两种:
- 写到Redis里后,由于过期时间过长或者压根没有过期时间,加之长期不访问,这类key可以被称为死键。
- 明明已经过了过期时间,但还占用Redis内存(没有真的删除),这类key也可以被称为死键。
注:本文讨论第二种情况
一、两个例子
下面两个列子中的键值均有过期时间,同时有些键值已过期
1. 对某Redis集群进行全量scan后,键值数和容量的变化:
键值数 | 容量GB | |
扫描前 | 5,628,636,513 | 1116 |
扫描后 | 4,206,662,303 | 798 |
2. 两个同名不同版本的Redis集群的键值数和容量(全部为string类型)
版本键值数容量GBRedis 4.0.14821,131,528831Redis 6.0.15821,131,528433
初步结论:
- scan操作可能会加速Redis的过期键值删除。
- Redis 6版本比Redis 4在同等数据下更节省空间,考虑到Redis成本优化-版本升级-1.SDS优化历史一文中提到4.0~7.0字符串类型在容量上并没有过多优化,因此初步判定Redis 6可能在过期上做过优化
二、基础知识-Redis过期
1. Redis过期数据怎么存?
每个Redis有多个redisDb(但正常只用db0),每个redisDb包含两个dict:dict存key-value、expires存key的过期时间
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
...
} redisDb;
具体如图所示
(1) 正常的dict图:(该图来自《Redis设计与实现》)
(2) 抽象的表现dict和expires表可以使用如下图:(来源于google图片)
再次借个图:expires中key stringobject和dict中的 key stringobject是同一个:
2. Redis过期策略
由于Redis单线程(work thread)的特性,如果精准实时删除每个过期键值,会耗费大量CPU。所以Redis折中实现两种过期删除策略:惰性删除和定期删除。
(1) 惰性删除
客户端执行key相关命令时(以get为例子),首先会去检测key是否在expires表里:
- 如果在expires表里
- 如果已经过期:直接删除,并返回空
- 如果没有过期:从dict表里获取value
- 如果不在expires表里,从dict表里获取value
(2) 定期删除
惰性删除的问题是依赖于主动访问,如果一直不访问,数据将长期保存,造成内存浪费,于是需要添加新的策略:定期删除。
Redis会每100毫秒(如果hz默认是10),会对expires表中已过期数据进行自适应算法删除(具体方法下面会详细介绍)。
三、Redis版本优化
为了更生动的表现Redis 6在死键上的升级,可以做如下实验:写入500万条string,key和value都是16字节,过期时间在1-18秒
版本全部数据过期耗时Redis 4.0.1438262msRedis 6.0.1519267ms
1. Redis 6.0之前:
Redis定期删除过期有两种模式:快模式和慢模式(默认)
注意:
1. 快模式:希望每次定期删除快速结束,防止占用Redis处理正常命令的CPU。
2. 快模式和慢模式在执行过程中自适应的进行转换,本质都是防止占用Redis处理正常命令的CPU。
3. 快模式和慢模式:只是超时时间不同,删除逻辑是一样的
默认进入慢模式:
(1) 循环遍历全部redisDb,随机抽取20个键值,如果发现过期就直接删除。
(2) 判断20个键值中的25%(也就是5个)是否过期
- 如果小于等于25%,则退出当前redisDb的循环,继续下一个redisDb
- 如果大于25%,继续抽取20个键值进行循环,每次判断总的执行时间是否超过25毫秒
- 超过25毫秒,过期进入快模式(超时时间会变短)
- 没超过25毫秒,则退出当前redisDb的循环,继续下一个redisDb
几个重要参数:
#define ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 20 /* 上述的20个键值 */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* 慢模式超时时间:25%的CPU时间 */
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* 快模式超时时间:1ms */
2. Redis 6.0优化
(1) 每次随机-->记录遍历游标
Redis 6.0之前每次执行定期删除都是随机抽取20个键值,如果当前Redis有过期时间的键值数量较多(例如几百万、几千万),那么这个随机会导致很多key不会被扫描到,因此在Redis 6.0中在redisDb加了一个游标(expires_cursor),记录上一次扫描的位置,可以保证最终全部的键值会被扫描到,有效的提升效率。
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
.......
} redisDb;
(2) 判断20个键值中的25%(也就是5个)-->10%(也就是2个)
Before 6.0:
do {
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
while (num--) {
//检测每个key的过期时间,并做相关记录,如果已经过期expired++
}
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
After 6.0,config_cycle_acceptable_stale可调配
do {
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
while (num--) {
//检测每个key的过期时间,并做相关记录,如果已经过期expired++
}
}
} while ((expired*100/sampled) > config_cycle_acceptable_stale);
(3) 添加增强系数
新增active_expire_effort配置,可以适当增强定期删除粒度,它的值范围在1-10。
unsigned long effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
//增加每次扫描key的个数
config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP + ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
//增加快模式的超时时间
config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION + ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
//增加慢模式的超时时间
config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC + 2*effort,
//上述while中的比率
config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE- effort;
四、为什么会有死键、危害是什么
1. 为什么会有死键
(1) 惰性删除:如果很多key不会被二次访问,就会产生死键
(2) 定期删除:如果过期键值生产速度大于定期删除速度
针对(2)有两种情况:
第一种:当前Redis有大量写入同时键值过期时间都很短。
第二种:当前Redis包含大量键值(例如百万级别),但已经过期的数据只占很小的比率,这种相对诡异。
我们还是举个例子:线上某集群
键值数Million容量GB扫描前991.18396.37扫描后814.58296.90
我们可以分析它的键值空间:短过期时间只占很少的比率,它无法自行完成死键的快速过期。
我们回到之前分析的流程图就很容易得出答案:每次遍历redisdb有个核心条件是是否超过25%的键值过期了,从上面的键值分析图可以判断,每次扫描大部分可能就循环一次。
2.死键的危害
危害本质:例如当前集群100GB, 键值如果没有死键只有50G,如果有死键可能就是90GB。
(1) 增加运维次数:业务侧可能会频繁提交扩容。
(2) 浪费成本:
(3) 可能产生逐出:不可预期的使用容量,可能会造成数据逐出(大部分逐出算法都是近似算法,例如lru)
五、如何解决
关于这个比较有意思的是看到官方给出的一个方法是重启,这个对线上环境不太现实(即使有failover)。我们在生产中可以使用如下方法:
1.适度调整active_expire_effort参数(针对Redis 6.0+)
怎么叫适度呢?我们要理解本质上Redis是要对外提供服务的,所以我们必须保证有足够多的CPU时间给正常的命令访问,Redis 4.0之后有个核心指标stat_expired_time_cap_reached_count可以作为参考,其实它就是记录了超时次数,代表在过期删除上投入过多CPU时间。
/* We can't block forever here even if there are many keys to
* expire. So after a given amount of milliseconds return to the
* caller waiting for the other active expire cycle. */
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;
}
}
可以对齐进行监控。
2.定期scan
当识别到某些集群有如下特点,可以借助外力scan(其实就是惰性删除)帮助过期键值数据删除,但是也要力度适度,例如要结合当前Redis的CPU繁忙程度进行sleep时间设定。
3. hz:
这个建议不要乱调整。。hz影响的地方不止这个,所以不要乱调(网上各种文章让调整这个,一定要慎重)
六、如何识别?
说句实话这个问题比较复杂,我也琢磨了挺长时间,可以大概归结成6点(欢迎大佬来喷)
1.expires表要“大”:
需要有一定规模(不然死键问题不存在),一般认为超过100万(但这个不绝对,比如第2中情况)
2.批量生成大量短过期时间的键值:
avg_ttl的例子如下:
键值分析的效果如下:
3.avg_ttl不可靠
avg_ttl是一个近似值,同时它会受到非常长过期时间的干扰(俗称“被平均”),上述中例子就是个典型,avg_ttl是15day,但是确实包含了大量死键
4.利用stat_expired_time_cap_reached_count定位
stat_expired_time_cap_reached_count比较频繁说明过期键值很多,因为已经超时了,可以把全部实例绘图监控或者告警
5.键值分析结合stat_expired_stale_perc指标
stat_expired_stale_perc是total_expired/total_sampled的近似比率,如果偏高说明过期键值很多,如果偏低,需要结合键值分析看是否受到了整体的干扰。
double current_perc;
if (total_sampled) {
current_perc = (double)total_expired/total_sampled;
} else
current_perc = 0;
server.stat_expired_stale_perc = (current_perc*0.05)+
(server.stat_expired_stale_perc*0.95);
6. 终极绝招:scan后给集群打标签
低峰期对每个可疑集群进行清理scan,记录前后键值容量变化,对集群进行标签后,开启定期scan
请控制好redis实例的键值个数
参考Redis开发规范解析(三)--一个Redis最好存多少key
七、最后一个小实验
如何证明expires中的key和dict中的key是同一个。
实验:在Redis 6.0.15插入两组数据
key数量键值过期时间used_memory_human500万string类型,key和value都是16字节不过期598.158MB500万string类型,key和value都是16字节过期时间1天776.599MB
多出:178.44MB
现在对第二组数据执行debug htstats
127.0.0.1:12615> debug HTSTATS 0
[Dictionary HT]
Hash table 0 stats (main hash table):
table size: 8388608
number of elements: 5000000
different slots: 3766504
max chain length: 10
avg chain length (counted): 1.33
avg chain length (computed): 1.33
Chain length distribution:
0: 4622104 (55.10%)
1: 2755189 (32.84%)
2: 820438 (9.78%)
3: 163161 (1.95%)
4: 24475 (0.29%)
5: 2930 (0.03%)
6: 278 (0.00%)
7: 32 (0.00%)
10: 1 (0.00%)
[Expires HT]
Hash table 0 stats (main hash table):
table size: 8388608
number of elements: 5000000
different slots: 3766504
max chain length: 10
avg chain length (counted): 1.33
avg chain length (computed): 1.33
Chain length distribution:
0: 4622104 (55.10%)
1: 2755189 (32.84%)
2: 820438 (9.78%)
3: 163161 (1.95%)
4: 24475 (0.29%)
5: 2930 (0.03%)
6: 278 (0.00%)
7: 32 (0.00%)
10: 1 (0.00%)
下面分别计算:
(1) table size: 8388608,每个需要8个字节指针 = 8388608 * 8 / 1024 / 1024 = 64MB
(2) number of elements: 5000000,刨除dictEntry中的key(这里假设是共享的),value = 16字节(一个int编码的redisobj,为什么是16字节(redis 6),请参考Redis成本优化-版本升级-1.SDS优化历史加上一个8字节next指针,最终折合=5000000 * (16 + 8 ) /1024/1024=114.44MB
最终我们可以验证expires中的key和dict中的key是共享的。
八、几个思考
1.为什么Redis的过期不能立即删除
2.为什么不能用过期事件功能:用事件通知删除。
相关推荐
- 订单超时自动取消业务的 N 种实现方案,从原理到落地全解析
-
在分布式系统架构中,订单超时自动取消机制是保障业务一致性的关键组件。某电商平台曾因超时处理机制缺陷导致日均3000+订单库存锁定异常,直接损失超50万元/天。本文将从技术原理、实现细节、...
- 使用Spring Boot 3开发时,如何选择合适的分布式技术?
-
作为互联网大厂的后端开发人员,当你满怀期待地用上SpringBoot3,准备在项目中大显身手时,却发现一个棘手的问题摆在面前:面对众多分布式技术,究竟该如何选择,才能让SpringBoot...
- 数据库内存爆满怎么办?99%的程序员都踩过这个坑!
-
你的数据库是不是又双叒叕内存爆满了?!服务器监控一片红色警告,老板在群里@所有人,运维同事的电话打爆了手机...这种场景是不是特别熟悉?别慌!作为一个在数据库优化这条路上摸爬滚打了10年的老司机,今天...
- springboot利用Redisson 实现缓存与数据库双写不一致问题
-
使用了Redisson来操作Redis分布式锁,主要功能是从缓存和数据库中获取商品信息,以下是针对并发时更新缓存和数据库带来不一致问题的解决方案1.基于读写锁和删除缓存策略在并发更新场景下,...
- 外贸独立站数据库炸了?对象缓存让你起死回生
-
上周黑五,一个客户眼睁睁看着服务器CPU飙到100%——每次页面加载要查87次数据库。这让我想起2024年Pantheon的测试:Redis缓存能把WooCommerce查询速度提升20倍。跨境电商最...
- 手把手教你在 Spring Boot3 里纯编码实现自定义分布式锁
-
为什么要自己实现分布式锁?你是不是早就受够了引入各种第三方依赖时的繁琐?尤其是分布式锁这块,每次集成Redisson或者Zookeeper,都得额外维护一堆配置,有时候还会因为版本兼容问题头疼半...
- 如何设计一个支持百万级实时数据推送的WebSocket集群架构?
-
面试解答:要设计一个支持百万级实时数据推送的WebSocket集群架构,需从**连接管理、负载均衡、水平扩展、容灾恢复**四个维度切入:连接层设计-**长连接优化**:采用Netty或Und...
- Redis数据结构总结——面试最常问到的知识点
-
Redis作为主流的nosql存储,面试时经常会问到。其主要场景是用作缓存,分布式锁,分布式session,消息队列,发布订阅等等。其存储结构主要有String,List,Set,Hash,Sort...
- skynet服务的缺陷 lua死循环
-
服务端高级架构—云风的skynet这边有一个关于云风skynet的视频推荐给大家观看点击就可以观看了!skynet是一套多人在线游戏的轻量级服务端框架,使用C+Lua开发。skynet的显著优点是,...
- 七年Java开发的一路辛酸史:分享面试京东、阿里、美团后的心得
-
前言我觉得有一个能够找一份大厂的offer的想法,这是很正常的,这并不是我们的饭后谈资而是每个技术人的追求。像阿里、腾讯、美团、字节跳动、京东等等的技术氛围与技术规范度还是要明显优于一些创业型公司...
- mysql mogodb es redis数据库之间的区别
-
1.MySQL应用场景概念:关系型数据库,基于关系模型,使用表和行存储数据。优点:支持ACID事务,数据具有很高的一致性和完整性。缺点:垂直扩展能力有限,需要分库分表等方式扩展。对于复杂的查询和大量的...
- redis,memcached,nginx网络组件
-
1.理解阻塞io,非阻塞io,同步io,异步io的区别2.理解BIO和AIO的区别io多路复用只负责io检测,不负责io操作阻塞io中的write,能写多少是多少,只要写成功就返回,譬如准备写500字...
- SpringBoot+Vue+Redis实现验证码功能
-
一个小时只允许发三次验证码。一次验证码有效期二分钟。SpringBoot整合Redis...
- AWS MemoryDB 可观测最佳实践
-
AWSMemoryDB介绍AmazonMemoryDB是一种完全托管的、内存中数据存储服务,专为需要极低延迟和高吞吐量的应用程序而设计。它与Redis和Memcached相似,但具有更...
- 从0构建大型AI推荐系统:实时化引擎从工具到生态的演进
-
在AI浪潮席卷各行各业的今天,推荐系统正从幕后走向前台,成为用户体验的核心驱动力。本文将带你深入探索一个大型AI推荐系统从零起步的全过程,揭示实时化引擎如何从单一工具演进为复杂生态的关键路径。无论你是...
你 发表评论:
欢迎- 一周热门
-
-
Redis客户端 Jedis 与 Lettuce
-
高并发架构系列:Redis并发竞争key的解决方案详解
-
redis如何防止并发(redis如何防止高并发)
-
Java SE Development Kit 8u441下载地址【windows版本】
-
开源推荐:如何实现的一个高性能 Redis 服务器
-
redis安装与调优部署文档(WinServer)
-
Redis 入门 - 安装最全讲解(Windows、Linux、Docker)
-
一文带你了解 Redis 的发布与订阅的底层原理
-
Redis如何应对并发访问(redis控制并发量)
-
Oracle如何创建用户,表空间(oracle19c创建表空间用户)
-
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (83)
- php redis (97)
- redis 存储 (67)
- redis 锁 (74)
- 启动 redis (73)
- redis 时间 (60)
- redis 删除 (69)
- redis内存 (64)
- redis并发 (53)
- redis 主从 (71)
- redis同步 (53)
- redis结构 (53)
- redis 订阅 (54)
- redis 登录 (62)
- redis 面试 (58)
- redis问题 (54)
- 阿里 redis (67)
- redis的缓存 (57)
- lua redis (59)
- redis 连接池 (64)