guava里面如何实现缓存

为了提高遍历大量记录时的效率,通常需要利用本地缓存。Guava Cache提供了一种解决方案,允许在内存中存储预先计算或检索的结果,以避免重复昂贵的查找操作。它特别适合那些预期会被多次查询的键,并且确保缓存数据量不超过内存限制,不同于集中式缓存服务器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  我们经常会遇到一些本地缓存的场景,比如遍历N条记录,依据记录里面的某个ID,获取到相应的名称,如果遍布的时候逐条去调用外部依赖的服务查询名称,显然会比较慢,如果把获取到的记录保存到本地的缓存里面,先判断本地缓存里面是否有记录存在,如果能够找到记录则直接取本地的数据。显然这样处理的话效率会提升不少。

  相对于ConcurrentMap来讲guava cache适用于:

  • 你愿意消耗一些内存空间来提升速度
  • 你预料到某些键会被查询一次以上
  • 缓存中存放的数据总量不会超出内存容量。本地单机缓存不是集中式缓存服务器。
只要你的场景符合其中的一项,那guava cache就适合你。

加载
   “获取缓存--如果没有--则计算”【get - if - absent - compute】原子语义
在调用get时传入一个Callable实例。缓存元素也可以通过Cache.put方法直接插入,但自动加载是首选。因为它更容易推断所有缓存内容的一致性。

CacheLoader
  LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。
从LoadingCache查询的正规方式是使用get(K)方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。
由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionExceptino异常。
如下:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(
            new CacheLoader<Key, Graph>() {
                public Graph load(Key key) throws AnyException {     // 手工抛异常
                    return createExpensiveGraph(key);
                }
            });

...
try {
    // 也需要在get的时候抛出异常
    return graphs.get(key);
} catch (ExecutionException e) {
    throw new OtherException(e.getCause());
}

如果你定义的CacheLoader没有声明任何检查异常,则可以通过getUnchecked(K)查找缓存;但必须注意,一旦CacheLoader声明了检查型异常,就不可以调用getUnchecked(K).

如下:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .build(
            new CacheLoader<Key, Graph>() {
                public Graph load(Key key) { // no checked exception
                    return createExpensiveGraph(key);
                }
            });

...
return graphs.getUnchecked(key);

Callable

    所有类型的guava cache,不管有没有自动加载功能都支持get(K,Callable(V))方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。
在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法实现了这个模式:如果有缓存则返回;否则运算、缓存、然后返回。
    
public static void main(String[] args) {
        String key = "instance_id";

        Cache<String,Double> cache = CacheBuilder.newBuilder().maximumSize(1000).build();

        int i = 1;
        while (i < 5){
            i++;
            try {
                Double value = cache.get(key, new Callable<Double>() {
                    @Override
                    public Double call() throws Exception {
                        System.out.println("hello");  //第一次没有的时候会运算然后添加到缓存里面。后面就直接从缓存里面读取数据.
                        return 120d;
                    }
                });
                System.out.println(value);
            } catch (ExecutionException ex) {
                System.out.println(ex.getCause());
            }
        }
    }
输出:
hello
120.0
120.0
120.0
120.0

相对于像Cache.asMap().putIfAbsent(K,V)来讲,Cache.get(K,Callable<V>)应该最优先使用。

缓存回收机制
提供了三种回收方式:基于容量回收、定时回收、基于引用回收
基于容量回收
需要使用CacheBuilder.maximumSize(long). 

显式清除
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()

在JDK8里面的实现代码
static Map<String, String> cache2 = new ConcurrentHashMap<>();


    public static String getAppProduct(String appName){
        // call aone
        System.out.println("call aone");
        return "hello1";
    }

    public static void main(String[] args) {
        String key = "instance_id";

        String result = cache2.computeIfAbsent(key,(item) -> getAppProduct(key));
        System.out.println(result);

其实我觉得用jdk8的这种方式也是可以的.





<think>嗯,用户想了解Guava本地缓存在分布式微服务架构中的适用性和限制。这个问题很专业,需要结合分布式系统的特性来分析本地缓存的优缺点。 用户应该是个后端开发工程师,正在设计微服务架构的缓存方案。ta提到"分布式微服务架构"这个关键词,说明关注点在于多实例环境下的数据一致性和扩展性问题。从问题表述看,ta可能已经遇到本地缓存带来的数据不一致困扰,或者正在评估技术选型。 先看适用场景。根据引用[2],本地缓存最大优势是访问速度快,因为数据存在应用进程内存里。这对高频读取的小数据特别有用,比如用户基础信息、配置参数这类变更不频繁的数据。引用[1]提到Caffeine适合这种场景,其实Guava Cache也类似。在微服务里,每个实例缓存自己的副本,确实能减少数据库压力。 但限制更值得关注。引用[2]明确指出本地缓存无法跨实例共享,这会导致严重的数据一致性问题。比如商品库存更新时,不同实例的缓存可能显示不同库存数。引用[3]的对比表更清晰展示了这点:Redis是强一致的,而本地缓存只能保证弱一致。如果业务要求实时准确,比如金融交易,这就很危险。 另一个限制是内存压力。引用[3]提到本地缓存受限于堆内存,而Redis支持TB级数据。当缓存数据量很大时,Guava Cache可能引发GC问题。不过引用[1]提到可以用LRU策略控制条目数,算是折中方案。 用户没问但值得补充的是:微服务部署时,本地缓存会使金丝雀发布或蓝绿部署更复杂。因为新老版本实例的缓存结构可能不兼容,容易引发NPE。建议在缓存键中加入版本号来规避。 最后想到个实用技巧:可以结合本地缓存和Redis做二级缓存。先用Guava Cache扛住90%的读取请求,miss时再查Redis。这样既保留本地缓存的速度,又通过Redis保证基础一致性。不过要注意缓存穿透问题,需要设计好空值处理机制。</think>Guava本地缓存在分布式微服务架构中的适用性和限制需结合具体场景分析: ### 适用场景(优势) 1. **高频只读数据加速** 如用户基础信息、配置参数等变更频率低的数据,通过本地缓存可减少网络调用和数据库压力。访问延迟达**微秒级**(引用[3]),远优于远程缓存。 ```java // Guava缓存配置示例(引用[4]) Cache<String, User> userCache = CacheBuilder.newBuilder() .expireAfterWrite(30, TimeUnit.MINUTES) // 写后30分钟失效 .maximumSize(500) // 最多缓存500用户 .build(); ``` 2. **临时性状态存储** 如接口限流计数器、短时任务状态跟踪等无需跨节点共享的数据,利用本地内存可避免Redis依赖。 3. **无状态服务性能优化** 当服务实例无状态且负载均衡采用**粘性会话**时,相同用户请求会路由到同一实例,此时本地缓存可有效复用(引用[2])。 --- ### 关键限制(挑战) 1. **数据一致性风险** - 多实例间缓存独立更新,导致**数据不一致**(如实例A更新库存后,实例B缓存仍为旧值) - 适用**弱一致性**场景(如非实时展示的排行榜),不适用金融交易等强一致性需求(引用[3]) 2. **内存资源受限** - 单实例缓存容量受JVM堆内存限制(通常GB级),无法支撑TB级数据(引用[3]) - 需谨慎设置淘汰策略(如LRU、最大条目数)避免OOM(引用[1]) 3. **缓存穿透与雪崩** - 未命中缓存的请求直接穿透到数据库,分布式环境下可能引发连锁故障 - 需结合布隆过滤器或空值缓存缓解 4. **实例扩缩容问题** 新实例启动后本地缓存冷启动,可能引发数据库瞬时压力;缩容时缓存数据丢失。 --- ### 优化实践 1. **多级缓存架构** ```mermaid graph LR A[客户端] --> B[Guava本地缓存] -- 未命中 --> C[Redis集群] -- 未命中 --> D[数据库] B -- 定期失效 --> C[同步更新通知] ``` 优先读本地缓存,未命中时查询Redis,最后回源数据库(引用[2]) 2. **一致性增强方案** - 通过Pub/Sub广播失效事件(如Redis Channel) - 短过期时间+主动刷新:设置较短的本地缓存TTL(如1分钟),结合定时任务更新 3. **资源隔离策略** 使用`weigher`函数控制内存占用,避免大对象导致内存溢出: ```java CacheBuilder.newBuilder() .maximumWeight(1024 * 1024 * 100) // 100MB内存上限 .weigher((String key, byte[] value) -> value.length) .build(); ``` --- ### 决策建议 | 场景 | 推荐方案 | 原因 | |---------------------|-------------------|--------------------------| | 高频读+数据量小 | ✅ Guava独立使用 | 微秒级延迟,无网络开销 | | 数据变更频繁 | ⚠️ 结合Redis | 需跨节点一致性保障 | | 大数据量(>10GB) | ❌ 避免纯本地缓存 | 受限于单实例堆内存 | | 无状态服务+粘性会话 | ✅ Guava优先 | 请求路由固定,缓存利用率高 | > **结论**:Guava缓存适用于分布式系统中**对一致性要求不高、数据量小、访问频次高**的场景。需通过多级缓存、失效广播、容量限制等手段规避风险,关键业务建议搭配Redis实现最终一致性(引用[3])。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值