学习日报 20250921|LoadingCache

LoadingCache 是 Google Guava 库中的一个接口,它提供了一种方便的方式来管理缓存,具备自动加载缓存值的功能。以下从 5W2H 的角度来详细介绍它:

1. What(是什么)

LoadingCache 是一个接口,属于 Google Guava 库中的缓存工具类。它扩展自Cache接口, 允许在请求缓存值不存在时,自动通过指定的加载函数来加载并缓存这个值。比如在电商系统中,频繁获取商品详情信息,就可以使用 LoadingCache,当缓存中没有该商品详情时,自动从数据库加载并缓存,下次请求时直接从缓存获取,提升查询效率。

2. Why(为什么要用)

  • 提升性能:减少对数据库、远程服务等后端数据源的频繁访问,对于热点数据,直接从内存缓存中获取数据,响应速度快,极大地提升了系统整体性能 。例如在新闻资讯平台中,热门新闻的详情数据,通过 LoadingCache 缓存后,用户请求时可以快速返回,降低响应时间。
  • 降低资源消耗:减少对数据库连接、网络请求等资源的占用,降低后端系统的负载。比如在高并发的秒杀系统中,大量用户请求商品信息,利用 LoadingCache 缓存商品库存、价格等信息,能有效减少数据库的压力 。

3. Who(谁用)

  • Java 开发者:在 Java 项目开发过程中,尤其是在需要缓存机制来优化性能的场景下,Java 开发者可以使用 LoadingCache 。
  • 应用于各类系统:如 Web 应用程序、后端服务系统、数据处理系统等,只要存在对数据进行缓存加速需求的场景,开发人员都可以考虑使用 LoadingCache。例如,一个企业内部的员工信息管理系统,频繁查询员工基础信息时,使用 LoadingCache 来缓存员工信息数据。

4. When(什么时候用)

  • 数据相对静态:数据更新频率较低,但是访问频率很高的场景。比如在一个在线教育平台中,课程分类信息、教师基本介绍信息等,这类数据不会频繁变动,却会被大量查询,适合用 LoadingCache 进行缓存。
  • 存在高并发读操作:在高并发的读多写少场景中,LoadingCache 可以有效应对高并发请求,避免大量请求同时打到后端数据源上。像电商平台的商品类目数据,在大促期间,大量用户浏览商品,类目数据几乎不会变化,通过 LoadingCache 缓存,能很好地应对高并发的读请求 。

5. Where(在哪里用)

  • 互联网应用:在各类互联网产品的后端服务中,像电商平台、社交平台、内容平台等,用于缓存商品信息、用户信息、帖子内容等数据 。
  • 企业级应用:在企业内部的 ERP 系统、OA 系统等,缓存员工信息、部门信息、审批流程配置等数据 。

6. How(怎么用)

  • 引入依赖 :如果是 Maven 项目,在pom.xml文件中添加 Guava 库的依赖:

xml

XML 复制代码
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version> <!-- 根据需要选择合适版本 -->
</dependency>
  • 创建 LoadingCache :通过CacheBuilder来构建LoadingCache实例,示例代码如下:

java

运行

java 复制代码
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.ExecutionException;

public class LoadingCacheExample {
    public static void main(String[] args) {
        LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
                .maximumSize(100) // 设置缓存最大容量
                .build(key -> {
                    // 这里是缓存加载函数,当缓存中不存在key对应的值时,会调用此函数加载值
                    return key.length(); 
                });
        try {
            // 获取缓存值,如果缓存中不存在,会自动调用加载函数
            Integer value = cache.get("example"); 
            System.out.println(value);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
java 复制代码
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 优惠券模板缓存服务:管理优惠券模板的内存缓存,减少数据库访问
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CouponTemplateCacheService {

    // 依赖注入:优惠券模板DAO(实际项目中可能是Mapper或Repository)
    private final CouponTemplateDao couponTemplateDao;

    // 核心缓存对象:Key为模板ID,Value为模板对象(用Optional避免空指针)
    private final LoadingCache<Long, Optional<CouponTemplate>> couponTemplateCache = 
        CacheBuilder.newBuilder()
            // 1. 基础容量配置
            .initialCapacity(500)  // 初始容量:根据业务预估(如日常活跃模板约300个)
            .maximumSize(2000)    // 最大容量:防止缓存过大导致OOM(预留扩容空间)
            
            // 2. 并发控制
            .concurrencyLevel(Runtime.getRuntime().availableProcessors())  // 并发级别=CPU核心数,保证线程安全
            
            // 3. 过期策略:根据业务数据更新频率设置
            .expireAfterWrite(30, TimeUnit.MINUTES)  // 写入后30分钟过期(模板更新后30分钟内缓存失效)
            .expireAfterAccess(10, TimeUnit.MINUTES) // 10分钟未访问则过期(清理冷数据)
            
            // 4. 缓存移除通知(可选,用于监控或统计)
            .removalListener(notification -> {
                Long templateId = notification.getKey();
                String reason = notification.getCause().name(); // 移除原因:EXPIRED(过期)、SIZE(容量满)等
                log.info("优惠券模板缓存移除:templateId={}, 原因={}", templateId, reason);
            })
            
            // 5. 加载逻辑:缓存不存在时如何获取数据
            .build(new CacheLoader<Long, Optional<CouponTemplate>>() {
                /**
                 * 当缓存中没有templateId对应的数据时,自动调用此方法加载数据
                 * @param templateId 优惠券模板ID
                 * @return 数据库查询结果(用Optional包装,允许null)
                 * @throws Exception 加载过程中的异常(会被包装为ExecutionException)
                 */
                @Override
                public Optional<CouponTemplate> load(Long templateId) throws Exception {
                    log.info("缓存未命中,从数据库加载优惠券模板:templateId={}", templateId);
                    
                    // 实际业务逻辑:从数据库查询模板
                    CouponTemplate template = couponTemplateDao.selectById(templateId);
                    
                    // 若数据库中不存在,返回Optional.empty(),避免缓存null值(Guava默认不允许缓存null)
                    return Optional.ofNullable(template);
                }
            });

    /**
     * 从缓存获取单个优惠券模板
     * @param templateId 模板ID
     * @return 模板对象(可能为null,需判断)
     */
    public CouponTemplate getTemplateById(Long templateId) {
        try {
            // 调用get()方法:若缓存存在则直接返回,不存在则触发load()方法加载
            Optional<CouponTemplate> templateOpt = couponTemplateCache.get(templateId);
            return templateOpt.orElse(null); // 若Optional为空,返回null
        } catch (ExecutionException e) {
            // 处理加载过程中的异常(如数据库连接失败)
            log.error("获取优惠券模板缓存异常:templateId={}", templateId, e);
            return null; // 异常时返回null,由上层处理
        }
    }

    /**
     * 批量获取模板(减少多次缓存查询的开销)
     * @param templateIds 模板ID列表
     * @return 键为ID、值为模板的Map(不存在的ID对应值为null)
     */
    public Map<Long, CouponTemplate> getTemplatesByIds(List<Long> templateIds) {
        try {
            // 批量查询缓存:返回Map<ID, Optional<模板>>
            Map<Long, Optional<CouponTemplate>> resultMap = couponTemplateCache.getAll(templateIds);
            
            // 转换为Map<ID, 模板>,并处理空值
            return resultMap.entrySet().stream()
                    .collect(Collectors.toMap(
                            Map.Entry::getKey,
                            entry -> entry.getValue().orElse(null)
                    ));
        } catch (ExecutionException e) {
            log.error("批量获取优惠券模板缓存异常:ids={}", templateIds, e);
            return Map.of(); // 异常时返回空Map
        }
    }

    /**
     * 主动刷新缓存(当模板更新时调用,避免缓存与数据库不一致)
     * @param template 最新的模板对象
     */
    public void refreshCache(CouponTemplate template) {
        if (template == null || template.getId() == null) {
            log.warn("刷新缓存失败:模板对象或ID为空");
            return;
        }
        // 主动将新数据放入缓存,覆盖旧值(触发写入时间更新,延长过期时间)
        couponTemplateCache.put(template.getId(), Optional.of(template));
        log.info("主动刷新优惠券模板缓存:templateId={}", template.getId());
    }

    /**
     * 主动移除缓存(当模板删除时调用)
     * @param templateId 模板ID
     */
    public void removeCache(Long templateId) {
        if (templateId == null) {
            log.warn("移除缓存失败:模板ID为空");
            return;
        }
        couponTemplateCache.invalidate(templateId); // 立即从缓存中移除该条目
        log.info("主动移除优惠券模板缓存:templateId={}", templateId);
    }
}

7. How Much(有多大效果)

  • 性能提升方面:对于热点数据的查询,响应时间可以降低 80% 甚至更多。比如在一个查询商品库存的接口中,使用 LoadingCache 前响应时间平均为 200ms,使用后,对于缓存命中的请求,响应时间可以降低到 20ms 以内。
  • 资源消耗方面:能显著减少后端数据源的负载,以数据库为例,查询请求量可能会降低 50% - 80% ,减少数据库连接的占用,提升数据库的稳定性和可用性 。
相关推荐
10km1 年前
guava:支持数组(Object[])为Key的缓存实现
java·缓存·cache·数组·guava·object[]·loadingcache