Guava Cache 实战:构建高并发场景下的字典数据缓存

一、场景背景

在系统开发中,字典数据(如状态类型、分类数据)具有以下特点:

  • 高频读取(每个请求都可能涉及)
  • 低频变化(管理员修改后才会变更)
  • 数据一致性要求适中(允许分钟级延迟)
  • 传统方案每次查询数据库的方式会造成性能瓶颈,本文展示如何基于 Guava Cache 构建缓存层。

二、技术选型分析

为什么选择 Guava Cache?

  • 轻量级:无需引入 Redis 等中间件
  • 自动加载:提供 LoadingCache 自动回源能力
  • 灵活策略:支持基于时间/权重的淘汰策略
  • 线程安全:内置并发控制机制

三、实战代码解析

1.相关代码文件

KeyValue

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyValue<K, V> implements Serializable {

    private K key;
    private V value;

}

CacheUtils

java 复制代码
public class CacheUtils {
    // 异步刷新缓存(适合全局数据)
    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
        return CacheBuilder.newBuilder()
                .refreshAfterWrite(duration) // 写后刷新时间
                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool()));
    }
    
    // 同步刷新缓存(适合用户关联数据)
    public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) {
        return CacheBuilder.newBuilder()
                .refreshAfterWrite(duration)
                .build(loader);
    }
}

DictFrameworkUtils

java 复制代码
public class DictFrameworkUtils {

    private static DictDataApi dictDataApi;

    private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();

    /**
     * 针对 {@link #getDictDataLabel(String, String)} 的缓存
     */
    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {

                @Override
                public DictDataRespDTO load(KeyValue<String, String> key) {
                    return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
                }

            });

    /**
     * 针对 {@link #getDictDataLabelList(String)} 的缓存
     */
    private static final LoadingCache<String, List<String>> GET_DICT_DATA_LIST_CACHE = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<String, List<String>>() {

                @Override
                public List<String> load(String dictType) {
                    return dictDataApi.getDictDataLabelList(dictType);
                }

            });

    /**
     * 针对 {@link #parseDictDataValue(String, String)} 的缓存
     */
    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {

                @Override
                public DictDataRespDTO load(KeyValue<String, String> key) {
                    return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
                }

            });

    public static void init(DictDataApi dictDataApi) {
        DictFrameworkUtils.dictDataApi = dictDataApi;
        log.info("[init][初始化 DictFrameworkUtils 成功]");
    }


    @SneakyThrows
    public static String getDictDataLabel(String dictType, String value) {
        return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
    }

    @SneakyThrows
    public static List<String> getDictDataLabelList(String dictType) {
        return GET_DICT_DATA_LIST_CACHE.get(dictType);
    }

    @SneakyThrows
    public static String parseDictDataValue(String dictType, String label) {
        return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
    }

}

2. 核心工具类封装(CacheUtils)

java 复制代码
public class CacheUtils {
    // 异步刷新缓存(适合全局数据)
    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
        return CacheBuilder.newBuilder()
                .refreshAfterWrite(duration) // 写后刷新时间
                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool()));
    }
}

关键配置说明:

  • refreshAfterWrite:写入后指定时间触发异步刷新
  • asyncReloading:使用独立线程池执行刷新任务
  • Executors.newCachedThreadPool:弹性线程池应对突发流量

3. 字典缓存实现(DictFrameworkUtils)

3.1 缓存初始化

java 复制代码
   private static DictDataApi dictDataApi;

    private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO();

  	// 使用复合 Key 缓存字典项(类型+值 → 标签)
    private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
            Duration.ofMinutes(1L), // 过期时间 1 分钟
            new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {

                @Override
                public DictDataRespDTO load(KeyValue<String, String> key) {
                    return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL);
                }

            });

3.2 缓存使用示例

java 复制代码
@SneakyThrows // 通过 Lombok 隐藏异常
public static String getDictDataLabel(String dictType, String value) {
    return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
}

四、最佳实践总结

1. 缓存策略设计

策略 说明 本案例实现
缓存穿透 非法 Key 导致频繁回源 返回 DICT_DATA_NULL 空对象
缓存雪崩 大量缓存同时失效 随机过期时间(可扩展)
缓存击穿 热点 Key 失效导致并发回源 使用 LoadingCache 原子加载

2. 性能优化点

  • 异步刷新:通过 asyncReloading 实现后台线程刷新,避免阻塞请求线程
  • 分层缓存:同时缓存 字典项 → 标签 和 标签 → 字典项 两种关系
  • 弹性线程池:使用 CachedThreadPool 应对突发刷新请求
相关推荐
大只鹅1 小时前
两级缓存 Caffeine + Redis 架构:原理、实现与实践
redis·缓存·架构
zzywxc7872 小时前
如何高效清理C盘、释放存储空间,让电脑不再卡顿。
经验分享·缓存·性能优化·电脑
UQI-LIUWJ4 小时前
计算机组成笔记:缓存替换算法
笔记·缓存
harmful_sheep5 小时前
Spring 为何需要三级缓存解决循环依赖,而不是二级缓存
java·spring·缓存
软件2057 小时前
【redis使用场景——缓存——数据淘汰策略】
数据库·redis·缓存
加勒比海涛7 小时前
Spring Cloud Gateway 实战:从网关搭建到过滤器与跨域解决方案
数据库·redis·缓存
香饽饽~、8 小时前
【第十一篇】SpringBoot缓存技术
java·开发语言·spring boot·后端·缓存·intellij-idea
MonkeyKing_sunyuhua13 小时前
Guava Cache 本地项目缓存
缓存·guava
笨手笨脚の10 天前
Redis 源码分析-Redis 中的事件驱动
数据库·redis·缓存·select·nio·epoll·io模型
(:满天星:)10 天前
Redis哨兵模式深度解析与实战部署
linux·服务器·网络·数据库·redis·缓存·centos