Caffeine三种缓存过期策略总结:原理、实战与调优

🌈 我是"没事学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.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的过期管理基于时间驱动+事件触发的双重机制:

  1. 时间基准:所有过期策略均以"时间戳"为核心判断依据,通过维护每个缓存条目的创建/更新/访问时间戳,结合预设时长计算过期时间点;
  2. 清理时机 :过期条目并非实时删除,而是在以下时机触发清理:
    • 主动访问(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. 避免"缓存雪崩":由于所有条目过期时间相对分散(基于写入时间),天然具备抗雪崩能力;
    3. 写操作较少但数据时效性要求高的场景(如用户订单状态,更新后保留1小时)。
  • 注意事项

    1. 若数据长期不更新,即使频繁访问也会过期,可能导致"缓存穿透"(需配合布隆过滤器);
    2. 不适用于"热点数据"(如首页热门商品),频繁访问但不更新会导致频繁过期,命中率下降。

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 适用场景与注意事项
  • 适用场景

    1. 热点数据缓存(如首页Banner、热门文章),通过访问延长过期时间,减少重复加载;
    2. 会话级缓存(如用户登录态、临时操作记录),用户活跃时保留,闲置时自动清理;
    3. 资源敏感场景(如内存有限的服务),通过"闲置过期"释放未使用的缓存空间。
  • 注意事项

    1. 可能导致"缓存堆积":若某数据长期被访问,会一直不过期,占用内存(需配合maximumSize限制容量);
    2. 不适用于"数据时效性强且更新频率低"的场景(如实时库存,即使频繁访问也需定期更新)。

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 适用场景与注意事项
  • 适用场景

    1. 多维度数据缓存(如不同类型、优先级、来源的数据,需差异化过期);
    2. 动态过期需求(如根据数据更新频率、用户等级调整过期时间);
    3. 复杂业务规则场景(如会员数据过期时间=会员等级×24小时)。
  • 注意事项

    1. 自定义逻辑需轻量:Expiry接口的方法会在缓存操作(get/put/update)时同步执行,若逻辑复杂会阻塞缓存操作,影响性能;
    2. 避免动态修改过期时间:Expiry接口的方法仅在缓存操作时触发,若需动态调整已存在条目的过期时间,需通过put()重新写入条目;
    3. 时间单位需注意:Caffeine内部使用"纳秒"作为时间单位,自定义Expiry时需将业务时长(如秒、分钟)转换为纳秒(1秒=1000_000_000纳秒),避免因单位错误导致过期时间异常。

三、过期策略的组合使用与场景适配进阶

在实际业务中,单一过期策略往往无法满足复杂需求,此时需要通过"策略组合"或"策略与其他缓存特性结合"实现更灵活的缓存管理。

3.1 策略组合的核心逻辑与案例

Caffeine不支持同时配置多个基础过期策略(如同时设置expireAfterWriteexpireAfterAccess),但可通过以下两种方式实现"组合效果":

3.1.1 基于expireAfter自定义组合逻辑

通过expireAfterExpiry接口,融合"写入过期"与"访问过期"的逻辑,实现"双重过期控制"。

场景需求:用户会话缓存需满足两个条件:

  1. 自上次写入(登录/刷新会话)后不超过2小时;
  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分钟)。
  • 缓存容量 :通过maximumSizemaximumWeight限制缓存容量,避免内存溢出:
    • 建议根据服务内存大小设置(如服务内存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则通过自定义逻辑满足复杂业务需求。在实际应用中,需结合业务场景选择合适策略,必要时通过"策略组合+手动失效"实现更灵活的缓存管理。

相关推荐
Micro麦可乐3 小时前
为什么两个看似相等的 Integer 却不相等?一次诡异的缓存折扣商品 BUG 排查
java·缓存·bug·包装类判断·integer判断
Never_z&y3 小时前
HTTP学习之路:代理中的缓存投毒
网络·网络安全·缓存
杨杨杨大侠3 小时前
手把手教你写 httpclient 框架(七)- 异步处理与性能优化
java·http·github
ajassi20003 小时前
开源 java android app 开发(十四)自定义绘图控件--波形图
android·java·开源
一叶飘零_sweeeet3 小时前
从 0 到 1 精通 SkyWalking:分布式系统的 “透视镜“ 技术全解析
java·skywalking
七夜zippoe3 小时前
Java 生态监控体系实战:Prometheus+Grafana+SkyWalking 整合全指南(三)
java·grafana·prometheus
陈遇巧4 小时前
Spring Framework
java·笔记·spring
我是华为OD~HR~栗栗呀4 小时前
20届-高级开发(华为oD)-Java面经
java·c++·后端·python·华为od·华为
摇滚侠5 小时前
java.lang.RuntimeException: java.lang.OutOfMemoryError
java·开发语言·intellij-idea