Guava Cache案例

wen java案例 1

本文目录导读:

Guava Cache案例

  1. 文章标题:Guava Cache实战指南:从本地缓存到高并发架构的四个经典案例
  2. 案例一:单机应用中的“热点数据”加速
  3. 案例二:多级缓存架构中的“一级缓存”
  4. 案例三:高并发场景下的“防击穿”与“防雪崩”
  5. 案例四:基于RemovalListener的“数据同步”与“资源回收”
  6. 常见问题问答(FAQ)

Guava Cache实战指南:从本地缓存到高并发架构的四个经典案例


目录导读

  1. 单机应用中的“热点数据”加速——如何用Guava Cache替代ConcurrentHashMap,减少数据库压力。
  2. 多级缓存架构中的“一级缓存”——Guava Cache与Redis的协同策略,解决缓存穿透问题。
  3. 高并发场景下的“防击穿”与“防雪崩”——基于expireAfterWriterefreshAfterWrite的差异选择。
  4. 基于RemovalListener的“数据同步”与“资源回收”——监听器在订单状态变更中的实际用法。
  5. 常见问题问答(FAQ)——关于容量控制、序列化、线程安全的深度答疑。

在Java生态中,Guava Cache(Google Guava库的缓存组件)一直被低估,它不像Redis那样需要独立部署,也不像Caffeine那样在极致性能上崭露头角,但它凭借简单、可控、无外部依赖的特性,依然是本地缓存的“首选平替”,本文结合搜索引擎中的实操案例,去伪存真,提炼出四个可落地的业务场景,并附赠高频面试问答,帮助你彻底掌握Guava Cache的实战精髓。


单机应用中的“热点数据”加速

场景描述:一个订单服务,每次请求都需要查询数据库获取用户等级(变化频率低,但查询频繁),如果直接用HashMap,无法自动清理过期数据;如果用ConcurrentHashMap+定时任务,代码冗余。

实现方案

LoadingCache<String, UserLevel> cache = CacheBuilder.newBuilder()
    .maximumSize(10000)               // 最大容量,防止内存溢出
    .expireAfterWrite(30, TimeUnit.MINUTES)  // 写入后30分钟过期
    .build(new CacheLoader<String, UserLevel>() {
        @Override
        public UserLevel load(String userId) {
            return userDao.getUserLevel(userId); // 缓存未命中时,自动调用
        }
    });

为什么要用LoadingCache
它封装了“先查缓存-未命中-加载DB-放入缓存”的完整逻辑,且默认线程安全,对比直接使用ConcurrentHashMap,Guava Cache能自动处理并发情况下多个线程同时请求同一个未命中的key,避免重复查询数据库(内部通过LocalCache.Segment的锁机制实现)。


多级缓存架构中的“一级缓存”

场景描述:大型电商的首页推荐位,数据链路为:本地缓存(一级)→ Redis(二级)→ DB,Guava Cache作为最靠近应用的缓存,能减少对Redis的网络IO。

关键配置

  • expireAfterWrite(5, TimeUnit.MINUTES):本地缓存最多生效5分钟,防止数据长期不一致。
  • softValues():允许JVM在内存紧张时回收value引用,避免OOM。

代码逻辑

String value = guavaCache.getIfPresent(key);
if (value == null) {
    value = redisCache.get(key);
    if (value == null) {
        value = db.query(key);
        redisCache.set(key, value, 10, TimeUnit.MINUTES);
    }
    guavaCache.put(key, value);
}

注意坑点:Guava Cache的多级缓存一致性是个难题,当DB更新时,需主动失效一级和二级缓存,建议通过Cache.invalidate(key)在数据变更接口中显式删除本地缓存,避免因过期时间太长读到脏数据。


高并发场景下的“防击穿”与“防雪崩”

两种过期策略的本质区别: | 策略 | 行为 | 适用场景 | |------|------|----------| | expireAfterWrite | 写完后固定时间过期,过期后所有线程同时回源,可能导致DB压力激增(击穿)。 | 数据变化不频繁,允许偶尔的DB压力。 | | refreshAfterWrite | 过期后第一个线程回源刷新,其他线程仍返回旧值(除非旧值已不可用),实现平滑刷新。 | 数据可以短暂不一致,但DB压力必须可控。 |

防击穿示例(结合expireAfterWrite+CacheLoader):

// 由于CacheLoader会自动加载,高并发下只有第一个线程去DB,其余线程等待该线程的结果。
// 但若DB慢,会导致大量线程阻塞,解决方法:使用ListenableFuture异步刷新。

防雪崩推荐配置

CacheBuilder.newBuilder()
    .maximumSize(1000)
    .refreshAfterWrite(1, TimeUnit.MINUTES)  // 1分钟刷新一次
    .build(new CacheLoader<String, Data>() {
        @Override
        public Data load(String key) {
            return loadFromDB();  // 重载数据
        }
    });

配合ListeningExecutorService实现异步加载,避免阻塞请求线程。


基于RemovalListener的“数据同步”与“资源回收”

场景:用户登录状态缓存,当用户登出或状态变更时,需要通知其他系统(如清除当前用户在其他微服务中的会话)。RemovalListener可以在key被移除时触发回调。

代码实现

Cache<String, UserSession> cache = CacheBuilder.newBuilder()
    .maximumSize(50000)
    .removalListener(new RemovalListener<String, UserSession>() {
        @Override
        public void onRemoval(RemovalNotification<String, UserSession> notification) {
            if (notification.wasEvicted()) {  // 是否因容量满或过期被移除
                String userId = notification.getKey();
                sendLogoutEvent(userId); // 通知其他服务
            }
        }
    })
    .build();
// 手动失效时触发
cache.invalidate("user123");

真实业务中,该监听器常用于:

  1. 释放非托管资源(如关闭数据库连接,如果值对象持有连接)。
  2. 数据回写(当缓存被淘汰时,将未持久化的数据落盘)。
  3. 监控告警(统计缓存命中率或淘汰频率)。

注意RemovalListener默认在移除操作所在的线程同步执行,如果回调逻辑较重(如网络调用),建议改为RemovalListeners.asynchronous(..., executor)


常见问题问答(FAQ)

Q1:Guava Cache的maximumSize是基于什么淘汰策略?
A:基于LRU近似算法(Least Recently Used,最近最少使用),Guava内部维护一个频率和访问顺序的记录,但并非严格LRU,而是类似TinyLFU的一种变体,对于精确的LRU,可手动维护AccessOrder(但Guava不支持)。

Q2:Guava Cache可以被序列化用于分布式缓存吗?
A:不建议,Guava Cache是JVM本地缓存,设计初衷是单机内存,分布式中应使用Redis或Memcached,如果需要本地缓存+分布式缓存配合,务必保证本地缓存的生命周期短于分布式缓存,或引入消息总线(如Redis Pub/Sub)来主动失效本地项。

Q3:getIfPresentget(K, Callable)有何区别?

  • getIfPresent:只查缓存,未命中返回null,不触发加载。
  • get(K, Callable):未命中时执行Callable并缓存结果,而且这个操作是原子的(同一key并发只会执行一次Callable),适合需要动态指定加载器的场景。

Q4:如何监控Guava Cache的命中率?
A:调用cache.stats()获取CacheStats对象,包含hitCountmissCountloadSuccessCount等,启用方式:CacheBuilder.recordStats(),线上环境可定期打印,或通过Micrometer接入监控系统。

Q5:高并发下,expireAfterWrite会不会导致缓存穿透?
A:会,如果缓存过期瞬间有10万请求进来,因为CacheLoader的同步加载机制,只有第一个请求去DB加载,其余9万请求会阻塞等待该线程的结果,但如果DB响应慢,可能导致线程池积压,解决方式是使用Cache.asMap().put(key, value)提前填充或采用refreshAfterWrite+异步刷新。


Guava Cache虽然小巧,但在本地缓存领域依然是久经考验的“老兵”,通过上述四个案例,你可以精准应对大部分缓存痛点,在实现时,务必根据业务对一致性、延迟、内存占用的容忍度,选择合适的过期策略与监听器机制,如果需要在更大型的分布式场景中使用,建议升级为Caffeine+Redis的组合,但理解Guava的核心理念依然是一面很好的“透视镜”。

抱歉,评论功能暂时关闭!