
guava cache的过期淘汰并非实时自动进行,而是通过在写入操作期间或偶尔的读取操作中触发维护任务来清理过期条目。这种设计避免了创建专用线程的开销和锁竞争,同时确保了在受限环境中的可用性,从而优化了性能和资源利用。
Guava Cache过期淘汰的核心原理
许多开发者在使用Guava Cache时,可能会误以为一旦设置了过期时间(TTL),缓存中的条目就会在精确的到期时刻被立即移除。然而,Guava Cache的过期淘汰机制并非如此。它采用了一种“按需清理”的策略,而不是依赖一个独立的后台线程进行持续的维护。
何时触发清理?
Guava Cache的清理操作主要在以下两种情况下触发:
- 写入操作期间: 当有新的键值对被写入缓存(如put()操作),或者现有条目被更新、失效(如invalidate()操作)时,Guava Cache会顺带执行少量的维护工作,检查并移除部分过期条目。
- 读取操作期间(偶发性): 如果写入操作不频繁,缓存会在偶尔的读取操作(如get()操作)中执行轻量级的维护,以清理过期数据。
这种设计意味着,一个条目即使已经过期,也可能不会立即从缓存中消失。它会一直存在,直到下一次维护操作被触发时才会被移除。当尝试获取一个已过期但尚未被清理的条目时,Guava Cache会识别其过期状态并返回null(或重新加载,如果配置了CacheLoader),同时将其标记为待清理。
为何采用这种机制?
Guava Cache选择这种“懒惰”的清理方式,是基于性能和资源利用的考量:
- 避免创建专用线程: 如果Guava Cache为了持续清理而创建一个后台线程,这将增加系统的资源开销。
- 减少锁竞争: 持续的后台清理线程需要与用户操作竞争共享锁,可能导致性能下降和并发问题。
- 环境兼容性: 某些运行环境(如特定的应用服务器或受限的容器)可能限制或不允许创建额外的线程,Guava Cache的这种设计确保了其在这些环境中的可用性。
配置Guava Cache的过期策略
Guava Cache提供了两种主要的过期策略,可以通过CacheBuilder进行配置:
-
基于写入时间的过期(expireAfterWrite): 从条目被写入缓存(或最后一次更新)的时间点开始计算,经过指定时间后过期。
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.TimeUnit; public class GuavaCacheExample { public static void main(String[] args) throws InterruptedException { Cachecache = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) // 写入后10秒过期 .maximumSize(100) // 最大缓存容量 .build(); cache.put("key1", "value1"); System.out.println("放入 key1: " + cache.getIfPresent("key1")); // value1 Thread.sleep(5000); // 等待5秒 System.out.println("5秒后获取 key1: " + cache.getIfPresent("key1")); // value1 Thread.sleep(6000); // 再等待6秒,总计11秒 // 此时key1已过期,但可能尚未被清理 System.out.println("11秒后获取 key1: " + cache.getIfPresent("key1")); // null (已过期,即使未被物理移除) cache.put("key2", "value2"); // 写入操作,可能触发清理 System.out.println("写入key2后再次尝试获取 key1: " + cache.getIfPresent("key1")); // null } } -
基于访问时间的过期(expireAfterAccess): 从条目被读取或写入缓存的时间点开始计算,经过指定时间后过期。每次访问都会重置计时器。
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.TimeUnit; public class GuavaCacheAccessExpiryExample { public static void main(String[] args) throws InterruptedException { Cachecache = CacheBuilder.newBuilder() .expireAfterAccess(5, TimeUnit.SECONDS) // 5秒内未访问则过期 .build(); cache.put("data", "important_data"); System.out.println("Initial data: " + cache.getIfPresent("data")); // important_data Thread.sleep(3000); // 等待3秒 System.out.println("Accessed data after 3s: " + cache.getIfPresent("data")); // important_data (访问重置计时器) Thread.sleep(3000); // 再等待3秒 (总计6秒,但上次访问在3秒前) System.out.println("Accessed data after another 3s: " + cache.getIfPresent("data")); // important_data (上次访问后3秒,未过期) Thread.sleep(6000); // 再等待6秒 (上次访问后9秒,已过期) System.out.println("Accessed data after another 6s: " + cache.getIfPresent("data")); // null } }
注意事项
- 非精确的实时过期: 理解Guava Cache的过期机制不是实时的。一个条目即使已经过期,也可能在内存中停留一段时间,直到有新的写入或偶尔的读取操作触发清理。
- 清理的驱动力: 缓存的清理频率与写入和读取操作的频率正相关。在高并发写入或读取的场景下,过期条目会相对及时地被清理。而在低活动量的缓存中,过期条目可能会停留更长时间。
- 手动清理: 如果需要强制清理,可以调用cache.cleanUp()方法。这个方法会执行一次维护任务,清理所有已过期的条目。然而,通常情况下,无需手动调用,让Guava Cache自行管理即可。
- 内存占用: 即使过期条目尚未被物理移除,它们也不会被返回给调用者。但它们仍然会占用一定的内存,直到被清理。对于内存敏感的场景,应合理设置缓存容量和过期时间。
总结
Guava Cache的过期淘汰机制是一个经过精心设计的权衡,旨在提供高性能和高可用性,同时避免不必要的资源消耗。它通过在写入或读取操作中“搭便车”的方式进行清理,而不是依赖一个独立的后台线程。理解这一机制对于正确配置和使用Guava Cache至关重要,能够帮助开发者避免潜在的误解,并更好地优化应用程序的性能。










