🌈 我是"没事学AI", 欢迎咨询、交流,共同学习:
👁️ 【关注】我们一起挖 AI 的各种门道,看看它还有多少新奇玩法等着咱们发现
👍 【点赞】为这些有用的 AI 知识鼓鼓掌,让更多人知道学 AI 也能这么轻松
🔖 【收藏】把这些 AI 小技巧存起来,啥时候想练手了,翻出来就能用
💬 【评论】说说你学 AI 时的想法和疑问,让大家的思路碰出更多火花
👉 关注获取更多AI技术干货,点赞/收藏备用,欢迎评论区交流学习心得! 🚀
目录
-
- 一、Caffeine过期策略的底层逻辑与设计初衷
- 二、三种核心过期策略的深度解析与实战
-
- [2.1 expireAfterWrite:固定写入过期策略(写后过期)](#2.1 expireAfterWrite:固定写入过期策略(写后过期))
-
- [2.1.1 核心原理](#2.1.1 核心原理)
- [2.1.2 实战配置与代码示例](#2.1.2 实战配置与代码示例)
- [2.1.3 适用场景与注意事项](#2.1.3 适用场景与注意事项)
- [2.2 expireAfterAccess:访问过期策略(读后过期)](#2.2 expireAfterAccess:访问过期策略(读后过期))
-
- [2.2.1 核心原理](#2.2.1 核心原理)
- [2.2.2 实战配置与代码示例](#2.2.2 实战配置与代码示例)
- [2.2.3 适用场景与注意事项](#2.2.3 适用场景与注意事项)
- [2.3 expireAfter:自定义过期策略(灵活过期)](#2.3 expireAfter:自定义过期策略(灵活过期))
-
- [2.3.1 核心原理](#2.3.1 核心原理)
- [2.3.2 实战配置与代码示例](#2.3.2 实战配置与代码示例)
- [2.3.3 适用场景与注意事项](#2.3.3 适用场景与注意事项)
- 三、过期策略的组合使用与场景适配进阶
-
- [3.1 策略组合的核心逻辑与案例](#3.1 策略组合的核心逻辑与案例)
-
- [3.1.1 基于`expireAfter`自定义组合逻辑](#3.1.1 基于
expireAfter
自定义组合逻辑) - [3.1.2 策略与"手动失效"结合](#3.1.2 策略与“手动失效”结合)
- [3.1.1 基于`expireAfter`自定义组合逻辑](#3.1.1 基于
- [3.2 不同业务场景的策略选型指南](#3.2 不同业务场景的策略选型指南)
- 四、生产环境的优化建议与监控调优
-
- [4.1 生产环境优化建议](#4.1 生产环境优化建议)
-
- [4.1.1 合理设置过期时长与容量](#4.1.1 合理设置过期时长与容量)
- [4.1.2 避免缓存雪崩与穿透](#4.1.2 避免缓存雪崩与穿透)
- [4.1.3 异步加载与重试机制](#4.1.3 异步加载与重试机制)
- [4.2 监控与调优手段](#4.2 监控与调优手段)
-
- [4.2.1 开启缓存统计](#4.2.1 开启缓存统计)
- [4.2.2 结合监控系统告警](#4.2.2 结合监控系统告警)
- [4.2.3 压测验证策略合理性](#4.2.3 压测验证策略合理性)
- 五、总结
一、Caffeine过期策略的底层逻辑与设计初衷
在深入具体策略前,我们需要先理解Caffeine过期机制的底层共性。Caffeine的过期管理基于时间驱动+事件触发的双重机制:
- 时间基准:所有过期策略均以"时间戳"为核心判断依据,通过维护每个缓存条目的创建/更新/访问时间戳,结合预设时长计算过期时间点;
- 清理时机 :过期条目并非实时删除,而是在以下时机触发清理:
- 主动访问(get/put)时触发"惰性清理",检查当前条目是否过期;
- 后台定时任务(默认每1秒)触发"主动清理",批量扫描并删除过期条目;
- 缓存容量达到阈值时,淘汰算法会优先清理过期条目。
这种设计既避免了实时清理的性能开销,又通过多时机互补确保了过期数据不会长期占用资源。而三种过期策略的差异,本质上是"时间戳更新规则"与"过期判断逻辑"的不同组合。
二、三种核心过期策略的深度解析与实战
Caffeine通过Caffeine.newBuilder()
的链式调用配置过期策略,三种策略分别对应expireAfterWrite()
、expireAfterAccess()
、expireAfter()
三个核心方法。下面将逐一拆解其原理、配置、案例与适用场景。
2.1 expireAfterWrite:固定写入过期策略(写后过期)
2.1.1 核心原理
expireAfterWrite
以缓存条目的创建时间或最后一次更新时间为计时起点,设置一个固定的过期时长。只要在该时长内没有发生"写入操作"(put/update),无论条目是否被访问,都会被判定为过期。
举个通俗例子:如果设置expireAfterWrite(5, TimeUnit.MINUTES)
,某条目在10:00被创建,10:03被更新一次,那么其过期时间会从10:03重新计算,最终过期时间为10:08;若10:03后无任何更新,即使10:07有访问操作,10:08后该条目仍会过期。
2.1.2 实战配置与代码示例
基础配置 :通过expireAfterWrite(long duration, TimeUnit unit)
设置固定过期时长,适用于"数据更新后需保留固定时长"的场景(如商品基础信息、用户配置等)。
java
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;
public class WriteExpireDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 配置expireAfterWrite:写入后5秒过期
Cache<String, String> productCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS) // 核心配置
.maximumSize(1000) // 避免缓存无限增长,配合过期策略使用
.build();
// 2. 第一次写入:创建时间戳=当前时间
productCache.put("product:1001", "iPhone 15 Pro 256G 黑色");
System.out.println("第一次获取(写入后1秒):" + productCache.getIfPresent("product:1001")); // 输出:iPhone 15 Pro...
// 3. 3秒后更新:更新时间戳重置,过期时间顺延
Thread.sleep(3000);
productCache.put("product:1001", "iPhone 15 Pro 256G 白色(库存更新)");
System.out.println("更新后获取:" + productCache.getIfPresent("product:1001")); // 输出:iPhone 15 Pro...白色
// 4. 6秒后再次获取:距离上次更新已超过5秒,条目过期
Thread.sleep(6000);
System.out.println("过期后获取:" + productCache.getIfPresent("product:1001")); // 输出:null
}
}
进阶配置 :结合recordStats()
统计缓存命中率,验证过期策略的有效性:
java
Cache<String, String> productCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.maximumSize(1000)
.recordStats() // 开启统计
.build();
// 业务逻辑执行后,打印统计信息
System.out.println("缓存命中率:" + productCache.stats().hitRate());
System.out.println("过期条目数:" + productCache.stats().evictionCount());
2.1.3 适用场景与注意事项
-
适用场景:
- 数据更新频率可预测,且更新后需稳定保留一段时间(如商品价格、活动规则);
- 避免"缓存雪崩":由于所有条目过期时间相对分散(基于写入时间),天然具备抗雪崩能力;
- 写操作较少但数据时效性要求高的场景(如用户订单状态,更新后保留1小时)。
-
注意事项:
- 若数据长期不更新,即使频繁访问也会过期,可能导致"缓存穿透"(需配合布隆过滤器);
- 不适用于"热点数据"(如首页热门商品),频繁访问但不更新会导致频繁过期,命中率下降。
2.2 expireAfterAccess:访问过期策略(读后过期)
2.2.1 核心原理
expireAfterAccess
以缓存条目的最后一次访问时间(包括get/put操作)为计时起点,设置固定过期时长。只要在该时长内有任何"访问操作"(读或写),过期时间就会重新计算;若超过时长无访问,则判定为过期。
对比expireAfterWrite
:假设同样设置5分钟过期,某条目10:00创建,10:03访问一次,10:07再访问一次,那么其过期时间会从10:07重新计算,最终过期时间为10:12;而expireAfterWrite
在10:00创建后无更新的话,10:05就会过期。
2.2.2 实战配置与代码示例
基础配置 :通过expireAfterAccess(long duration, TimeUnit unit)
设置访问过期时长,适用于"热点数据临时缓存"场景(如首页热门商品、高频查询的用户信息)。
java
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;
public class AccessExpireDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 配置expireAfterAccess:最后一次访问后3秒过期
Cache<String, String> hotGoodsCache = Caffeine.newBuilder()
.expireAfterAccess(3, TimeUnit.SECONDS) // 核心配置
.maximumSize(500)
.recordStats()
.build();
// 2. 写入热点商品数据
hotGoodsCache.put("hot:101", "2024夏季新款连衣裙(销量TOP1)");
System.out.println("初始获取:" + hotGoodsCache.getIfPresent("hot:101")); // 输出:2024夏季新款...
// 3. 2秒后访问(读操作):过期时间重置为当前+3秒
Thread.sleep(2000);
System.out.println("2秒后访问:" + hotGoodsCache.getIfPresent("hot:101")); // 输出:2024夏季新款...
// 4. 再等2秒(累计4秒,但最后一次访问在2秒前):仍未过期
Thread.sleep(2000);
System.out.println("再等2秒访问:" + hotGoodsCache.getIfPresent("hot:101")); // 输出:2024夏季新款...
// 5. 再等4秒(距离最后一次访问已超过3秒):过期
Thread.sleep(4000);
System.out.println("过期后访问:" + hotGoodsCache.getIfPresent("hot:101")); // 输出:null
// 6. 打印统计:验证访问驱动的过期效果
System.out.println("缓存命中率:" + hotGoodsCache.stats().hitRate());
System.out.println("过期淘汰数:" + hotGoodsCache.stats().evictionCount());
}
}
实战延伸 :结合get()
方法的"缓存加载器",实现"过期后自动重新加载":
java
// 当缓存过期或不存在时,自动调用load方法加载数据
Cache<String, String> hotGoodsCache = Caffeine.newBuilder()
.expireAfterAccess(3, TimeUnit.SECONDS)
.build((key) -> loadHotGoodsFromDB(key)); // 自定义加载逻辑
// 模拟从数据库加载热点商品
private static String loadHotGoodsFromDB(String key) {
System.out.println("从DB加载数据:" + key);
return "2024夏季新款连衣裙(DB加载)";
}
2.2.3 适用场景与注意事项
-
适用场景:
- 热点数据缓存(如首页Banner、热门文章),通过访问延长过期时间,减少重复加载;
- 会话级缓存(如用户登录态、临时操作记录),用户活跃时保留,闲置时自动清理;
- 资源敏感场景(如内存有限的服务),通过"闲置过期"释放未使用的缓存空间。
-
注意事项:
- 可能导致"缓存堆积":若某数据长期被访问,会一直不过期,占用内存(需配合
maximumSize
限制容量); - 不适用于"数据时效性强且更新频率低"的场景(如实时库存,即使频繁访问也需定期更新)。
- 可能导致"缓存堆积":若某数据长期被访问,会一直不过期,占用内存(需配合
2.3 expireAfter:自定义过期策略(灵活过期)
2.3.1 核心原理
expireAfter
是Caffeine最灵活的过期策略,允许开发者通过自定义函数 为每个缓存条目设置独立的过期时间,且支持动态调整过期逻辑。其核心是实现Expiry
接口,重写三个方法定义时间戳规则:
expireAfterCreate(K key, V value, long currentTime)
:条目创建时,返回"创建后多久过期";expireAfterUpdate(K key, V value, long currentTime, long currentDuration)
:条目更新时,返回"更新后多久过期";expireAfterRead(K key, V value, long currentTime, long currentDuration)
:条目访问时,返回"访问后多久过期"(返回currentDuration
表示不改变原过期时间)。
这种策略打破了"所有条目统一过期时长"的限制,支持根据业务属性(如数据类型、优先级)动态设置过期时间。
2.3.2 实战配置与代码示例
场景需求:电商系统中,不同类型的商品缓存设置不同过期时间:
- 秒杀商品:创建后10秒过期(时效性极强);
- 普通商品:创建后5分钟过期,更新后重置为5分钟;
- 预售商品:创建后24小时过期,访问不延长过期时间。
代码实现:
java
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Expiry;
import java.util.concurrent.TimeUnit;
// 1. 自定义Expiry实现类,按商品类型设置过期时间
class GoodsExpiry implements Expiry<String, Goods> {
// 商品类型枚举
public enum GoodsType {
SECKILL(10), // 秒杀商品:10秒过期
NORMAL(300), // 普通商品:5分钟(300秒)过期
PRE_SALE(86400); // 预售商品:24小时(86400秒)过期
private final long expireSeconds;
GoodsType(long expireSeconds) {
this.expireSeconds = expireSeconds;
}
public long getExpireSeconds() {
return expireSeconds;
}
}
// 条目创建时:根据商品类型返回过期秒数
@Override
public long expireAfterCreate(String key, Goods goods, long currentTime) {
return goods.getType().getExpireSeconds() * 1000_000; // 转换为纳秒(Caffeine时间单位)
}
// 条目更新时:普通商品重置过期时间,其他类型保持原时间
@Override
public long expireAfterUpdate(String key, Goods goods, long currentTime, long currentDuration) {
if (goods.getType() == GoodsType.NORMAL) {
return GoodsType.NORMAL.getExpireSeconds() * 1000_000;
}
return currentDuration; // 其他类型不改变过期时间
}
// 条目访问时:所有类型均不延长过期时间
@Override
public long expireAfterRead(String key, Goods goods, long currentTime, long currentDuration) {
return currentDuration; // 保持原过期时间
}
}
// 2. 商品实体类
class Goods {
private String id;
private String name;
private GoodsExpiry.GoodsType type;
// 构造器、getter、setter省略
public Goods(String id, String name, GoodsExpiry.GoodsType type) {
this.id = id;
this.name = name;
this.type = type;
}
public GoodsExpiry.GoodsType getType() {
return type;
}
@Override
public String toString() {
return "Goods{id='" + id + "', name='" + name + "', type=" + type + "}";
}
}
// 3. 实战测试
public class CustomExpireDemo {
public static void main(String[] args) throws InterruptedException {
// 配置自定义过期策略
Cache<String, Goods> goodsCache = Caffeine.newBuilder()
.expireAfter(new GoodsExpiry()) // 核心:传入自定义Expiry
.maximumSize(1000)
.recordStats()
.build();
// 写入不同类型的商品
goodsCache.put("goods:1", new Goods("1", "秒杀iPhone 15", GoodsExpiry.GoodsType.SECKILL));
goodsCache.put("goods:2", new Goods("2", "普通夏季T恤", GoodsExpiry.GoodsType.NORMAL));
goodsCache.put("goods:3", new Goods("3", "预售冬季羽绒服", GoodsExpiry.GoodsType.PRE_SALE));
// 11秒后检查:秒杀商品已过期,其他商品未过期
Thread.sleep(11000);
System.out.println("秒杀商品(11秒后):" + goodsCache.getIfPresent("goods:1")); // null
System.out.println("普通商品(11秒后):" + goodsCache.getIfPresent("goods:2")); // 存在
System.out.println("预售商品(11秒后):" + goodsCache.getIfPresent("goods:3")); // 存在
// 更新普通商品:过期时间重置为5分钟
Thread.sleep(1000);
goodsCache.put("goods:2", new Goods("2", "普通夏季T恤(降价)", GoodsExpiry.GoodsType.NORMAL));
System.out.println("更新后普通商品:" + goodsCache.getIfPresent("goods:2")); // 存在
// 再等301秒(超过原5分钟):普通商品仍未过期(因更新重置了时间)
Thread.sleep(301000);
System.out.println("301秒后普通商品:" + goodsCache.getIfPresent("goods:2")); // 存在
}
}
2.3.3 适用场景与注意事项
-
适用场景:
- 多维度数据缓存(如不同类型、优先级、来源的数据,需差异化过期);
- 动态过期需求(如根据数据更新频率、用户等级调整过期时间);
- 复杂业务规则场景(如会员数据过期时间=会员等级×24小时)。
-
注意事项:
- 自定义逻辑需轻量:
Expiry
接口的方法会在缓存操作(get/put/update)时同步执行,若逻辑复杂会阻塞缓存操作,影响性能; - 避免动态修改过期时间:
Expiry
接口的方法仅在缓存操作时触发,若需动态调整已存在条目的过期时间,需通过put()
重新写入条目; - 时间单位需注意:Caffeine内部使用"纳秒"作为时间单位,自定义
Expiry
时需将业务时长(如秒、分钟)转换为纳秒(1秒=1000_000_000纳秒),避免因单位错误导致过期时间异常。
- 自定义逻辑需轻量:
三、过期策略的组合使用与场景适配进阶
在实际业务中,单一过期策略往往无法满足复杂需求,此时需要通过"策略组合"或"策略与其他缓存特性结合"实现更灵活的缓存管理。
3.1 策略组合的核心逻辑与案例
Caffeine不支持同时配置多个基础过期策略(如同时设置expireAfterWrite
和expireAfterAccess
),但可通过以下两种方式实现"组合效果":
3.1.1 基于expireAfter
自定义组合逻辑
通过expireAfter
的Expiry
接口,融合"写入过期"与"访问过期"的逻辑,实现"双重过期控制"。
场景需求:用户会话缓存需满足两个条件:
- 自上次写入(登录/刷新会话)后不超过2小时;
- 自上次访问后不超过30分钟;
只要满足任一条件过期,会话即失效。
代码实现:
java
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;
// 自定义会话过期逻辑:同时满足写入过期和访问过期
class SessionExpiry implements Expiry<String, UserSession> {
// 最大写入过期时长:2小时(转换为纳秒)
private static final long MAX_WRITE_EXPIRE_NS = TimeUnit.HOURS.toNanos(2);
// 最大访问过期时长:30分钟(转换为纳秒)
private static final long MAX_ACCESS_EXPIRE_NS = TimeUnit.MINUTES.toNanos(30);
// 条目创建时:记录创建时间(写入时间),初始过期时间取两者最小值
@Override
public long expireAfterCreate(String sessionId, UserSession session, long currentTime) {
session.setLastWriteTime(currentTime); // 记录写入时间
session.setLastAccessTime(currentTime); // 记录访问时间
return Math.min(MAX_WRITE_EXPIRE_NS, MAX_ACCESS_EXPIRE_NS);
}
// 条目更新时:更新写入时间,重新计算过期时间
@Override
public long expireAfterUpdate(String sessionId, UserSession session, long currentTime, long currentDuration) {
session.setLastWriteTime(currentTime);
session.setLastAccessTime(currentTime);
return Math.min(MAX_WRITE_EXPIRE_NS, MAX_ACCESS_EXPIRE_NS);
}
// 条目访问时:更新访问时间,计算剩余写入过期时间和剩余访问过期时间,取较小值
@Override
public long expireAfterRead(String sessionId, UserSession session, long currentTime, long currentDuration) {
session.setLastAccessTime(currentTime);
// 剩余写入过期时间 = 写入时间 + 最大写入过期时长 - 当前时间
long remainingWriteExpire = session.getLastWriteTime() + MAX_WRITE_EXPIRE_NS - currentTime;
// 剩余访问过期时间 = 访问时间 + 最大访问过期时长 - 当前时间
long remainingAccessExpire = session.getLastAccessTime() + MAX_ACCESS_EXPIRE_NS - currentTime;
// 返回剩余时间的较小值,确保任一条件满足即过期
return Math.min(remainingWriteExpire, remainingAccessExpire);
}
}
// 用户会话实体类
class UserSession {
private String userId;
private long lastWriteTime; // 最后写入时间(纳秒)
private long lastAccessTime; // 最后访问时间(纳秒)
// 构造器、getter、setter省略
public UserSession(String userId) {
this.userId = userId;
}
// getter/setter
public long getLastWriteTime() { return lastWriteTime; }
public void setLastWriteTime(long lastWriteTime) { this.lastWriteTime = lastWriteTime; }
public long getLastAccessTime() { return lastAccessTime; }
public void setLastAccessTime(long lastAccessTime) { this.lastAccessTime = lastAccessTime; }
@Override
public String toString() {
return "UserSession{userId='" + userId + "'}";
}
}
// 测试类
public class SessionCacheDemo {
public static void main(String[] args) throws InterruptedException {
Cache<String, UserSession> sessionCache = Caffeine.newBuilder()
.expireAfter(new SessionExpiry())
.maximumSize(10000)
.recordStats()
.build();
// 创建会话(10:00)
sessionCache.put("session:abc123", new UserSession("user:1001"));
System.out.println("初始会话:" + sessionCache.getIfPresent("session:abc123")); // 存在
// 2小时5分钟后访问(超过写入过期时长):会话过期
Thread.sleep(TimeUnit.HOURS.toMillis(2) + TimeUnit.MINUTES.toMillis(5));
System.out.println("2小时5分钟后访问:" + sessionCache.getIfPresent("session:abc123")); // null
// 重新创建会话
sessionCache.put("session:def456", new UserSession("user:1002"));
// 1小时后访问(未超过写入过期),之后35分钟无访问(超过访问过期)
Thread.sleep(TimeUnit.HOURS.toMillis(1));
System.out.println("1小时后访问:" + sessionCache.getIfPresent("session:def456")); // 存在
Thread.sleep(TimeUnit.MINUTES.toMillis(35));
System.out.println("35分钟无访问后:" + sessionCache.getIfPresent("session:def456")); // null
}
}
3.1.2 策略与"手动失效"结合
对于需要"主动触发过期"的场景(如商品下架、活动结束),可在基础过期策略之上,通过invalidate()
或invalidateAll()
手动删除缓存条目,实现"自动过期+手动失效"的双重控制。
代码示例:
java
// 1. 配置基础过期策略(expireAfterWrite)
Cache<String, String> goodsCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(1000)
.build();
// 2. 正常业务:写入商品缓存
goodsCache.put("goods:1001", "夏季T恤(在售)");
// 3. 商品下架(手动失效):无需等待自动过期,立即删除
public void offlineGoods(String goodsId) {
// 数据库更新商品状态为"下架"
updateGoodsStatus(goodsId, "offline");
// 手动失效缓存,避免用户访问到下架商品
goodsCache.invalidate("goods:" + goodsId);
System.out.println("商品" + goodsId + "缓存已手动失效");
}
// 调用手动失效方法
offlineGoods("1001");
System.out.println("下架后获取商品:" + goodsCache.getIfPresent("goods:1001")); // null
3.2 不同业务场景的策略选型指南
业务场景 | 推荐策略 | 核心原因 |
---|---|---|
商品基础信息(价格、规格) | expireAfterWrite | 数据更新后需稳定保留,访问不改变过期时间,避免频繁加载 |
首页热门商品、高频查询数据 | expireAfterAccess | 热点数据通过访问延长过期,减少重复加载,闲置时自动释放内存 |
秒杀商品、实时活动数据 | expireAfter(自定义) | 需按商品类型设置短过期时间,且更新/访问不延长过期,确保数据实时性 |
用户会话、临时操作记录 | expireAfter(组合逻辑) | 需同时满足"写入过期"和"访问过期",确保会话安全且资源不浪费 |
多类型数据混合缓存 | expireAfter(自定义) | 支持为不同类型数据设置差异化过期,避免单一策略导致部分数据过期不合理 |
四、生产环境的优化建议与监控调优
在生产环境中,缓存过期策略的合理性直接影响系统稳定性与性能,需结合以下优化建议与监控手段确保缓存体系高效运行。
4.1 生产环境优化建议
4.1.1 合理设置过期时长与容量
- 过期时长 :避免"过短"或"过长":
- 过短:导致缓存命中率低,数据库压力增大(如秒杀商品可设10-30秒,普通商品设5-30分钟);
- 过长:导致数据陈旧,与数据库不一致(如实时库存数据不建议超过5分钟)。
- 缓存容量 :通过
maximumSize
或maximumWeight
限制缓存容量,避免内存溢出:- 建议根据服务内存大小设置(如服务内存8G,缓存容量设为2-3G);
- 配合
weigher
实现按"权重"限制容量(如大对象权重高,小对象权重低)。
代码示例:按权重限制缓存容量
java
Cache<String, byte[]> bigDataCache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumWeight(1024 * 1024 * 1024) // 总权重:1GB(假设1字节=1权重)
.weigher((key, value) -> value.length) // 按数据大小计算权重
.build();
4.1.2 避免缓存雪崩与穿透
- 缓存雪崩 :通过"过期时间随机化"避免大量条目同时过期:
- 在自定义
Expiry
中,为过期时长增加随机偏移(如±10%),分散过期时间; - 示例:普通商品过期5分钟,实际设置为4.5-5.5分钟。
- 在自定义
- 缓存穿透 :结合"布隆过滤器"与"空值缓存":
- 对不存在的key,缓存空值并设置短过期(如30秒),避免频繁查询数据库;
- 布隆过滤器过滤不存在的key,直接返回空,不进入缓存逻辑。
代码示例:空值缓存与随机过期
java
// 空值缓存 + 随机过期
Cache<String, String> productCache = Caffeine.newBuilder()
.expireAfterWrite((key, value, currentTime) -> {
long baseExpire = TimeUnit.MINUTES.toNanos(5);
// 随机偏移:±10%
long randomOffset = (long) (baseExpire * (Math.random() * 0.2 - 0.1));
// 空值设短过期:30秒
if (value == null || value.isEmpty()) {
return TimeUnit.SECONDS.toNanos(30);
}
return baseExpire + randomOffset;
})
.build();
// 业务逻辑:查询商品,不存在则缓存空值
public String getProduct(String productId) {
String product = productCache.get(productId, key -> {
String dbProduct = queryProductFromDB(key);
return dbProduct == null ? "" : dbProduct; // 空值返回空字符串
});
return product.isEmpty() ? null : product;
}
4.1.3 异步加载与重试机制
对于缓存过期后的数据加载,建议使用AsyncCache
实现异步加载,避免阻塞业务线程;同时增加重试机制,应对数据库临时不可用。
代码示例:异步加载与重试
java
import com.github.benmanes.caffeine.cache.AsyncCache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
// 配置异步缓存
AsyncCache<String, String> asyncProductCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000)
.buildAsync();
// 异步加载,带重试(最多3次)
public CompletableFuture<String> getProductAsync(String productId) {
return asyncProductCache.get(productId, key -> loadProductWithRetry(key, 3));
}
// 带重试的加载逻辑
private String loadProductWithRetry(String productId, int retryCount) {
try {
return queryProductFromDB(productId);
} catch (Exception e) {
if (retryCount > 0) {
// 重试间隔:100ms * (4 - retryCount)(重试次数越多,间隔越长)
try {
TimeUnit.MILLISECONDS.sleep(100 * (4 - retryCount));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
return loadProductWithRetry(productId, retryCount - 1);
}
throw new RuntimeException("加载商品失败:" + productId, e);
}
}
// 业务调用
getProductAsync("1001").thenAccept(product -> {
System.out.println("异步获取商品:" + product);
});
4.2 监控与调优手段
4.2.1 开启缓存统计
通过recordStats()
开启缓存统计,核心指标包括:
- 命中率(hitRate):缓存命中次数/总访问次数,建议维持在90%以上;
- 过期淘汰数(evictionCount):因过期被淘汰的条目数,过高可能表示过期时长过短;
- 加载失败数(loadFailureCount):缓存加载失败次数,需排查数据库或加载逻辑问题。
代码示例:统计指标输出
java
Cache<String, String> productCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000)
.recordStats()
.build();
// 定时输出统计指标(每10秒)
new Thread(() -> {
while (true) {
var stats = productCache.stats();
System.out.println("=== 缓存统计(10秒) ===");
System.out.println("命中率:" + String.format("%.2f%%", stats.hitRate() * 100));
System.out.println("过期淘汰数:" + stats.evictionCount());
System.out.println("加载失败数:" + stats.loadFailureCount());
System.out.println("平均加载时间(ms):" + stats.averageLoadPenalty() / 1000_000);
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
4.2.2 结合监控系统告警
将缓存统计指标接入Prometheus+Grafana或ELK等监控系统,设置阈值告警:
- 命中率低于80%时告警,需检查过期时长或缓存容量;
- 加载失败数连续5分钟大于0时告警,需排查数据库连接或SQL问题;
- 过期淘汰数突增时告警,需确认是否有大量数据更新或过期策略调整。
4.2.3 压测验证策略合理性
在上线前通过压测工具(如JMeter、Gatling)模拟高并发场景,验证过期策略:
- 压测不同过期时长下的命中率与数据库压力,选择最优值;
- 模拟缓存失效(如手动触发
invalidateAll()
),验证系统是否能承受缓存穿透压力; - 压测缓存容量达到阈值时的淘汰性能,确保无内存溢出或性能骤降。
五、总结
Caffeine的三种缓存过期策略各有侧重,expireAfterWrite
适合"数据更新驱动过期"的场景,expireAfterAccess
适合"热点数据访问驱动过期"的场景,而expireAfter
则通过自定义逻辑满足复杂业务需求。在实际应用中,需结合业务场景选择合适策略,必要时通过"策略组合+手动失效"实现更灵活的缓存管理。