REDIS 7 · EXPIRE · LAZY · ACTIVE EXPIRE CYCLE · MAXMEMORY · LRU · LFU
到点的 key 不会准时消失,内存满了要挑人赶走
这是两件不同的事。过期删除管的是“设了过期时间的 key,到点后什么时候真正删掉”;内存淘汰管的是“内存用到上限了,还要写,该把谁赶出去”。前者靠查到时顺手删 + 定期巡逻,后者靠 maxmemory 和淘汰策略。
expires单独一张哈希表,记着每个设了过期的 key 什么时候到期。
key → 毫秒时间戳activeExpireCycle每秒 10 次,随机抽 20 个带过期时间的 key,到期的删掉。
最多占 25% 的 CPU 时间maxmemoryRedis 能用的内存上限,默认 0 表示不限。
超了才会淘汰eviction pool · 16每次抽几个 key 放进池子,挑最该走的那个淘汰。
近似 LRU / LFU 靠它过期时间存在哪
每个库有两张表:主表存 key 和值;过期表只存设了过期时间的 key,以及它到期的绝对时间(毫秒)。判断一个 key 过没过期,就是拿当前时间和过期表里的值比一下。
SET code:138xx 4721 EX 300 # 300 秒后过期;PX 是毫秒,EXAT / PXAT 给绝对时间
EXPIRE session:9f 1800 # 给已有的 key 设过期;7.0 起可加 NX / XX / GT / LT
TTL code:138xx # 还剩多少秒;-1 = 没设过期,-2 = key 不存在
PERSIST session:9f # 去掉过期时间
SET code:138xx 9999 KEEPTTL # 改值但保留原来的过期时间(6.0 起);不加 KEEPTTL 会把过期时间清掉
到期了,什么时候真删
有三种思路。Redis 用的是后两种的组合:读到时顺手删,再派个巡逻员定期抽查,把没人读的那些也清掉。
定时删除
Redis 不用每个 key 设一个定时器,一到点就删。
内存最干净;但几百万个定时器,CPU 扛不住。惰性删除
Redis 用平时不管。有人读写这个 key 时,先看一眼过期表,过期了就删掉,当作不存在。
几乎不花 CPU;但没人访问的过期 key 会一直占着内存。定期删除
Redis 用后台定期随机抽查一批带过期时间的 key,删掉到期的。
兜住没人访问的那些;抽查也要花 CPU,所以限时、限量。hz 10 决定,在定时任务 serverCron 里跑此外,每次事件循环进入等待前,还会跑一次“快速巡逻”,最多 1 毫秒。active-expire-effort(1~10,默认 1)调大,每轮抽得更多、容忍的过期比例更低,代价是多花 CPU。
巡逻实验台:没人读的过期 key 会不会一直赖着
400 个 key,其中 300 个设了过期时间。只有前 80 个是热门 key 会被读到,其余没人碰。打开或关掉两种删除方式,看“过期了还占着内存”的 key 有多少。
前 4 行(80 个)是热门 key,每 0.1 秒随机被读 3 次。
过期碰上主从复制和持久化
为了让主从数据一致,删过期 key 这件事只由主节点做,删完再通知从节点和 AOF。
| 场景 | 怎么处理 |
|---|---|
| 主节点删了过期 key | 往复制流和 AOF 里补一条 DEL(或 UNLINK),从节点和 AOF 跟着删 |
| 从节点上读到过期 key | 从节点自己不删,等主节点的 DEL;但 3.2 起读的时候会按过期处理,返回空 |
| 生成 RDB、AOF 重写 | 已过期的 key 通常会被跳过;就算写进去了,主节点加载时也会再过滤一遍 |
| 主节点加载 RDB | 已过期的跳过;从节点加载时全部载入,等主节点来删 |
复制和持久化的细节见 Redis 第 03 篇 和 第 02 篇。
内存满了:八种淘汰策略
设了 maxmemory 后,每次执行写命令前,Redis 检查已用内存。超了,就按 maxmemory-policy 挑 key 删,直到降回上限以下。两个维度:从哪些 key 里挑,按什么标准挑。
allkeys-lru最常用的缓存配置allkeys-lfu有明显热点时更好allkeys-random访问均匀时—没设过期就没有 TTLvolatile-lru没设过期的 key 绝不动volatile-lfu同上,按频率volatile-random同上,随机volatile-ttl剩余时间最短的先走noeviction默认策略。不淘汰,写命令直接报错:OOM command not allowed when used memory > 'maxmemory'.读和删除照常。volatile-* 策略找不到可以淘汰的 key 时(比如大部分 key 都没设过期),效果和 noeviction 一样,写入报错。
近似 LRU:不排队,抽样挑
真正的 LRU 要把所有 key 按访问时间串成链表,每次访问都要挪位置,几千万个 key 太费内存和 CPU。Redis 只在 redisObject 里用 24 位记一个秒级的访问时间,淘汰时随机抽几个 key(maxmemory-samples,默认 5),放进一个能装 16 个候选的池子,挑空闲最久的那个淘汰。
淘汰得准不准:1000 个老 key,再写 500 个新 key
每写入一个新 key 就超出上限,淘汰一个。理想情况是最老的 500 个全走。抽样数越大越接近真正的 LRU,但每次淘汰花的 CPU 也越多。
LFU:看用得多不多,而不只是最近用没用
Redis 4.0 起有 LFU。它用同样的 24 位:高 16 位记上次衰减的时间(分钟),低 8 位是一个最大 255 的对数计数器。
新 key 从 5 开始,免得刚写进来就被淘汰。每次访问以一定概率加 1:计数越大,越难再加(lfu-log-factor,默认 10)。一段时间没访问就往下减(lfu-decay-time,默认每 1 分钟减 1)。OBJECT FREQ key 可以看计数。
| lfu-log-factor | 100 次 | 1000 次 | 10 万次 | 100 万次 |
|---|---|---|---|---|
| 0 | 104 | 255 | 255 | 255 |
| 1 | 18 | 49 | 255 | 255 |
| 10(默认) | 10 | 18 | 142 | 255 |
| 100 | 8 | 11 | 49 | 143 |
表里是访问多少次后计数器大约到几,数据来自 redis.conf 里的注释。
怎么选、怎么配
先想清楚 Redis 里的数据丢了要不要紧,再选策略。
| 用法 | 策略 | 原因 |
|---|---|---|
| 纯缓存 | allkeys-lru 或 allkeys-lfu | 丢了能从数据库补,满了就把用得少的挤掉;热点明显用 LFU |
| 缓存和重要数据混放 | volatile-lru / volatile-lfu | 缓存设过期时间,重要数据不设,淘汰只动缓存;更好的做法是分开两个实例 |
| 队列、计数、分布式锁 | noeviction | 宁可写不进去报错,也不能悄悄丢;配好内存告警 |
| 知道谁先过期就该先走 | volatile-ttl | 比如验证码、临时令牌 |
maxmemory 6gb # 8 GB 的机器,留出 fork 写时复制和各种缓冲区的余量
maxmemory-policy allkeys-lfu # 默认 noeviction
maxmemory-samples 5 # 调到 10 更准,多花一点 CPU
lazyfree-lazy-eviction yes # 淘汰大 key 时在后台线程释放
lazyfree-lazy-expire yes # 删除过期大 key 时也放到后台
hz 10 # 定期删除等后台任务的频率
看效果:INFO stats 里的 expired_keys、evicted_keys;INFO memory 里的 used_memory 和 maxmemory。
Redis 版本,以及 Java 这边
JDK 8 还是 17、Boot 2 还是 3 不影响这些机制。要注意的是 Java 代码怎么设过期时间。
Redis 4.0 ~ 6.2
机制成形- 4.0:新增 LFU 两种策略、
UNLINK和 lazyfree 系列配置 - 6.0:定期删除改写,新增
active-expire-effort;SET … KEEPTTL - 6.2:新增
maxmemory-eviction-tenacity,控制一次写命令里最多花多少力气淘汰;GETEX读的同时改过期时间
Redis 7.x
更细的控制- 7.0:
EXPIRE支持 NX / XX / GT / LT;新增EXPIRETIME直接看到期的绝对时间 - 7.4:Hash 的单个字段也能设过期时间(
HEXPIRE、HTTL)
redisTemplate.opsForValue().set(k, v) 不带过期时间,会把原来的 TTL 清掉;要么每次都传 Duration,要么在 RedisConnection 这一层用 Expiration.keepTtl()。Spring Cache 统一用 spring.cache.redis.time-to-live 设过期时间。写入被 OOM 拒绝时,会抛出带 OOM command not allowed 的异常。常踩的坑
内存问题多半是两种:该删的没删掉,该限的没限住。
默认不限,一直涨到机器内存耗尽,被 OOM killer 杀掉,或者开始用交换分区变得极慢。缓存实例一定要设。
复制缓冲区、AOF 缓冲区不算在 maxmemory 里,fork 时写时复制还要额外内存。留出余量,常见是机器内存的 60%~75%。
默认策略是 noeviction,缓存写满后所有写入报错。纯缓存改成 allkeys-lru / allkeys-lfu。
大部分 key 没设过期时间,可淘汰的 key 不够。要么给缓存都设上 TTL,要么换 allkeys-*。
普通的 SET 会清掉原来的 TTL,key 从此永不过期,越积越多。用 SET … KEEPTTL,或者每次都重新带上过期时间。
大批 key 在同一时刻过期,定期删除每轮抽到的都是过期的,一直循环删,吃满 25% 的 CPU。过期时间加随机值(Redis 第 01 篇的雪崩也是这个原因)。
一是没人访问、还没被抽到的过期 key 还在;二是删掉后内存碎片没还给系统,看 mem_fragmentation_ratio,可以开 activedefrag。
全表扫描把大量冷 key 变成“刚访问过”,LRU 把热 key 挤掉了。热点明显的缓存用 allkeys-lfu。
被淘汰、被过期删除的恰好是大 key,在主线程里一个个释放元素。打开 lazyfree-lazy-eviction 和 lazyfree-lazy-expire。
只有 maxmemory-policy 是 LFU 类策略时才能用;LRU 策略下用 OBJECT IDLETIME 看空闲了多久。