资讯中心

JCache过期策略详解:AccessedExpiryPolicy滑动过期原理与实战

📅 2026/9/24 22:55:11
JCache过期策略详解:AccessedExpiryPolicy滑动过期原理与实战
“现场说说JCache里的过期策略AccessedExpiryPolicy是怎么工作的”这是在2025年5月的基础篇面试题也是我这些年在Java后端面试中遇到频率相当高的一道题。别看它只是“解释一个策略”真往深了问能从滑动过期扯到缓存雪崩、从JSR-107规范扯到具体缓存实现的差异。今天就把这道题彻底讲透既是给准备面试的朋友一份可以直接背的答案也把实际项目中配置和排查的坑一并整理了。这道题适合正在准备Java中高级面试的工程师也适合那些在项目里用了Caffeine、Hazelcast、Ehcache 3这些实现了JSR-107标准的缓存框架但一直没有系统梳理过过期策略的开发者。看完这篇文章你能说清楚AccessedExpiryPolicy和CreatedExpiryPolicy的本质区别能自己写出一段标准API配置代码还能回答面试官大概率追问的几个问题。1. JCache面试题解析从JSR-107到Expiry Policy1.1 什么是JCache为什么面试官爱问它JCache是Java Community Process发布的缓存API标准对应的规范编号是JSR-107。它的核心价值是把缓存操作抽象成一套统一接口落点在javax.cache包。你写业务代码时面向这套API底层可以随时切换不同的缓存实现不用动业务逻辑。面试官喜欢拿这个做文章是因为它能检验一个人到底是在“用缓存”还是在“懂缓存”。很多开发者在项目里加了缓存注解配置了几个过期时间就以为缓存搞懂了。但一问到标准API层级的过期策略接口立刻就露馅了——因为他们用的都是Spring Cache注解甚至根本没接触过JCache底层接口。这道题天然能把“只会用”和“理解原理”的候选人区分开。另一个原因是它背后藏着缓存设计中最核心的几个命题数据什么时候失效、失效后怎么处理、不同业务场景该用哪种失效策略。JSR-107把这套东西标准化了所以面试官从它切入既能考察基础知识又能顺藤摸瓜聊到分布式缓存、数据一致性这些实战话题。1.2 Expiry Policy在JCache中的地位在JCache的世界里Expiry Policy负责回答一个问题缓存条目什么时候应该被视为过期。它不是简单地给你一个“设置过期时间”的方法而是定义了三个关键时机由ExpiryPolicy接口统一描述getExpiryForCreation条目被放入缓存时决定初始过期时间getExpiryForAccess条目被读取命中时决定是否需要重置过期时间getExpiryForUpdate条目被更新时决定是否需要重置过期时间这三个方法统一返回一个Duration对象代表当前操作之后条目的存活时长。返回null有一个特殊含义表示“保持上一个策略决定的结果不变”。这套设计极其灵活把“创建时定生死”和“每次操作后续命”的能力都解耦开了。也正是因为有这三个方法JCache才能组合出不同的过期行为固定过期、滑动过期、修改后过期、访问或修改后过期、永不过期。面试题中的AccessedExpiryPolicy就是“访问后重置过期时间”这个语义的官方实现。1.3 标准实现速览JCache规范内置了五个标准策略它们都在javax.cache.expiry包下策略类核心语义适用场景CreatedExpiryPolicy从创建时间开始计算固定生命周期验证码、一次性票据AccessedExpiryPolicy每次读取后重置过期时间用户会话、热点商品ModifiedExpiryPolicy每次修改后重置过期时间配置项、价格类数据TouchedExpiryPolicy访问或修改都重置过期时间在线状态、活跃用户标记EternalExpiryPolicy永不过期静态字典、几乎不变的基础数据记忆窍门很简单Accessed是“读到就续命”Modified是“改到就续命”Touched是“读到改到都续命”。面试时先把这个对比关系讲清楚考官就知道你掌握的不只是一个类名而是整个策略家族。2. AccessedExpiryPolicy深度拆解2.1 核心机制滑动过期与访问刷新AccessedExpiryPolicy的官方定位是“基于访问时间的过期策略”实现的是一种典型滑动过期机制。它做的事情可以用一句话概括只要一个缓存条目在被读取时是未过期的读取动作完成后系统就把它的过期时间点往后推推到你配置的那个Duration之后。举个例子。我把会话数据放进缓存设置AccessedExpiryPolicy(Duration.ofMinutes(30))表示“连续30分钟没人访问就失效”。用户在T1时刻登录产生了一个会话key过期时间是T130分钟。用户在第20分钟时发起了请求此时key被get命中过期时间立刻变成T230分钟T2是这次读取时刻。只要用户保持这个访问频率会话就可以一直续下去一旦超过30分钟没有访问下次get时发现已经过期缓存就会认为条目不存在并把该条目删掉。这种策略和固定过期最大的区别在于固定过期是“从生到死只有一个截止点”滑动过期是“每次被使用都在刷新截止点”。放到生活里类比一个是停车票上写的“限停2小时离场时间不变”另一个是健身房的“会员每次来锻炼有效期都往后延一个月”。2.2 源码层面的关键细节看JCache官方实现AccessedExpiryPolicy本身代码非常简洁核心逻辑就是对ExpiryPolicy接口三个方法的实现public final class AccessedExpiryPolicy implements ExpiryPolicy { private final Duration expiryDuration; public AccessedExpiryPolicy(Duration expiryDuration) { this.expiryDuration expiryDuration; } Override public Duration getExpiryForCreation() { // 创建时直接使用配置的过期时长 return expiryDuration; } Override public Duration getExpiryForAccess() { // 访问命中时同样返回配置的过期时长表示重置 return expiryDuration; } Override public Duration getExpiryForUpdate() { // 更新操作不改变过期时间返回null表示沿用当前策略 return null; } }注意几个容易被忽略的细节。第一getExpiryForUpdate返回null并不代表“不过期”而是代表“更新这条记录时不要重新设置过期时间”。也就是说我修改了这个key对应的value它的剩余存活时间还是按照之前创建或访问后确定的时间点走。这一点和ModifiedExpiryPolicy形成鲜明对比ModifiedExpiryPolicy在更新时会重新配置一个完整时长。第二Duration有两个静态常量需要注意Duration.ZERO表示立即过期Duration.ETERNAL表示永不过期。在创建AccessedExpiryPolicy时如果传入Duration.ZERO那么这个缓存条目创建后很快就会被视为过期。实际配置时几乎不会这么用但是面试官偶尔会拿这个来考察你是否看过API定义。第三工厂方法。JCache规范建议通过策略类的factoryOf静态方法创建工厂对象交给MutableConfiguration的setExpiryPolicyFactory。直接new策略类在代码里也能用但工厂方法更符合JCache的配置模型因为配置对象需要延迟到缓存创建时才真正实例化策略config.setExpiryPolicyFactory(AccessedExpiryPolicy.factoryOf(Duration.ONE_HOUR));2.3 和CreatedExpiryPolicy、TouchedExpiryPolicy的差异对比先把三个最容易被混的策略放在一起对比操作类型CreatedExpiryPolicyAccessedExpiryPolicyTouchedExpiryPolicy创建条目时设置固定时长设置固定时长设置固定时长读取命中时不变重置固定时长重置固定时长更新条目时不变不变重置固定时长典型语义固定过期读续期读写都续期很多面试者能说出AccessedExpiryPolicy是“访问后刷新”但被追问“那TouchedExpiryPolicy呢”就卡住了。其实区别就卡在更新操作上如果你只希望读操作维持热点数据的活跃度用Accessed如果你希望每次修改数据也算作一次活跃信号用Touched。还有一种比较容易踩的混淆场景很多人以为CreatedExpiryPolicy的“固定过期”是“从创建那一刻开始倒计时中途怎么操作都不影响”。这一点理解是对的。但这里有个隐藏坑如果缓存条目因为某种原因被覆盖put同一个key在JCache规范里这算“更新”。对于CreatedExpiryPolicygetExpiryForUpdate返回null所以新值会沿用旧值剩余的时间而不是重新计时。很多做短时缓存的人在这里栽过跟头以为put一下就重新计算了实际并没有。你要重新计时得用ModifiedExpiryPolicy或TouchedExpiryPolicy。3. 实战配置与代码示例3.1 用标准API配置AccessedExpiryPolicy下面给出一段可以直接运行的JCache标准API配置代码场景是模拟用户会话缓存用户登录后生成会话ID会话只要持续被访问就不会超时30分钟没有活跃则自动失效。import javax.cache.Cache; import javax.cache.CacheManager; import javax.cache.Caching; import javax.cache.configuration.MutableConfiguration; import javax.cache.expiry.AccessedExpiryPolicy; import javax.cache.expiry.Duration; import java.util.concurrent.TimeUnit; public class SessionCacheExample { public static void main(String[] args) { // 1. 获取默认CacheManager CacheManager cacheManager Caching.getCachingProvider().getCacheManager(); // 2. 定义缓存配置 MutableConfigurationString, Object config new MutableConfiguration(); config.setTypes(String.class, Object.class) .setExpiryPolicyFactory( AccessedExpiryPolicy.factoryOf( new Duration(TimeUnit.MINUTES, 30) ) ) .setStatisticsEnabled(true); // 3. 创建缓存 CacheString, Object sessionCache cacheManager.createCache(sessionCache, config); // 4. 模拟登录放入会话数据 String sessionId SESSION-20250525-001; sessionCache.put(sessionId, new Object()); // 5. 模拟后续请求访问 Object sessionData sessionCache.get(sessionId); if (sessionData ! null) { System.out.println(会话有效继续续期); } // 6. 关闭资源 cacheManager.close(); } }这段代码有几个地方值得注意。setTypes一定要显式指定key和value类型否则部分实现会返回类型不匹配的警告。setStatisticsEnabled打开后才能拿到命中率、过期条目数量这些监控指标生产环境强烈建议开启。最后的cacheManager.close()别忘很多测试代码不关导致线程一直挂着。3.2 在Spring Boot项目中接入JCache实现实际项目用纯API的JCache写着写着就会发现Spring的CacheManager抽象更贴合业务。Spring Cache支持JSR-107注解但底层缓存实现仍可以走JCache标准。你可以在引入了JCache实现依赖比如Hazelcast、Ehcache 3之后通过配置类显式声明一个使用AccessedExpiryPolicy的缓存管理器Configuration public class CacheConfig { Bean public CacheManager cacheManager() { CachingProvider cachingProvider Caching.getCachingProvider(); CacheManager cacheManager cachingProvider.getCacheManager(); MutableConfigurationString, Object sessionConfig new MutableConfiguration(); sessionConfig.setTypes(String.class, Object.class); sessionConfig.setExpiryPolicyFactory( AccessedExpiryPolicy.factoryOf(Duration.ONE_HOUR) ); cacheManager.createCache(userSession, sessionConfig); return new JCacheCacheManager(cacheManager); } }实际开发中我更推荐直接在配置文件里声明过期策略而不是用Java代码写死。Ehcache 3的jc.xml配置支持以下形式config xmlnshttp://www.ehcache.org/v3 cache aliasuserSession expiry ttl unitminutes30/ttl /expiry resources heap unitentries10000/heap offheap unitMB64/offheap /resources /cache /config等等这里要区分一下。Ehcache默认的expiry类型是ETERNAL配置ttl表示固定过期也就是CreatedExpiryPolicy语义。如果你想要AccessedExpiryPolicy的滑动过期需要用class标签指向一个自定义的ExpiryPolicy实现类或者使用Ehcache专门的配置expiry classcom.example.CustomAccessedExpiryPolicy/class /expiry而我通常的做法是直接写一个复用AccessedExpiryPolicy的工厂类让它既满足Ehcache的SPI要求又保留JCache的标准语义。每次配置新的缓存只需要改动配置文件里的类名和时长参数不必重新编译。3.3 参数计算与配置经验面试时候选人最容易被追问但最缺乏经验的就是这里你凭什么把这个过期时间设成30分钟而不是5分钟或者1小时我的建议是从业务指标倒推。拿用户会话举例你可以统计产品的用户平均访问间隔。如果大多数活跃用户的请求间隔在5到20分钟之间那就把滑动过期窗口设在30分钟留出1.5倍到2倍的余量。如果设置成5分钟WiFi切换、网络抖动就可能让真实用户的会话被误杀如果设置成24小时就会导致后端权限变更后用户端要等一天才能感知。具体计算时可以用这个思路先看业务容忍的最长无状态时间 T这个时间超过后就认为“用户已经离开”再看用户请求的平均间隔 I配置的Duration D 应该在 max(2I, T/2) 到 T 之间举个例子如果业务认为用户超过1小时没操作就算掉线T就是60分钟用户平均请求间隔是10分钟I就是10分钟。那么D取30分钟比较稳妥既不会因为一次短等待就过期也不会拖到60分钟才验证身份。还有一个实际经验使用AccessedExpiryPolicy时要配合缓存的大小上限一起使用。因为滑动过期有个天然的隐患——一个key被频繁访问就可能一直活着。如果某个热点key长期霸占内存而其他key频繁过期缓存里可能出现“数据分布不均”。JCache的MutableConfiguration里没有直接设置最大条目的标准API但具体实现比如Hazelcast、Ehcache都有max-size配置。实践上我建议在缓存创建时同时设置max-size和驱逐策略让极端情况下系统还有兜底方案。4. 面试题常见追问与避坑指南4.1 面试官常问的跟进问题“AccessedExpiryPolicy遇到热点key会有什么问题”可能是被问到最多的追问。答不上来就很可惜因为这里有一个经典场景某热门商品详情被大量请求缓存里的商品信息因为每次访问都在续期过期时间永远无法到来。这在滑动过期策略里是正常表现但会导致两个问题第一如果同时配合了固定过期策略做“定时刷新价格”两个策略可能会打架。第二如果缓存没有大小上限大量热点key长期不淘汰内存会持续增长最终触发Full GC甚至OOM。另一个高频追问是“getAll方法会不会触发访问刷新”。这个问题在不同实现里答案并不统一。JCache规范里Cache.getAll是一个批量读取方法有些实现会把里面的每个条目都当作一次访问调用对应的getExpiryForAccess而另一些实现为了性能会跳过访问时间更新。面试时最好这样回答规范允许实现自行决定但要清楚这一点对业务的影响——如果你依赖滑动过期来清理会话而底层实现批量读取时不刷新可能会导致会话过早失效。还有一题是“过期条目是立刻被物理删除吗”。这题我会先答“不是”然后解释JCache标准没有强制要求后台线程实时扫描大多数实现采用惰性删除。也就是说get一个已经过期的key实现发现它过期了才会真正从存储中移除。如果一直没人访问这个key会占用内存直到被后续操作碰到或触发驱逐。意识到这个问题之后给定高并发场景设计缓存时就要在“惰性删除定期清理”之间做取舍。4.2 使用AccessedExpiryPolicy的坑前面重点讲了概念和代码但真正落地时有几个坑是我在实际项目中踩过的。第一个坑在没有统计信息的情况下调试过期。很多项目只配置了缓存没开统计。结果线上反馈“缓存没过期”排查时看不到任何指标只能靠猜。建议配置中开启setStatisticsEnabled(true)并通过CacheStatistics.getExpirations()观察过期条目数配合命中率变化判断策略是否生效。第二个坑依赖的底层实现会改变语义。同样是JCache标准APIHazelcast和Ehcache 3对“访问”的定义有时候并不同。比如有些实现把putIfAbsent、containsKey也算作访问有些不算。这就意味着如果业务代码大量使用containsKey判断后不读取数据部分实现里这个key的生命周期其实已经刷新了。要避免这个坑不要在同一个缓存里混用多种操作语义或者明确查阅实现文档。第三个坑策略Duration使用错误单位。JCache的Duration构造器第一个参数是TimeUnit第二个才是数量值。很容易写成new Duration(30, TimeUnit.MINUTES)这样单位顺序完全反了。我以前接手过一个服务业务方写成了new Duration(30, TimeUnit.SECONDS)导致会话没过多久就过期排查了一整天。这种低级错误在面试代码题里也很常见写的时候多看一眼被问参数含义时也不慌。第四个坑更新操作不重置过期时间。这一点前面提到过但在实战中仍然高频踩雷。错误的需求理解是“我对缓存里的数据做了一次更新希望它重新活30分钟”结果用AccessedExpiryPolicy发现缓存还是很快就过期了。因为Access策略的getExpiryForUpdate返回null。如果这个场景需求明确要立刻换用TouchedExpiryPolicy或ModifiedExpiryPolicy而不是在业务代码里手动重新put一遍来骗刷新。4.3 问题排查方法缓存过期问题排查起来最忌讳的就是拍脑袋。我自己的排查顺序一般是先看统计指标定位是“大量提前过期”还是“根本不过期”。如果是提前过期打开日志查一下每次写入和访问时实际设置的Duration值确认配置来源。重点检查是不是存在多个CacheManager同一个缓存名被重新创建过导致配置覆盖。这个问题在Spring Boot JCache场景下尤其常见多个配置类都声明了同名的CacheManager后注册的覆盖先注册的。再看代码路径。查找所有对该缓存的写操作确认没有在写操作后又手动重置了过期时间。很多项目里会有人为了“保证一致”在写入后主动调用cache.put这相当于触发一次更新如果是AccessedExpiryPolicy且更新返回null不会带来续期效果反而增加了性能损耗。最后看底层实现日志。Hazelcast等框架都有过期相关的事件监听开启后能看到具体哪个条目在什么时间被触发过期可以精确定位问题发生在“访问时”还是“后台清理时”。这个手段比较深层面试能提到会显得你对分布式缓存实现有实际接触。5. 每日一题延伸学习建议与扩展方向5.1 从一道题到一套知识体系只背下AccessedExpiryPolicy的概念面试其实走不远。JCache这道题实际上是“缓存基础知识体系”的一个入口。面试官既然问了过期策略接下来很可能就会顺藤摸瓜问到缓存击穿、缓存雪崩、缓存一致性以及Redis中的过期策略和JCache的区别。我建议沿着这条线把知识网补全先彻底搞懂ExpiryPolicy三个方法和五个标准实现再对比本地缓存与分布式缓存的过期实现差异然后研究Spring Cache对JSR-107的封装搞明白Cacheable底层到底发生了什么最后拿Redis的EXPIRE、滑动窗口、淘汰策略做对比理解不同中间件在不同场景下的设计取舍这个过程不用专门找文档从头读拿一遍自己的项目代码看每个缓存注解对应的缓存配置挨个思考“这个key的过期策略是什么合理吗”比单纯刷题效率高得多。5.2 后续可以深挖的方向如果这道题你已经完全吃透建议下一步关注JCache的CacheLoader和CacheWriter它们解决的是“缓存不命中时怎么加载”以及“缓存更新时如何同步到数据库”。这两个问题在实际项目里比过期策略更常见面试被问到的概率也更高。另一个值得深挖的方向是高并发场景下的缓存一致性。JCache标准本身不提供分布式一致性保证它只定义了API。所以在分布式架构里你依然要自己处理多个副本之间的过期时间同步问题。这部分知识没有标准答案要多读Hazelcast、Redis集群这类的实现源码和设计文档自己总结出一套取舍逻辑来。学习途径上我建议把JSR-107官方规范文档和OpenJDK的Cache实现源码对照看一遍。规范描述了“应该是什么样”源码则展示“实际上怎么实现”。两者对照完你对缓存的理解会从面试八股提升到能动手设计的程度。最后分享一个我在面试和带新人时反复使用的方法拿到一道缓存相关的题目先问自己三个问题——这个数据真的需要缓存吗缓存多久合理缓存失效后系统能不能正常降级。把这三个问题想明白了不管面试官怎么换问法你都能答到点子上。这就是从“背题”到“理解”的分水岭。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案