工作中最常用的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 谢谢分享,试用一下 感谢发布原创作品,程序园因你更精彩 很好很强大我过来先占个楼 待编辑 很好很强大我过来先占个楼 待编辑 很好很强大我过来先占个楼 待编辑 分享、互助 让互联网精神温暖你我 不错,里面软件多更新就更好了 感谢分享,下载保存了,貌似很强大 感谢发布原创作品,程序园因你更精彩 过来提前占个楼 前排留名,哈哈哈 感谢分享,下载保存了,貌似很强大 鼓励转贴优秀软件安全工具和文档! 过来提前占个楼 东西不错很实用谢谢分享 用心讨论,共获提升! 前排留名,哈哈哈 懂技术并乐意极积无私分享的人越来越少。珍惜 这个有用。
页:
[1]
2