removeeldestentry不是自动过期开关,它仅在put后size超限时触发并删除最老entry;过期需自行在value中存储时间戳,并在该方法中比对当前时间判断是否删除。

为什么 removeEldestEntry 不是“自动过期”的开关
它本身不处理时间,只管“要不要删最老的 entry”。所谓“过期”,得你自己在 put 时把时间戳塞进去,再在 removeEldestEntry 里比对当前时间和这个时间戳。很多人卡在这儿,以为重写完就万事大吉,结果缓存永远不清理——因为压根没存时间,也没做时间判断。
- 必须用自定义 value(比如
Map.Entry<k timedvalue></k>)或包装类,把timestamp和真实值一起存 -
removeEldestEntry的触发时机是每次put后、且 map size >maxSize时,不是定时轮询 - 它删的是
LinkedHashMap尾部(最老)的 entry,跟“是否过期”无关,你得自己加逻辑决定“该不该删”
怎么让 removeEldestEntry 判断是否真正过期
核心是在重写方法里取出 entry 的时间戳,和当前时间比较。注意:别直接用 System.currentTimeMillis() 做硬编码,容易被测试干扰;更别用 new Date(),性能差还难 mock。
- 推荐把时间源抽成可注入的接口(如
Clock),默认用Clock.systemUTC() - entry 的 value 必须能拿到时间字段,例如:
((TimedValue)v).getExpiryTime() - 返回
true才会删除,所以条件是:currentTime > expiryTime或size() > maxSize(二者常需“或”关系) - 示例逻辑:
protected boolean removeEldestEntry(Map.Entry<string timedvalue> eldest) { return eldest.getValue().isExpired(clock.millis()); }</string>
用 LinkedHashMap 实现 LRU + TTL 混合策略的坑
LRU 是靠访问顺序维护的,TTL 是靠时间戳判断的,两者目标冲突:一个想留最近用的,一个想删早就该死的。如果只依赖 removeEldestEntry,过期项可能卡在链表中间,永远等不到被检查。
- 它只检查最老 entry,不会遍历全表,所以过期但非最老的项会被漏掉
- 高并发下,
put触发清理是单线程的,但多个线程同时put可能导致清理滞后 - 如果业务要求强时效性(比如 token 过期必须立刻不可用),不能只靠这个机制,得配合读时校验(
get里先 check expired) - 别把
maxSize设太大,否则过期项堆积多,removeEldestEntry被调用频率低,延迟更高
替代方案比死磕 removeEldestEntry 更靠谱的场景
当你要的不只是“简单过期”,而是精确控制、支持刷新、带统计或分布式,LinkedHashMap 就力不从心了。这时候硬改 removeEldestEntry 只会让代码越来越难维护。
- 本地缓存选
Caffeine:原生支持expireAfterWrite、expireAfterAccess,还能异步清理 - 需要主动剔除(比如登出清 token):用
ConcurrentHashMap+ 定时扫描,或监听事件回调 - 微服务间共享过期状态:上 Redis,用
EXPIRE或Redisson的RLocalCachedMap - 如果只是临时过渡,又不想引入新依赖,至少把
removeEldestEntry的逻辑拆成独立方法,方便单元测试










