当前位置: 首页 > news >正文

实战案例:用 Guava ImmutableList 优化缓存查询系统,解决多线程数据篡改与内存浪费问题

在高并发系统中,“缓存查询结果”是提升性能的核心手段,但缓存中的集合数据往往面临两大痛点:多线程环境下被意外修改导致数据不一致,以及频繁创建临时列表造成的内存浪费。本文以“电商商品分类缓存系统”为背景,详细演示如何用 Guava ImmutableList 解决这些问题,通过“不可变列表 + 缓存”的组合,让系统在高并发下更稳定,内存占用降低 40%,且彻底杜绝数据篡改风险。

Java筑基(基础)面试专题系列(一):Tomcat+Mysql+设计模式

场景说明:商品分类缓存的核心诉求

业务场景:电商平台首页需要展示商品分类树(如“数码>手机>智能手机”“服饰>男装>T恤”),分类数据变更频率低(每天更新一次),但查询频率极高(每秒 thousands 次)。为减轻数据库压力,架构设计为:

  1. 应用启动时从数据库加载全部分类数据,缓存到本地内存;
  2. 前端查询分类时直接从本地缓存获取,无需访问数据库;
  3. 每天凌晨通过定时任务更新缓存(全量覆盖)。

数据模型

// 商品分类实体
@Data
public class Category {private Long id; // 分类IDprivate String name; // 分类名称private Long parentId; // 父分类ID(顶级分类为0)private Integer sort; // 排序权重(升序)
}

核心问题

  • 多线程并发查询时,缓存中的分类列表可能被某个线程意外修改(如添加无效分类、修改排序),导致前端展示错乱;
  • 每次查询都创建新的 ArrayList 存储分类子集(如“查询所有顶级分类”),重复创建列表导致内存浪费;
  • 缓存未命中时返回 null,调用方需频繁判空,代码冗余且易抛 NPE。

传统方案:用 ArrayList 缓存分类数据(问题频发)

传统实现中,开发者常使用 ArrayList 存储缓存数据,查询时直接返回列表引用,导致一系列问题。

传统方案代码实现

@Service
public class CategoryCacheService {@Autowiredprivate CategoryMapper categoryMapper;// 本地缓存:存储全部分类(key=parentId,value=子分类列表)private final Map<Long, List<Category>> categoryCache = new ConcurrentHashMap<>();// 应用启动时初始化缓存@PostConstructpublic void initCache() {// 从数据库查询全部分类List<Category> allCategories = categoryMapper.listAll();// 按 parentId 分组(顶级分类 parentId=0)Map<Long, List<Category>> groupByParent = allCategories.stream().collect(Collectors.groupingBy(Category::getParentId));// 存入缓存(用 ArrayList 存储)groupByParent.forEach((parentId, children) -> {// 排序后存入缓存List<Category> sortedChildren = children.stream().sorted(Comparator.comparingInt(Category::getSort)).collect(Collectors.toList()); // 默认返回 ArrayListcategoryCache.put(parentId, sortedChildren);});}// 查询指定父分类的子分类(对外提供的接口)public List<Category> getChildrenByParentId(Long parentId) {// 从缓存获取,未命中返回 nullList<Category> children = categoryCache.get(parentId);// 问题1:直接返回 ArrayList 引用,调用方可修改return children; }// 定时任务:每天凌晨更新缓存@Scheduled(cron = "0 0 0 * * ?")public void refreshCache() {// 重新查询并覆盖缓存(逻辑同 initCache)initCache();}
}

传统方案的三大核心问题

  1. 数据篡改风险getChildrenByParentId 直接返回 ArrayList 引用,调用方若恶意或误操作调用 add/remove,会直接修改缓存中的数据。例如:

    // 恶意代码:修改缓存中的顶级分类
    List<Category> topCategories = categoryCacheService.getChildrenByParentId(0L);
    topCategories.add(new Category(-1L, "钓鱼链接", 0L, 0)); // 缓存被污染
    

    后果:所有用户访问首页时都会看到“钓鱼链接”,直到缓存刷新。

  2. 多线程安全问题ConcurrentHashMap 虽保证“键的线程安全”,但 ArrayList 本身是线程不安全的。若某线程正在查询分类(遍历列表),同时定时任务触发缓存更新(覆盖列表),可能导致 ConcurrentModificationException,中断查询流程。

  3. 内存浪费严重:每次调用 getChildrenByParentId 时,若业务需要对分类列表做过滤(如“只展示启用状态的分类”),开发者常创建新的 ArrayList 存储结果:

    List<Category> children = categoryCacheService.getChildrenByParentId(0L);
    List<Category> enabledChildren = new ArrayList<>();
    for (Category c : children) {if (c.getStatus() == 1) { // 假设新增 status 字段enabledChildren.add(c);}
    }
    

    高并发下,每秒可能创建数百个临时 ArrayList,每个列表默认初始化容量为 10,而实际有效元素可能仅 3-5 个,内存浪费率达 50%-70%。

优化方案:用 ImmutableList 重构缓存系统(安全高效)

基于 ImmutableList 的“不可修改性”“线程安全”“内存高效”特性,对传统方案进行三点核心优化:

  1. 缓存中存储 ImmutableList,杜绝修改风险;
  2. 查询接口返回不可变列表,避免调用方篡改;
  3. 预生成常用子集的不可变列表,减少临时对象创建。

步骤 1:缓存存储不可变列表,初始化时完成转换

将缓存中的 List<Category> 替换为 ImmutableList<Category>,确保缓存数据从根源上不可修改。

@Service
public class OptimizedCategoryCacheService {@Autowiredprivate CategoryMapper categoryMapper;// 优化1:缓存值改为 ImmutableList,确保不可修改private final Map<Long, ImmutableList<Category>> categoryCache = new ConcurrentHashMap<>();// 应用启动时初始化缓存(存储不可变列表)@PostConstructpublic void initCache() {List<Category> allCategories = categoryMapper.listAll();Map<Long, List<Category>> groupByParent = allCategories.stream().collect(Collectors.groupingBy(Category::getParentId));groupByParent.forEach((parentId, children) -> {// 排序后转为不可变列表ImmutableList<Category> sortedChildren = children.stream().sorted(Comparator.comparingInt(Category::getSort)).collect(ImmutableList.toImmutableList()); // 关键:转为不可变列表categoryCache.put(parentId, sortedChildren);});}
}

核心改进:用 ImmutableList.toImmutableList() 替代 Collectors.toList(),直接将排序后的分类列表转为不可变列表。此时缓存中的数据无法被修改,即使通过反射也难以篡改(Guava 做了特殊处理)。

步骤 2:查询接口返回不可变列表,防篡改传递

查询接口返回 ImmutableList,确保调用方无法修改数据,同时用 emptyList() 替代 null,避免 NPE。

public class OptimizedCategoryCacheService {// 优化2:返回 ImmutableList,且未命中时返回空列表public ImmutableList<Category> getChildrenByParentId(Long parentId) {// 未命中时返回空列表(非 null),避免调用方判空return categoryCache.getOrDefault(parentId, ImmutableList.of());}
}

调用方测试

// 尝试修改返回的不可变列表
ImmutableList<Category> topCategories = optimizedCacheService.getChildrenByParentId(0L);
try {topCategories.add(new Category(-1L, "钓鱼链接", 0L, 0));
} catch (UnsupportedOperationException e) {// 预期:抛异常,缓存数据安全log.warn("尝试修改不可变列表,已拦截");
}

空安全测试

// 查询不存在的父分类(parentId=999)
ImmutableList<Category> invalidChildren = optimizedCacheService.getChildrenByParentId(999L);
System.out.println(invalidChildren.size()); // 输出 0(无 NPE)

步骤 3:预生成常用子集的不可变列表,减少临时对象

针对高频查询的子集(如“仅启用的分类”“排序前3的分类”),在缓存初始化时预生成不可变列表,避免每次查询都创建临时集合。

public class OptimizedCategoryCacheService {// 新增:缓存“仅启用的子分类”(key=parentId)private final Map<Long, ImmutableList<Category>> enabledCategoryCache = new ConcurrentHashMap<>();@Override@PostConstructpublic void initCache() {List<Category> allCategories = categoryMapper.listAll();Map<Long, List<Category>> groupByParent = allCategories.stream().collect(Collectors.groupingBy(Category::getParentId));groupByParent.forEach((parentId, children) -> {// 1. 全量排序后的不可变列表(基础缓存)ImmutableList<Category> sortedChildren = children.stream().sorted(Comparator.comparingInt(Category::getSort)).collect(ImmutableList.toImmutableList());categoryCache.put(parentId, sortedChildren);// 2. 预生成“仅启用”的不可变列表(高频查询子集)ImmutableList<Category> enabledChildren = children.stream().filter(c -> c.getStatus() == 1) // 过滤启用状态.sorted(Comparator.comparingInt(Category::getSort)).collect(ImmutableList.toImmutableList());enabledCategoryCache.put(parentId, enabledChildren);// 3. 预生成“排序前3”的不可变列表(另一高频场景)ImmutableList<Category> top3Children = children.stream().sorted(Comparator.comparingInt(Category::getSort)).limit(3).collect(ImmutableList.toImmutableList());top3CategoryCache.put(parentId, top3Children);});}// 提供查询“仅启用分类”的接口public ImmutableList<Category> getEnabledChildrenByParentId(Long parentId) {return enabledCategoryCache.getOrDefault(parentId, ImmutableList.of());}
}

核心价值:将原本“每次查询都创建临时 ArrayList”的逻辑,改为“初始化时预生成不可变列表”,高并发下可减少 90% 的临时对象创建,大幅降低 GC 压力。

步骤 4:多线程安全测试与缓存更新优化

为验证 ImmutableList 的线程安全特性,模拟 100 个线程同时查询缓存,同时触发缓存更新,观察是否出现异常。

// 多线程测试代码
@Test
public void testMultiThreadSafety() throws InterruptedException {OptimizedCategoryCacheService cacheService = new OptimizedCategoryCacheService();cacheService.initCache(); // 初始化缓存// 100 个线程同时查询Runnable queryTask = () -> {for (int i = 0; i < 1000; i++) {cacheService.getChildrenByParentId(0L); // 查询顶级分类}};// 1 个线程触发缓存更新Runnable refreshTask = () -> cacheService.refreshCache();// 启动线程ExecutorService executor = Executors.newFixedThreadPool(101);for (int i = 0; i < 100; i++) {executor.submit(queryTask);}executor.submit(refreshTask);executor.shutdown();executor.awaitTermination(1, TimeUnit.MINUTES);// 结果:无 ConcurrentModificationException,所有查询正常完成
}

缓存更新逻辑优化:定时任务更新缓存时,先创建新的不可变列表,再原子性覆盖旧值(ConcurrentHashMapput 方法是原子的),避免更新过程中读取到不完整数据。

性能对比:优化前后关键指标差异

在“每秒 1000 次查询,每次返回 5-10 个分类”的压测场景下,传统方案与优化方案的核心指标对比:

指标传统方案(ArrayList)优化方案(ImmutableList)提升幅度
数据篡改风险高(可被 add/remove 修改)无(不可修改,抛异常拦截)彻底解决
多线程稳定性低(10% 概率出现 ConcurrentModificationException高(0 异常)100% 提升
内存占用(日均)800MB(大量临时 ArrayList)480MB(预生成不可变列表)降低 40%
平均响应时间12ms(GC 频繁)6ms(GC 减少)提升 50%

避坑指南:ImmutableList 实战中的 4 个关键细节

  1. 注意“元素的不可变性”ImmutableList 仅保证“列表结构不可变”,若元素是可变对象(如 CategorysetName 方法),元素内部状态仍可被修改。解决方案:

    // 将 Category 改为不可变对象(移除 set 方法,字段用 final 修饰)
    @Data
    public class ImmutableCategory {private final Long id;private final String name;private final Long parentId;private final Integer sort;// 无 set 方法,仅通过构造器初始化public ImmutableCategory(Long id, String name, Long parentId, Integer sort) {this.id = id;this.name = name;this.parentId = parentId;this.sort = sort;}
    }
    
  2. 避免频繁创建不可变列表副本ImmutableListcopyOf 方法会创建新副本,若对同一列表频繁调用,会浪费内存。例如:

    // 错误:重复创建副本
    List<Category> list = categoryCache.get(0L);
    for (int i = 0; i < 1000; i++) {ImmutableList.copyOf(list); // 每次都创建新对象
    }
    

    正确做法:缓存 ImmutableList 实例,复用同一对象。

  3. 合理选择创建方式

    • 已知固定元素:用 ImmutableList.of(a, b, c)(最简洁);
    • 转换现有列表:用 ImmutableList.copyOf(mutableList)(确保独立副本);
    • 动态添加元素:用 ImmutableList.builder().addAll(list).add(e).build()(链式操作)。
  4. 不适合频繁修改的场景ImmutableList 适合“创建后不修改”的场景,若需要频繁 add/remove(如动态筛选条件),应先使用 ArrayList 处理,最后一步转为 ImmutableList

    // 正确:先修改,最后转不可变
    List<Category> temp = new ArrayList<>();
    for (Category c : allCategories) {if (condition) { // 复杂动态条件temp.add(c);}
    }
    ImmutableList<Category> result = ImmutableList.copyOf(temp);
    

总结

在“缓存查询系统”这类高并发场景中,Guava ImmutableList 的核心价值体现在:

  • 数据安全:通过“不可修改性”杜绝缓存数据被意外篡改,避免线上数据错乱;
  • 线程安全:天然支持多线程并发访问,无需额外同步措施,降低代码复杂度;
  • 内存高效:预生成不可变列表减少临时对象,降低 GC 压力,提升系统稳定性;
  • 空安全:用 ImmutableList.of() 替代 null,减少 30% 判空冗余代码。

本次实战案例通过“缓存存储不可变列表 + 预生成高频子集 + 安全传递不可变引用”的组合,完美解决了传统方案的三大痛点。如果你也在开发缓存系统、配置管理或高频查询服务,不妨试试 ImmutableList——它或许不会让你的代码变“酷”,但能让你的系统更稳定、更高效。

http://www.dtcms.com/a/581268.html

相关文章:

  • AR短视频SDK,打造差异化竞争壁垒
  • 什么是AR人脸特效sdk?
  • Angular由一个bug说起之二十:Table lazy load:防止重复渲染
  • 从0到1做一个“字母拼词”Unity小游戏(含源码/GIF)- 字母拼词正确错误判断
  • 网站建设自查情况报告做淘宝联盟网站要多少钱?
  • 重新思考 weapp-tailwindcss 的未来
  • RuoYi .net-实现商城秒杀下单(redis,rabbitmq)
  • Langchain 和LangGraph 为何是AI智能体开发的核心技术
  • C++与C#布尔类型深度解析:从语言设计到跨平台互操作
  • 贵阳 网站建设设计企业门户网站
  • Rust 练习册 :Matching Brackets与栈数据结构
  • Java基础——常用算法3
  • 【JAVA 进阶】SpringAI人工智能框架深度解析:从理论到实战的企业级AI应用开发指南
  • 对话百胜软件产品经理CC:胜券POS如何用“一个APP”,撬动智慧零售的万千场景?
  • 用ps怎么做短视频网站建立网站的步骤 实湖南岚鸿
  • wordpress使用latex乱码长沙优化网站厂家
  • 【uniapp】解决小程序分包下的json文件编译后生成到主包的问题
  • MySQL-5-触发器和储存过程
  • HTTPS是什么端口?443端口的工作原理与网络安全重要性
  • 从零搭建一个 PHP 登录注册系统(含完整源码)
  • Android 端离线语音控制设备管理系统:完整技术方案与实践
  • 网站流量一般多少合适asp网站实例
  • 想学网站建设与设计的书籍基于网站开发小程序
  • 【双指针类型】---LeetCode和牛客刷题记录
  • h5单页预览PDF文件模糊问题解决
  • LeetCode 每日一题 2025/11/3-2025/11/9
  • php网站开发干嘛的营销推广内容
  • STM32外设学习--TIM定时器--编码器接口
  • qiankun + Vue实现微前端服务
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段-二阶段(15):階段の訓練