RemovalListener 不会在 expireAfterWrite 过期后立即触发,仅在 get()、cleanUp() 等访问或主动清理时检查并移除过期项才回调;其设计为 lazy 清理,无后台线程,需定期调用 cleanUp() 保障分钟级通知,并注意异步处理、避免阻塞与递归调用。

RemovalListener 为什么在 expireAfterWrite 后不触发
Guava 的 RemovalListener 不会在过期时间一到就立刻回调,这是设计使然,不是 bug。它只在「缓存被实际访问或主动清理时」才检查并移除过期条目,同时触发监听器。比如你设了 expireAfterWrite(10, TimeUnit.MINUTES),但之后既没调用 get(),也没调用 invalidate(),那这个 key 就可能一直留在内存里,onRemoval() 永远不会执行。
根本原因在于 Guava 为避免后台线程竞争锁、兼容无权创建线程的运行环境(如某些容器/Serverless),**完全放弃了独立的过期扫描线程**。所有维护操作都 lazy 地绑定在用户线程的读写路径上。
- 只有
cache.get(key)或cache.getIfPresent(key)时,才会检测该 key 是否过期,并在过期时移除 + 触发onRemoval() -
invalidate(key)或invalidateAll()是主动移除,必然触发回调 -
refresh(key)不会触发onRemoval(),它只是异步重载,原值仍存在(除非 refresh 失败且配置了refreshAfterWrite的 fallback 行为)
如何让过期通知“看起来准时”
如果你依赖 RemovalListener 做资源释放(比如关闭数据库连接、清理临时文件),不能坐等用户访问触发,得主动“唤醒”检查逻辑。最常用也最轻量的做法是:在业务关键路径中,定期调用一次 cache.cleanUp()。
cleanUp() 是一个同步方法,它会立即执行一次轻量级的过期清理(扫描部分 segment,非全量),不阻塞主线程太久,且能确保已过期但尚未被访问的条目被移除并触发 onRemoval()。
- 建议在定时任务中每 30–60 秒调用一次
cache.cleanUp(),频率取决于你对“延迟容忍度”的要求 - 不要在每次
put()后都调用 —— 频繁 cleanUp 会增加 CPU 开销,且 Guava 内部已有写入时的轻量清理 - 注意:
cleanUp()不保证清理全部过期项,只做局部 sweep;但它足够让绝大多数场景下的RemovalListener在分钟级内被触发
RemovalListener 中必须处理的三个关键点
即使 onRemoval() 被触发,也不代表你可以直接写业务逻辑。Guava 明确要求监听器方法不能阻塞、不能抛出未捕获异常、不能依赖当前线程上下文 —— 因为它可能在任意用户线程中被调用(包括 get() 线程、cleanUp() 线程、甚至 GC 回收线程,取决于引用策略)。
- 用
notification.getCause()判断移除原因:EXPIRED、REPLACED、COLLECTED、INVALIDATED、REMOVAL,不同原因对应不同清理策略(比如只对EXPIRED关闭连接,对INVALIDATED可能只需 log) - 所有 I/O 或耗时操作(如 close DB connection、HTTP 请求)必须异步化,推荐用
CompletableFuture.runAsync(..., executor),别用new Thread() - 监听器内禁止调用
cache.get()或任何可能再次触发清理的方法,否则可能造成死锁或递归调用
LocalCache 二级缓存中 RemovalListener 的真实定位
很多人误以为 RemovalListener 是“缓存失效回调”,其实它更接近“缓存条目物理销毁通知”。它不承诺时效性,也不参与缓存语义控制(比如是否允许 stale read)。在二级缓存架构中,它只适合做**副作用清理**,而非业务流程控制点。
例如:一级缓存(Caffeine)负责低延迟、高命中;二级缓存(Guava + expireAfterAccess)负责兜底和长尾数据。此时 RemovalListener 的作用仅限于:当二级缓存某条 Connection 对象被真正丢弃时,调用 connection.close();而不是指望它来“通知上游刷新一级缓存”——这种协调必须由外部事件(如 MQ、DB binlog)驱动。
真正容易被忽略的是:如果你把 RemovalListener 当作“过期即通知”的基础设施来设计业务逻辑,系统会在低流量时段静默失联 —— 所有本该释放的资源都滞留内存,直到某次偶然访问或下一次 cleanUp() 运行。


















