读完这篇,你能做什么
能复现 TTL 被覆盖的情况,按数据用途解释内存满时应淘汰还是拒绝写入。
学习理由、准备要求与知识点
为什么学它
键过期与内存淘汰是两种独立机制,混淆它们容易造成会话丢失或缓存永不过期。
先读这些内容
知识点清单
- TTL 与 PTTL
- 惰性和主动过期
- SET 覆盖 TTL
- HSET 更新
- KEEPTTL
- maxmemory
- 淘汰策略
- 缓存 TTL 抖动
运行前先确认Redis7.4、Java17;Jedis源码v5.2.0。算法以单调逻辑秒和最小堆演示到期事件,不是Redis真实过期实现;不模拟maxmemory淘汰。
01先弄懂它是什么
Redis 中的键为什么会消失,要分两种情况:过期是有效时间到了,淘汰是内存紧张时按策略让出空间。还剩一分钟 TTL 的键也可能提前被淘汰;有些 allkeys 策略甚至会移除没有 TTL 的键。缓存通常能重建,但登录态、去重记录和锁提前丢失的影响不同,不能只因为都在 Redis 中就使用相同容量安排。
查看剩余有效期用 TTL:-2 表示键不存在,-1 表示键存在但没设有效期,非负数是剩余秒数;PTTL 返回毫秒粒度。这个数只是查询时看到的剩余时间,不保证键一定活到那一刻,因为它还可能被删除或淘汰。两次读数也不能直接证明客户端间隔和服务端计时完全相同。
普通SET覆盖键通常清除原有效期;显式EX/PX设置新期限,KEEPTTL保留已有期限。HSET修改现有Hash字段通常保留Key的TTL,但新建Hash不会自动拥有业务要求的期限。覆盖、修改内部值和重命名等命令的影响不同,应查具体命令,不能用“写入都刷新TTL”概括。
文中这几个词,先认一认
- 过期
- 达到规定的到期时间后,键失效,和当前内存是否紧张是两回事。
- 淘汰
- 内存压力下按策略移除键以释放空间,即使这个键尚未到期也可能发生。
- noeviction
- 内存超限时拒绝部分需要增加内存的写操作;不是内存无限,也不是所有命令都停止。
02看看它怎么工作
在隔离实例交互执行SET study:ttl v EX 60,观察TTL;再SET study:ttl newer,TTL应显示无期限。重新带EX写入后用SET study:ttl newest KEEPTTL观察保留期限。每一步记录键是否存在与TTL,不仅看SET返回OK。实验只操作练习前缀,实际键策略应由业务决定。
Redis 既会主动检查一部分键,也会在访问键时检查它是否过期;不是每个键都配一个 Java 定时线程。服务器记录的是到期时刻,保存和恢复数据时也要考虑中间已经过去了多久。本文的最小堆程序只演示一件事:键被更新后,旧的到期任务不能误删新值。它没有复现 Redis 内部怎样存储和调度过期任务,也不能用来判断真实删除延迟。
选择内存策略前,先确认哪些数据允许提前丢失。allkeys 策略可以从全部键中选,volatile 策略只从设置了过期时间的键中选;可选的键不够,写入仍可能被拒绝。LRU 主要参考最近是否访问,LFU 主要参考访问频率,但 Redis 用的是近似判断,不会给全部键维护一份绝对精确的完整排名。监控时应一起看内存用了多少、多少键被淘汰或过期,以及多少写入被拒绝。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["一次读取或写入"] --> B["检查键是否到期"]
B --> C["按当前版本处理失效"]
A --> D["检查内存与淘汰策略"]
D --> E{"是否有可淘汰候选"}
E -->|"有"| F["按策略移除候选"]
E -->|"无或禁止"| G["相关写入可能被拒绝"]
C --> H["返回值及TTL观察"]
F --> H按顺序看,每一步做了什么
- 用TTL区分不存在、持久键和有期限键。
- 逐命令复现SET覆盖、显式EX与KEEPTTL。
- 检查缓存与关键状态是否应分开容量策略。
- 在隔离测试中观察内存、淘汰和拒绝写指标。
- 为提前失效与写入失败分别定义业务行为。
03对照实现代码
Jedis v5.2.0 · SetParams#expiration、#ex、#keepTtl、#addParams
原创参数状态模型,非源码节选。只表达一次选择的过期模式,真实过期和内存淘汰由Redis服务端实现。
import java.util.*;
class SourceModel {
enum Mode { NONE, EX, KEEP }
static class Options {
Mode mode=Mode.NONE;
long seconds;
Options ex(long value) {
if (value<=0) throw new IllegalArgumentException("ttl");
mode=Mode.EX; seconds=value; // 明确选择新期限。
return this;
}
Options keep() { mode=Mode.KEEP; return this; } // 覆盖先前模式。
List<String> arguments() {
// 只输出当前模式,不同时发送互斥选项。
return switch(mode) {
case NONE -> List.of();
case EX -> List.of("EX",Long.toString(seconds));
case KEEP -> List.of("KEEPTTL");
};
}
}
}真实expiration方法保存类型与值,ex与keepTtl都通过它更新同一组选项。
链式调用并非不断累积所有模式,后设的过期选项会替换状态,代码审查应看最终参数。
KEEPTTL不带新的过期数值,它保留已有期限;不存在的键没有旧期限可继承。
客户端参数构造与Redis过期、淘汰、持久化是不同层,Java源码不能当作Redis服务端内核证据。
代码先调用谁,再调用谁
- 创建
SetParams - 选择EX或KEEPTTL
- 更新参数状态
addParams输出当前选项- 服务端按SET语义执行
04动手写一个例子
代际标识防止旧过期事件删除新值
实现单线程键值模型,支持普通SET、带TTL覆盖、KEEPTTL和TTL查询。过期事件进入最小堆,每次写分配新代际;旧事件只能删除同一代际的键。时间使用单调逻辑秒,TTL正整数。
- 输入 1
k在0写入TTL5,2时普通SET覆盖,6时查询- 输出 1
新值仍在,TTL=-1
- 输入 2
k截止10,8时KEEPTTL覆盖- 输出 2
截止仍10
最小堆先取出最早到期的任务,但任务等待期间,同名键可能已被改成新值。因此每次写入都带一个新的版本编号,也叫代际;到期任务记住它负责的那个编号。任务执行时,只有编号仍与键的当前编号相同,才允许删除,不能仅凭键名相同就动手。
在本模型中,普通 SET 没有指定 TTL 时,新值就没有到期时间;这里说的是不会按期限过期,不是已经保存到磁盘。KEEPTTL 则沿用当前有效旧值的到期时刻;若旧键已不存在,就没有期限可继承。写入前先清掉已经到期的条目,才能避免把失效的期限重新带给新值。
旧事件采用懒删除,简化更新,但队列空间随未出堆的历史事件增长,不能只按当前键数估计内存。生产定时器需要压缩、索引更新或容量限制,这是模型明确的代价。
为什么这样写是对的每个有效键只有一个当前代际;任何旧代际事件都不能删除它;TTL查询前已处理所有到期事件。
K为当前键数,H为尚在堆中的历史事件,E为本次清理数;懒删除可能使H大于K。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
record Value(String text,Long deadline,long generation) {}
record Event(String key,long deadline,long generation) {}
static class Store {
final Map<String,Value> values=new HashMap<>();
final PriorityQueue<Event> events=new PriorityQueue<>(Comparator.comparingLong(Event::deadline));
long generation,last=-1;
void advance(long now) {
if (now<0||now<last) throw new IllegalArgumentException("clock");
last=now;
while (!events.isEmpty()&&events.peek().deadline()<=now) {
Event event=events.remove();
Value current=values.get(event.key());
// 旧定时事件不能影响覆盖后产生的新代际。
if (current!=null&¤t.generation()==event.generation())
values.remove(event.key());
}
}
void set(String key,String text,Long ttl,boolean keep,long now) {
if (ttl!=null&&(ttl<=0||keep)) throw new IllegalArgumentException("options");
advance(now);
Value old=values.get(key);
Long deadline;
if (keep) deadline=old==null?null:old.deadline();
else deadline=ttl==null?null:Math.addExact(now,ttl);
generation=Math.addExact(generation,1);
// 普通覆盖没有TTL时,新代际明确不关联期限。
Value value=new Value(text,deadline,generation);
values.put(key,value);
if (deadline!=null) events.add(new Event(key,deadline,generation));
}
long ttl(String key,long now) {
advance(now);
Value value=values.get(key);
// 缺失与持久键使用不同特殊值。
return value==null?-2:value.deadline()==null?-1:value.deadline()-now;
}
}
static void eq(long expected,long actual) {
if (expected!=actual) throw new AssertionError(actual);
}
public static void main(String[] args) {
Store s=new Store();
s.set("k","a",5L,false,0); eq(5,s.ttl("k",0));
s.set("k","b",null,false,2); eq(-1,s.ttl("k",6));
s.set("j","a",5L,false,6);
s.set("j","b",null,true,8); eq(3,s.ttl("j",8)); eq(-2,s.ttl("j",11));
s.set("r","a",10L,false,11);
s.set("r","b",5L,false,12); eq(-2,s.ttl("r",17));
s.set("x","a",1L,false,17);
s.set("x","b",null,true,20); eq(-1,s.ttl("x",20));
try { s.set("bad","a",0L,false,20); throw new AssertionError("ttl"); }
catch (IllegalArgumentException expected) {}
eq(-2,s.ttl("bad",20));
System.out.println("PASS 8 expiration generation tests");
}
}这些特殊情况也要试一试
- 恰好到期视为失效;KEEPTTL作用于已过期键时不能复活旧期限。
- 普通覆盖使旧事件失效,即使事件稍后从堆弹出也不能删除新值。
- 逻辑时钟不允许倒退,期限与代际加法都检测溢出。
进阶选读 · 压缩定时事件与近似淘汰协同
场景热点键频繁改TTL,历史堆事件远多于活跃键,定时器内存增长。
做法当历史事件与活跃键比例超限时按当前有效期限重建堆,或采用可索引堆更新;另设独立容量策略,不让过期调度承担全部内存控制。
复杂度重建可O(K),索引堆更新O(log K),但需要额外索引维护。
好处与代价压缩降低空间却引入突发维护成本,近似淘汰降低排序成本但不能保证严格LRU次序。
05项目里怎么用
缓存、会话和可靠去重数据可能需要不同实例或容量政策。某些状态提前淘汰只会回源,另一些会造成重新登录或重复执行业务。noeviction减少主动淘汰,却把问题转为相关写入拒绝,应用必须处理错误而不是忽略返回值。
maxmemory 不能简单当成 Redis 进程全部内存的上限。复制、持久化和缓冲还需要空间,应同时观察进程实际驻留内存 RSS、碎片和相关缓冲。删除大键、很多键一起到期,也可能集中增加工作量,需要按数据规模安排到期时间和操作速度。
过期观察要说明单位和时点,持久化恢复后TTL还应检查剩余期限。读取到-2不一定证明自然到期,也可能被显式删除、覆盖重建或淘汰。定位原因需结合命令路径与指标,单次TTL不足以解释全部生命周期。
容易踩的坑
- 把过期和淘汰合并成同一种失效原因。
- SET覆盖后以为旧TTL自动保留。
- noeviction被当作内存问题自动解决方案。
- 懒删除定时器按活跃键数估算内存,忽略历史事件。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| allkeys淘汰 | 候选广,适合可重建缓存。 | 无TTL键也可能提前丢失。 | 可接受缓存丢失的实例。 |
| volatile淘汰 | 限定带TTL键作为候选。 | 候选不足仍可能拒绝写入。 | 明确区分可淘汰键且预算充足的场景。 |
| noeviction | 避免因该策略主动淘汰已有键。 | 内存超限写入会失败。 | 更重视保留状态且能处理拒绝的系统。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01TTL=-1和-2分别是什么?
展开参考答案与推导
-1表示存在但无过期,-2表示键不存在。
02有TTL就能保证有效期内一直存在吗?
展开参考答案与推导
不能,还可能被删除、覆盖或淘汰。
03Redis LRU是精确完整排序吗?
展开参考答案与推导
不是,使用近似采样等机制以降低维护成本。
换个情况,还会处理吗
01更新缓存后TTL变-1如何排查?
展开参考答案与推导
检查是否普通SET覆盖而没有显式期限或KEEPTTL。
02设置volatile策略后仍拒绝写入为什么?
展开参考答案与推导
可能可淘汰候选不足,需检查键TTL分布、预算和策略。
03登录态提前消失如何定位?
展开参考答案与推导
分别检查过期、显式删除、淘汰和重建路径,不能只读一次TTL下结论。
写代码试一试
01为何到期事件要保存代际?
- 输入 / 条件
旧值到期前被新值覆盖- 输出 / 预期
旧事件不删除新值
提示 比对当前身份。
展开参考答案与推导
只有事件代际等于当前条目代际才能执行删除。
02已过期键KEEPTTL应怎样处理?
- 输入 / 条件
旧期限18,20时覆盖- 输出 / 预期
新键无可继承旧期限
提示 写前处理到期。
展开参考答案与推导
先advance移除旧键,再按不存在分支创建持久条目。
03怎样控制历史事件增长?
- 输入 / 条件
一个键更新十万次TTL- 输出 / 预期
定时器空间受控
提示 不能只懒删除。
展开参考答案与推导
按比例重建堆或用可索引堆原地更新时间,并说明维护复杂度。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能复现 TTL 被覆盖的情况,按数据用途解释内存满时应淘汰还是拒绝写入。
本篇重点加练
保留有效期和内存淘汰的作用层不同,必须把业务可用期与缓存驻留时间分开验证。
- KEEPTTL
- 淘汰策略
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01排错实验
在独立本地 Redis 设置三种短 TTL 键,间隔观测 TTL、PTTL 和读取结果。解释计时精度及未主动读取的键可以怎样被处理。
- 提交内容
- 命令及带时刻的观测表
- 怎样算完成
- 区分逻辑过期与内存回收
- 测量误差有说明
练到的知识点TTL 与 PTTL · 惰性和主动过期
- 作业 02代码推演
同一资料键先写入带 TTL,再分别执行覆盖 SET、Hash 字段更新和保留 TTL 的写入。为每类合法类型准备独立样本,先预测再运行。
- 提交内容
- 预测表和实测记录
- 怎样算完成
- 不混淆键类型
- 每次写入前后都检查有效期
练到的知识点SET 覆盖 TTL · HSET 更新 · KEEPTTL
- 作业 03代码推演
后台预热程序读取旧 TTL 后更新值,期间键可能已经到期并被另一个请求重建。审查客户端自行恢复 TTL 与服务端保留 TTL 语义的时点差异。
- 提交内容
- 交错与有效期契约
- 怎样算完成
- 区分键的不同代次
- 包含到期后重建
练到的知识点KEEPTTL
综合应用
- 作业 04原理问答
本地测试实例内存上限很小,混有永久键和过期键。评审三类淘汰策略的候选范围及写入失败时应用应提供的信息。
- 提交内容
- 策略比较与故障契约
- 怎样算完成
- 区分过期与内存淘汰
- 列出不可丢的数据类别
练到的知识点maxmemory · 淘汰策略
- 作业 05设计评审
1000 家商户缓存由批任务集中预热,要求有效期分散但不能早于最短业务期限。设计 TTL 取值约束及分布检验。
- 提交内容
- 采样规则与分布报告计划
- 怎样算完成
- 给出上下界和单位
- 说明随机种子对可复现实验的作用
练到的知识点缓存 TTL 抖动
- 作业 06编程实践
编写本地 Redis 缓存写入实验,固定数据量与内存预算,记录有效期变化、淘汰统计及应用命中率;实验实例与其他数据隔离。
- 提交内容
- 程序、配置及报告
- 怎样算完成
- 资源和运行时间有上限
- 不把单次淘汰顺序当作算法保证
练到的知识点TTL 与 PTTL · SET 覆盖 TTL · HSET 更新 · KEEPTTL · maxmemory · 淘汰策略 · 缓存 TTL 抖动
- 作业 07设计评审
同一实例存放少量大值和大量小值,业务希望保留“重建最昂贵”的键。评审常见淘汰策略的观测信息是否足以表达该成本。
- 提交内容
- 策略目标与信息需求
- 怎样算完成
- 区分访问频率、时间和重建成本
- 不假设策略具有业务知识
练到的知识点淘汰策略
- 作业 08排错实验
在独立本地 Redis 用小内存上限建立两类缓存,持续更新其中一类并记录 TTL、驻留和淘汰统计,测量不超过固定时长。
- 提交内容
- 配置、脚本和时间记录
- 怎样算完成
- 不推断单键必然存活
- 区分保留 TTL 和避免淘汰
练到的知识点KEEPTTL · 淘汰策略
进阶挑战 · 掌握前置后选做
- 作业 09面试场景
服务会话与可重建缓存共用实例,缓存增长后用户集中掉线。提出验证性调查和资源分配评审,给出风险可观察的迁移验收条件。
- 提交内容
- 调查计划与迁移矩阵
- 怎样算完成
- 先确认丢失原因再调整
- 明确哪些数据不能依赖缓存可用期
练到的知识点TTL 与 PTTL · 惰性和主动过期 · maxmemory · 淘汰策略 · 缓存 TTL 抖动
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
研究“缓存寿命越长就越节省后端成本”是否总成立,将内存占用、更新频率和陈旧错误代价加入模型并设计反例。
- 提交内容
- 成本模型与反例
- 怎样算完成
- 各成本单位明确
- 不只用一次命中率比较下结论
练到的知识点KEEPTTL · 淘汰策略
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Redis:EXPIRE (新标签页)↗
阅读 TTL、覆盖写入和更新命令的交互,检查缓存是否会意外永不过期。
- 实践参考文档英文
Redis:SET (新标签页)↗
阅读 NX、PX 与 KEEPTTL,明确条件写入、租期和覆盖语义。
- 深入参考文档英文
Redis:Key eviction (新标签页)↗
阅读 maxmemory 与淘汰策略,区分过期、内存回收和写入拒绝。
- 深入参考文档英文
Jedis SetParams (新标签页)↗
查看互斥过期模式在客户端的组织方式。