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 应对突发刷新请求
相关推荐
后端漫漫12 小时前
Redis 客户端工具体系
数据库·redis·缓存
追梦开发者14 小时前
Redis 避坑指南①:从安装到连接,这 9 个坑 90% 的人都踩过
redis·缓存·database
何中应21 小时前
Redis集群搭建
数据库·redis·缓存
我是唐青枫21 小时前
别只会用 MemoryCache!C#.NET CacheManager 详解:多级缓存、Region 与 Redis 实战
缓存·c#·.net
Lyyaoo.2 天前
Redisson
数据库·缓存
倒霉蛋小马2 天前
【Redis】什么是缓存击穿?
数据库·redis·缓存
gQ85v10Db2 天前
Redis分布式锁进阶第十八篇:本地缓存+分布式锁双锁架构 + 高并发削峰兜底 + 极致性能无损优化实战
redis·分布式·缓存
小江的记录本2 天前
【Kafka核心】Kafka高性能的四大核心支柱:零拷贝、批量发送、页缓存、压缩
java·数据库·分布式·后端·缓存·kafka·rabbitmq
Komore3152 天前
商户查询缓存
java·redis·缓存