2. 缓存基础回顾:常见缓存类型与淘汰策略
在深入 Testing 环境下的缓存过期时间配置测试之前,有必要系统回顾缓存的核心分类与淘汰机制。理解这些基础概念,是后续设计测试用例、分析缓存行为的前提。
2.1 常见缓存类型
根据数据存储位置与访问方式,缓存主要分为本地缓存与分布式缓存两大类。两者在性能、容量、一致性模型上存在显著差异。
| 特性 | 本地缓存 | 分布式缓存 |
|---|---|---|
| 存储位置 | 应用进程内(JVM 堆 / 堆外内存) | 独立缓存服务节点(跨进程 / 跨机器) |
| 访问延迟 | 纳秒级(无网络开销) | 毫秒级(网络 RTT + 序列化) |
| 容量上限 | 受限于单机内存(通常 GB 级) | 可水平扩展至 TB / PB 级 |
| 数据一致性 | 单机强一致(无副本冲突) | 最终一致 / 需额外协议(如 Redis Sentinel) |
| 典型实现 | Caffeine, Guava Cache, Ehcache | Redis, Memcached, Hazelcast |
| 适用场景 | 配置元数据、热点数据、单机计算加速 | 会话共享、全局计数器、跨服务数据聚合 |
测试关注点: 在 Testing 环境中,本地缓存的过期时间通常依赖 JVM 时钟精度,而分布式缓存则受网络延迟与时钟漂移影响。测试时需区分这两种场景下的过期时间误差容忍度。
2.2 缓存淘汰策略
当缓存容量达到上限或条目过期时,需要按照特定策略移除数据。常见的淘汰策略包括 LRU、LFU 和 TTL,它们在淘汰依据与行为模式上各有侧重。
- LRU(Least Recently Used,最近最少使用):淘汰最长时间未被访问的条目。基于“时间局部性”假设,适合访问模式有明显热点切换的场景。
- LFU(Least Frequently Used,最不经常使用):淘汰访问频率最低的条目。基于“频率局部性”假设,适合访问模式稳定、热点长期不变的场景。
- TTL(Time To Live,生存时间):为每个条目设置固定的过期时间,到期后自动失效。不依赖访问历史,适合有明确时效性要求的数据。
| 策略 | 淘汰依据 | 内存开销 | 典型实现 | 测试难点 |
|---|---|---|---|---|
| LRU | 最近访问时间 | 低(双向链表 + 哈希表) | LinkedHashMap, Redis allkeys-lru |
访问顺序模拟、边界容量触发 |
| LFU | 访问频率计数 | 中(频率计数器 + 最小堆) | Redis allkeys-lfu, Caffeine |
频率衰减策略、冷热数据切换 |
| TTL | 写入时间 + 固定时长 | 低(过期时间戳) | Redis EXPIRE, Guava Cache |
时钟同步、过期时间精度、延迟淘汰 |
常见误区: 很多开发者误以为 TTL 是“到期立即删除”。实际上,大多数缓存实现采用惰性删除(访问时检查过期)或定时扫描(周期清理),并非实时删除。Testing 环境下需验证过期后的实际可见性窗口。
2.3 缓存类型与淘汰策略的组合架构
下图展示了本地缓存与分布式缓存如何与不同的淘汰策略组合,形成完整的缓存分层架构。箭头表示数据流向与淘汰触发关系。
2.4 淘汰策略在 Testing 环境中的测试要点
在 Testing 环境下验证缓存淘汰策略时,需重点关注以下维度:
- 容量触发验证:构造数据量超过缓存上限,确认 LRU/LFU 是否按预期淘汰最久/最冷数据。
- TTL 精度验证:设置不同粒度的过期时间(秒级、毫秒级),检查过期后的数据是否在预期时间窗口内失效。
- 淘汰顺序验证:对 LRU 模拟“热点转移”访问模式,验证淘汰顺序是否符合“最近未访问优先”原则。
- 频率衰减验证:对 LFU 模拟“冷数据高频访问后变热”场景,验证频率计数是否随时间衰减。
- 并发淘汰验证:多线程并发读写缓存,验证淘汰逻辑在竞争条件下是否出现数据丢失或重复淘汰。
测试设计建议: 在 Testing 环境中,建议为每种淘汰策略单独编写参数化测试用例,覆盖“未触发淘汰”、“触发淘汰”、“淘汰边界”三类场景。使用
@ParameterizedTest 或数据驱动框架可有效减少重复代码。
2.5 代码示例:Guava Cache 的 TTL 与 LRU 配置
以下代码展示了如何在 Guava Cache 中同时配置 TTL 过期与 LRU 淘汰策略,并模拟 Testing 环境下的验证逻辑。
// 构建缓存:最大容量 100,写入后 5 秒过期
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(5, TimeUnit.SECONDS)
.recordStats()
.build();
// 模拟 Testing 环境验证
String key = "test:config:timeout";
cache.put(key, "value_v1");
// 等待 6 秒后访问(超过 TTL)
Thread.sleep(6000);
String result = cache.getIfPresent(key);
assertNull(result, "TTL 过期后应返回 null");
// 验证 LRU 淘汰:插入 101 条数据
for (int i = 0; i < 101; i++) {
cache.put("key_" + i, "val_" + i);
}
// 最早插入的 key_0 应被淘汰
assertNull(cache.getIfPresent("key_0"), "LRU 应淘汰最早未访问条目");
// 输出缓存统计信息
CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate());
System.out.println("淘汰数: " + stats.evictionCount());
Testing 环境时钟控制: 在测试 TTL 时,建议使用
TestClock 或 Mockito 模拟时间推进,避免真实 Thread.sleep() 带来的测试不稳定。Caffeine 提供了 Ticker 接口用于注入自定义时钟。