【云原生进阶之PaaS中间件】第一章Redis-1.4过期策略

1 设置带过期时间的 key

bash 复制代码
# 时间复杂度:O(1),最常用方式
expire key seconds

# 字符串独有方式
setex(String key, int seconds, String value)

除了string独有设置过期时间的方法,其他类型都需依靠expire方法设置时间,若:

  • 未设置时间,则缓存永不过期
  • 设置过期时间,但之后又想让缓存永不过期,使用persist

设置key的过期时间。超时后,将会自动删除该key。在Redis的术语中一个key的相关超时是volatile的。

生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SETGETSET 命令覆写(overwrite)。这意味着,如果一个命令只是修改(alter)一个带生存时间的 key 的值而不是用一个新的 key 值来代替(replace)它的话,那么生存时间不会被改变。 如使用 INCR 递增key的值,执行 LPUSH 将新值推到 list 中或用 HSET 改变hash的field,这些操作都使超时保持不变。

  • 使用 PERSIST 命令可以清除超时,使其变成一个永久key;
  • 若 key 被 RENAME 命令修改,相关的超时时间会转移到新key;
  • 若 key 被 RENAME 命令修改,比如原来就存在 Key_A,然后调用 RENAME Key_B Key_A 命令,这时不管原来 Key_A 是永久的还是设为超时的,都会由Key_B的有效期状态覆盖;

注意,使用非正超时调用 EXPIRE/PEXPIRE 或具有过去时间的 EXPIREAT/PEXPIREAT 将导致key被删除而不是过期(因此,发出的key事件将是 del,而不是过期)。

1.1 刷新过期时间

对已经有过期时间的key执行EXPIRE操作,将会更新它的过期时间。有很多应用有这种业务场景,例如记录会话的session。

1.2 Redis 之前的 2.1.3 的差异

在 Redis 版本之前 2.1.3 中,使用更改其值的命令更改具有过期集的密钥具有完全删除key的效果。由于现在修复的复制层中存在限制,因此需要此语义。

EXPIRE 将返回 0,并且不会更改具有超时集的键的超时。

1.3 返回值

  • 1,如果成功设置过期时间。
  • 0,如果key不存在或者不能设置过期时间。

1.4 示例

假设有一 Web 服务,对用户最近访问的最新 N 页感兴趣,这样每个相邻页面视图在上一个页面之后不超过 60 秒。从概念上讲,可以将这组页面视图视为用户的导航会话,该会话可能包含有关ta当前正在寻找的产品的有趣信息,以便你可以推荐相关产品。

可使用以下策略轻松在 Redis 中对此模式建模:每次用户执行页面视图时,您都会调用以下命令:

bash 复制代码
MULTI
RPUSH pagewviews.user:<userid> http://.....
EXPIRE pagewviews.user:<userid> 60
EXEC

如果用户空闲超过 60 秒,则将删除该key,并且仅记录差异小于 60 秒的后续页面视图。此模式很容易修改,使用 INCR 而不是使用 RPUSH 的列表。

1.5 带过期时间的 key

通常,创建 Redis 键时没有关联的存活时间。key将永存,除非用户以显式方式(例如 DEL 命令)将其删除。

EXPIRE 族的命令能够将过期项与给定key关联,但代价是该key使用的额外内存。当key具有过期集时,Redis 将确保在经过指定时间时删除该key。

可使用 EXPIRE 和 PERSIST 命令(或其他严格命令)更新或完全删除生存的关键时间。

1.6 过期精度

在 Redis 2.4 中,过期可能不准确,并且可能介于 0 到 1 秒之间。

Redis 2.6,过期误差从 0 到 1 毫秒。

1.7 过期和持久化

过期信息的键存储为绝对 Unix 时间戳(Redis 版本 2.6 或更高版本为毫秒)。这意味着即使 Redis 实例不处于活动状态,时间也在流动。

要使过期工作良好,必须稳定计算机时间。若将 RDB 文件从两台计算机上移动,其时钟中具有大 desync,则可能会发生有趣的事情(如加载时加载到过期的所有key)。

即使运行时的实例,也始终会检查计算机时钟,例如,如果将一个key设置为 1000 秒,然后在将来设置计算机时间 2000 秒,则该key将立即过期,而不是持续 1000 秒。

2 过期策略

Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。你可以想象 Redis 内部有一个死神,时刻盯着所有设置了过期时间的 key,寿命一到就会立即收割。

你还可以进一步站在死神的角度思考,会不会因为同一时间太多的 key 过期,以至于忙不过来。同时因为 Redis 是单线程的,收割的时间也会占用线程的处理时间,如果收割的太过于繁忙,会不会导致线上读写指令出现卡顿。在过期这件事上,Redis 非常小心。

2.1 过期 key 集合

redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理。

2.2 定时扫描策略

Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

  1. 从过期字典中随机 20 个 key;
  2. 删除这 20 个 key 中已经过期的 key;
  3. 如果过期的 key 比率超过 1/4,那就重复步骤 1;

同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。

设想一个大型的 Redis 实例中所有的 key 在同一时间过期了,会出现怎样的结果?

毫无疑问,Redis 会持续扫描过期字典 (循环多次),直到过期字典中过期的 key 变得稀疏,才会停止 (循环次数明显下降)。这就会导致这期间线上读写 QPS 下降明显。还有另外一种原因是内存管理器需要频繁回收内存页,这也会产生一定的 CPU 消耗。

这里解析一下,假如单台 Redis 读写请求 QPS 是 10w,也就是每个请求需要 0.00001s 来完成,每秒执行十次过期扫描,每次过期扫描都达到上限 25ms,那么每秒过期扫描总花费 0.25s,相当于 QPS 降低了 2.5W。

所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。

bash 复制代码
#在目标过期时间上增加一天的随机时间

redis.expire_at(key, random.randint(86400) + expire_ts)

2.3 从库的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在。

2.4 懒惰删除策略

Redis 为什么要懒惰删除(lazy free)?

删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,又或者在使用 FLUSHDB 和 FLUSHALL 删除包含大量键的数据库时,那么删除操作就会导致单线程卡顿。

redis 4.0 引入了 lazyfree 的机制,它可以将删除键或数据库的操作放在后台线程里执行, 从而尽可能地避免服务器阻塞。

2.4.1 unlink指令

unlink 指令,它能对删除操作进行懒处理,丢给后台线程来异步回收内存。

bash 复制代码
unlink key

OK

2.4.2 flush指令

flushdb 和 flushall 指令,用来清空数据库,这也是极其缓慢的操作。Redis 4.0 同样给这两个指令也带来了异步化,在指令后面增加 async 参数就可以将整棵大树连根拔起,扔给后台线程慢慢焚烧。

bash 复制代码
flushall async

OK

2.4.3 异步队列

主线程将对象的引用从「大树」中摘除后,会将这个 key 的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。任务队列被主线程和异步线程同时操作,所以必须是一个线程安全的队列。 不是所有的 unlink 操作都会延后处理,如果对应 key 所占用的内存很小,延后处理就没有必要了,这时候 Redis 会将对应的 key 内存立即回收,跟 del 指令一样。

2.5 惰性删除流程

在进行get或setnx等操作时,先检查key是否过期:

  • 若过期,删除key,然后执行相应操作;
  • 若没过期,直接执行相应操作;

2.5.1 RDB处理过期key

过期key对RDB无影响:

  • 从内存数据库持久化数据到RDB文件,持久化key之前,会检查是否过期,过期的key不进入RDB文件;
  • 从RDB文件恢复数据到内存数据库,数据载入数据库之前,会对Key进行过期检查,若过期,不导入数据库(主库情况);

2.5.2 AOF处理过期Key

过期key对AOF没有任何影响。

2.5.2.1 从内存数据库持久化到AOF文件

  • 当key过期后,还没有被删除,此时进行执行持久化操作(该key不会进入aof文件,因为没有发生修改命令)
  • 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)

2.5.2.2 AOF重写

重写时,会先判断key是否过期,已过期的key不会重写到aof文件

2.5.2.3 在复制链路和 AOF 文件中处理过期的方式

为了在不牺牲一致性的情况下获得正确行为,当key过期时,DEL 操作将同时在 AOF 文件中合成并获取所有附加的从节点。这样,过期的这个处理过程集中到主节点中,还没有一致性错误的可能性。

但是,虽然连接到主节点的从节点不会独立过期key(但会等待来自master的 DEL),但它们仍将使用数据集中现有过期的完整状态,因此,当选择slave作为master时,它将能够独立过期key,完全充当master。

默认每台Redis服务器有16个数据库,默认使用0号数据库,所有的操作都是对0号数据库的操作

bash 复制代码
# 设置数据库数量。默认为16个库,默认使用DB 0,可使用"select 1"来选择一号数据库
# 注意:由于默认使用0号数据库,那么我们所做的所有的缓存操作都存在0号数据库上,
# 当你在1号数据库上去查找的时候,就查不到之前set过的缓存
# 若想将0号数据库上的缓存移动到1号数据库,可以使用"move key 1"
databases 16
  • memcached只是用了惰性删除,而redis同时使用了惰性删除与定期删除,这也是二者的一个不同点(可以看做是redis优于memcached的一点)
  • 对于惰性删除而言,并不是只有获取key的时候才会检查key是否过期,在某些设置key的方法上也会检查(eg.setnx key2 value2:该方法类似于memcached的add方法,如果设置的key2已经存在,那么该方法返回false,什么都不做;如果设置的key2不存在,那么该方法设置缓存key2-value2。假设调用此方法的时候,发现redis中已经存在了key2,但是该key2已经过期了,如果此时不执行删除操作的话,setnx方法将会直接返回false,也就是说此时并没有重新设置key2-value2成功,所以对于一定要在setnx执行之前,对key2进行过期检查)

可是,很多过期key,你没及时去查,定期删除也漏掉了,大量过期key堆积内存,Redis内存殆耗尽!因此内存满时,还需有内存淘汰机制!这就是 Redis 自己 主动删除 数据了!

3 Redis 内存淘汰机制

Redis 数据库可以通过配置文件来配置最大缓存,当写入的数据发现没有足够的内存可用的时候,Redis 会触发内存淘汰机制。Redis 为了满足多样化场景,提供了八种策略,可以在 redis.config 文件中配置:

  1. volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
  4. volatile-lfu:从已设置过期时间的数据集中挑选使用频率最低的数据淘汰
  5. allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰
  6. allkeys-lfu:从所有数据集中挑选使用频率最低的数据淘汰
  7. allkeys-random:从所有数据集中任意选择数据淘汰
  8. noenviction:不回收任何数据,返回一个写操作的错误信息。这也是默认策略

参考链接

Redis 详解_罗志宏的博客-CSDN博客

Redis 的过期策略_redis key过期策略_胖虎·的博客-CSDN博客

Redis键过期策略详解-云社区-华为云

详细聊聊Redis的过期策略_Redis_脚本之家

Redis的过期策略以及内存淘汰机制_redis过期策略以及内存淘汰机制_Felix-Yuan的博客-CSDN博客

Redis 详解_王叮咚的博客-CSDN博客

相关推荐
叫我阿柒啊11 小时前
Java全栈开发面试实战:从基础到微服务的深度探索
java·spring boot·redis·微服务·vue3·全栈开发·面试技巧
切糕师学AI15 小时前
.Net Core Web 架构(管道机制)的底层实现
中间件·系统架构·kestrel·mvc·.netcore·路由·请求管道
赵得C16 小时前
Java 多线程环境下的全局变量缓存实践指南
java·开发语言·后端·spring·缓存
梓沂16 小时前
centos7 安装memcached
数据库·缓存·memcached
切糕师学AI16 小时前
浏览器访问 ASP.NET Core wwwroot 目录下静态资源的底层实现
后端·中间件·kestrel·管道·.net core web
chen_note18 小时前
Redis集群介绍——主从、哨兵、集群
redis·主从模式·集群模式·哨兵模式
玦尘、20 小时前
微服务相关面试题
微服务·云原生·架构
拾忆,想起20 小时前
Redis发布订阅:实时消息系统的极简解决方案
java·开发语言·数据库·redis·后端·缓存·性能优化
小厂永远得不到的男人21 小时前
Redis 入门到精通:从基础到实战的全方位指南
java·redis·后端