祉遛吾 发表于 2026-1-14 10:25:03

工作中最常用的5种本地缓存

前言

今天和大家聊聊一个几乎所有Java开发者都会用到,但很多人对其理解不够深入的技术——本地缓存。
有些小伙伴在工作中可能遇到过这样的场景:系统性能测试时,发现某个接口响应时间越来越长,一查发现是数据库查询太频繁。
加了Redis分布式缓存后有所改善,但网络IO的开销依然存在,特别是对时效性要求高的数据。
这时候,本地缓存的价值就凸显出来了。
本地缓存是什么?
简单说就是将数据存储在应用进程的内存中,实现数据的快速访问。
它的访问速度通常是纳秒级,而分布式缓存(如Redis)是毫秒级,数据库查询更是毫秒到秒级。
01 为什么需要本地缓存?

在深入具体方案前,我们先搞清楚本地缓存的核心价值。
来看一个没有缓存的场景:
// 没有缓存的用户查询服务@Servicepublic class CategoryServiceWithoutCache {      @Autowired    private CategoryMapper categoryMapper;      public Category getCategoryById(Long categoryId) {      // 每次调用都直接查询数据库      return categoryMapper.selectById(categoryId);    }      // 假设这个接口每秒被调用1000次    // 数据库就要承受1000QPS的压力}这种设计的问题很明显:

[*]数据库压力大:每次请求都要查询数据库
[*]响应时间长:数据库查询通常需要几毫秒到几十毫秒
[*]系统可扩展性差:数据库成为单点瓶颈
引入本地缓存后,情况会大大改善:
// 使用本地缓存的用户查询服务@Servicepublic class CategoryServiceWithCache {      @Autowired    private CategoryMapper categoryMapper;      // 使用ConcurrentHashMap作为本地缓存    private final Map categoryCache = new ConcurrentHashMap();      public Category getCategoryById(Long categoryId) {      // 1. 先查缓存      Category category = categoryCache.get(categoryId);      if (category != null) {            return category;// 缓存命中,直接返回      }                // 2. 缓存未命中,查询数据库      category = categoryMapper.selectById(categoryId);      if (category != null) {            // 3. 将数据放入缓存            categoryCache.put(categoryId, category);      }                return category;    }}这样改造后,对于同一个分类的多次查询,只有第一次会访问数据库,后续查询都直接返回缓存数据,性能提升几个数量级。
但是,简单的ConcurrentHashMap只是本地缓存的最基础形态。
在实际生产中,我们需要考虑更多问题:内存管理、过期策略、缓存淘汰、并发控制等。
这就是各种本地缓存框架存在的意义。
02 本地缓存核心要素

在介绍具体方案前,我们先了解一个优秀本地缓存应该具备的特性:
https://img2024.cnblogs.com/blog/2238006/202601/2238006-20260114095253873-970129861.png
理解了这些核心要素,我们就能更好地评估各种缓存方案。
现在,让我们深入分析5种最常用的本地缓存方案。
03 方案一:ConcurrentHashMap

原理与特点

ConcurrentHashMap是JDK自发的线程安全的哈希表,它通过分段锁技术实现高并发访问。
作为缓存使用时,它是最简单、最轻量级的选择。
https://img2024.cnblogs.com/blog/2238006/202601/2238006-20260114095305279-96940489.png
核心机制:

[*]分段锁:将整个Map分成多个Segment,每个Segment独立加锁
[*]并发度:默认16个Segment,可支持16个线程并发写
[*]Java 8优化:在Java 8中改为使用synchronized+CAS+红黑树,性能更好
实战代码示例

import java.util.concurrent.*;/** * 基于ConcurrentHashMap的简易本地缓存 * 特点:简单、轻量、无过期策略 */public class ConcurrentHashMapCache {      // 核心缓存存储    private final ConcurrentHashMap cache;      // 可选的缓存加载器    private final CacheLoader loader;      public ConcurrentHashMapCache() {      this.cache = new ConcurrentHashMap();      this.loader = null;    }      public ConcurrentHashMapCache(CacheLoader loader) {      this.cache = new ConcurrentHashMap();      this.loader = loader;    }      /**   * 获取缓存值   * 如果不存在且配置了loader,则加载数据   */    public V get(K key) {      V value = cache.get(key);                if (value == null && loader != null) {            // 双重检查锁,防止并发加载            synchronized (this) {                value = cache.get(key);                if (value == null) {                  value = loader.load(key);                  if (value != null) {                        cache.put(key, value);                  }                }            }      }                return value;    }      /**   * 放入缓存   */    public void put(K key, V value) {      cache.put(key, value);    }      /**   * 移除缓存   */    public V remove(K key) {      return cache.remove(key);    }      /**   * 清空缓存   */    public void clear() {      cache.clear();    }      /**   * 获取缓存大小   */    public int size() {      return cache.size();    }      /**   * 判断是否包含key   */    public boolean containsKey(K key) {      return cache.containsKey(key);    }      /**   * 缓存加载器接口   */    @FunctionalInterface    public interface CacheLoader {      V load(K key);    }}/** * 使用示例:用户信息缓存 */@Servicepublic class UserServiceWithConcurrentHashMap {      @Autowired    private UserMapper userMapper;      // 使用自定义的ConcurrentHashMap缓存    private final ConcurrentHashMapCache userCache;      public UserServiceWithConcurrentHashMap() {      // 初始化缓存,配置缓存加载器      userCache = new ConcurrentHashMapCache(userId -> {            // 当缓存不存在时,调用此方法加载数据            System.out.println("缓存未命中,从数据库加载用户: " + userId);            return userMapper.selectById(userId);      });    }      /**   * 获取用户信息(带缓存)   */    public User getUserById(Long userId) {      return userCache.get(userId);    }      /**   * 更新用户信息(同时更新缓存)   */    public void updateUser(User user) {      // 1. 更新数据库      userMapper.updateById(user);                // 2. 更新缓存      userCache.put(user.getId(), user);    }      /**   * 删除用户(同时删除缓存)   */    public void deleteUser(Long userId) {      // 1. 删除数据库记录      userMapper.deleteById(userId);                // 2. 删除缓存      userCache.remove(userId);    }      /**   * 批量获取用户(优化N+1查询)   */    public Map getUsersByIds(List userIds) {      Map result = new HashMap();      List missingIds = new ArrayList();                // 1. 先从缓存获取      for (Long userId : userIds) {            User user = userCache.get(userId);            if (user != null) {                result.put(userId, user);            } else {                missingIds.add(userId);            }      }                // 2. 批量查询缺失的数据      if (!missingIds.isEmpty()) {            List dbUsers = userMapper.selectBatchIds(missingIds);            for (User user : dbUsers) {                result.put(user.getId(), user);                userCache.put(user.getId(), user);// 放入缓存            }      }                return result;    }}优缺点分析

优点:

[*]JDK原生支持:无需引入第三方依赖
[*]线程安全:分段锁/CAS保证并发安全
[*]性能优秀:读操作完全无锁,写操作锁粒度小
[*]简单轻量:代码简单,资源消耗小
缺点:

[*]功能有限:缺乏过期、淘汰等高级功能
[*]内存无法限制:可能造成内存溢出
[*]需要手动管理:缓存策略需要自己实现
适用场景:

[*]缓存数据量小且固定
[*]不需要自动过期功能
[*]作为更复杂缓存的底层实现
[*]临时性、简单的缓存需求
有些小伙伴在项目初期喜欢用ConcurrentHashMap做缓存,因为它简单直接。
但随着业务复杂化,往往会遇到内存溢出、缓存清理等问题,这时候就需要更专业的缓存方案了。
04 方案二:LRU缓存

原理与特点

LRU(Least Recently Used,最近最少使用)是一种经典的缓存淘汰算法。
当缓存空间不足时,优先淘汰最久未被访问的数据。
LinkedHashMap是JDK提供的实现了LRU特性的Map实现。
它通过维护一个双向链表来记录访问顺序。
https://img2024.cnblogs.com/blog/2238006/202601/2238006-20260114095325773-1561315843.png
核心机制:

[*]访问顺序:accessOrder=true时,每次访问会将节点移到链表头部
[*]淘汰策略:链表尾部的节点是最久未访问的
[*]重写removeEldestEntry:控制何时删除最老节点
实战代码示例

import java.util.*;/** * 基于LinkedHashMap的LRU缓存 * 特点:自动淘汰最久未使用的数据 */public class LRUCache extends LinkedHashMap {      private final int maxCapacity;      /**   * 创建LRU缓存   * @param maxCapacity 最大容量   */    public LRUCache(int maxCapacity) {      // 第三个参数accessOrder为true表示按访问顺序排序      super(16, 0.75f, true);      this.maxCapacity = maxCapacity;    }      /**   * 重写此方法决定何时删除最老的条目   * @param eldest 最老的条目(最近最少使用)   * @return true表示应该删除最老的条目   */    @Override    protected boolean removeEldestEntry(Map.Entry eldest) {      // 当大小超过最大容量时,删除最老的条目      return size() > maxCapacity;    }      /**   * 获取缓存值(会更新访问顺序)   */    @Override    public V get(Object key) {      synchronized (this) {            return super.get(key);      }    }      /**   * 放入缓存值   */    @Override    public V put(K key, V value) {      synchronized (this) {            return super.put(key, value);      }    }      /**   * 获取缓存统计信息   */    public CacheStats getStats() {      return new CacheStats(size(), maxCapacity);    }      /**   * 获取最近访问的N个键   */    public List getRecentlyAccessed(int n) {      List result = new ArrayList();      Iterator iterator = keySet().iterator();                for (int i = 0; i < n && iterator.hasNext(); i++) {            result.add(iterator.next());      }                return result;    }      /**   * 缓存统计信息   */    public static class CacheStats {      private final int currentSize;      private final int maxCapacity;      private final double usageRatio;                public CacheStats(int currentSize, int maxCapacity) {            this.currentSize = currentSize;            this.maxCapacity = maxCapacity;            this.usageRatio = maxCapacity > 0 ? (double) currentSize / maxCapacity : 0;      }                // getters...    }}/** * 线程安全的LRU缓存实现 */public class ConcurrentLRUCache {      private final LRUCache cache;      public ConcurrentLRUCache(int maxCapacity) {      this.cache = new LRUCache(maxCapacity);    }      /**   * 获取缓存值   */    public V get(K key) {      synchronized (cache) {            return cache.get(key);      }    }      /**   * 放入缓存值   */    public V put(K key, V value) {      synchronized (cache) {            return cache.put(key, value);      }    }      /**   * 批量放入缓存   */    public void putAll(Map

司寇涵涵 发表于 2026-1-30 08:21:58

谢谢分享,试用一下

威割 发表于 2026-2-1 00:25:07

感谢发布原创作品,程序园因你更精彩

秤陷曲 发表于 2026-2-2 06:00:33

很好很强大我过来先占个楼 待编辑

呵桢 发表于 2026-2-3 06:57:18

很好很强大我过来先占个楼 待编辑

仟仞 发表于 2026-2-3 07:03:55

很好很强大我过来先占个楼 待编辑

抽厉 发表于 2026-2-5 01:47:10

分享、互助 让互联网精神温暖你我

觐有 发表于 2026-2-5 06:44:57

不错,里面软件多更新就更好了

眩疝诺 发表于 2026-2-8 15:07:32

感谢分享,下载保存了,貌似很强大

蜴间囝 发表于 2026-2-9 09:34:51

感谢发布原创作品,程序园因你更精彩

劳暄美 发表于 2026-2-9 13:36:50

过来提前占个楼

赐度虻 发表于 2026-2-9 14:23:45

前排留名,哈哈哈

庇床铍 发表于 2026-2-9 16:59:26

感谢分享,下载保存了,貌似很强大

汇干环 发表于 2026-2-12 08:09:07

鼓励转贴优秀软件安全工具和文档!

岑韬哎 发表于 2026-2-24 07:17:33

过来提前占个楼

况雪柳 发表于 2026-2-25 03:15:44

东西不错很实用谢谢分享

申屠梓彤 发表于 2026-2-27 01:01:18

用心讨论,共获提升!

章绮云 发表于 2026-3-3 05:12:11

前排留名,哈哈哈

贼瘁 发表于 2026-3-7 07:57:03

懂技术并乐意极积无私分享的人越来越少。珍惜

抽厉 发表于 2026-3-8 20:21:05

这个有用。
页: [1] 2
查看完整版本: 工作中最常用的5种本地缓存