实现线程安全的缓存需解决并发读写一致性 (避免脏读、幻读)、缓存穿透 / 击穿 / 雪崩 (提升稳定性)、性能与锁竞争平衡 (减少开销开销)三大核心问题。以下是基于 Java 的完整实现方案,结合 ConcurrentHashMap、双重检查锁定和过期策略,兼顾安全性与高效性。
一、核心设计思路
- 底层容器选择 :用
ConcurrentHashMap作为基础存储,其天然支持并发读写(分段锁优化,JDK 8 后为 CAS + synchronized),避免手动实现复杂同步逻辑。 - 缓存加载原子性 :通过双重检查锁定(Double-Checked Locking)解决 "缓存击穿"(同一 key 并发请求穿透到数据库),确保同一 key 仅被一个线程加载数据。
- 过期策略:支持缓存过期自动清理,避免无效数据占用内存(基于定时任务 + 懒清理)。
- 缓存更新与失效 :提供安全的
put、remove方法,确保并发场景下数据一致性。
二、代码实现
java
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.function.Function;
/**
* 线程安全缓存实现
* 支持:并发读写安全、缓存加载原子性、过期清理、防缓存击穿
*/
public class ThreadSafeCache<K, V> {
// 底层存储:ConcurrentHashMap保证基础并发安全
private final ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
// 加载数据的函数(如从数据库/远程服务获取数据)
private final Function<K, V> loader;
// 定时清理过期缓存的线程池
private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
// 默认缓存过期时间(单位:秒)
private final long defaultExpireSeconds;
/**
* 构造函数
* @param loader 数据加载函数(缓存未命中时调用)
* @param defaultExpireSeconds 默认过期时间(秒)
*/
public ThreadSafeCache(Function<K, V> loader, long defaultExpireSeconds) {
this.loader = loader;
this.defaultExpireSeconds = defaultExpireSeconds;
// 启动定时清理任务(每30秒执行一次)
this.cleaner.scheduleAtFixedRate(this::cleanExpiredEntries, 30, 30, TimeUnit.SECONDS);
}
/**
* 获取缓存值(核心方法)
* 1. 先快速检查缓存,命中则直接返回(无锁)
* 2. 未命中则加锁(锁粒度为key),再次检查后加载数据
*/
public V get(K key) {
// 第一重检查:无锁快速判断
CacheEntry<V> entry = cache.get(key);
if (isValid(entry)) {
return entry.value;
}
// 未命中,加锁后再次检查(避免并发重复加载)
synchronized (key) { // 锁粒度细化到key,减少竞争
entry = cache.get(key);
if (!isValid(entry)) { // 第二重检查:确认缓存确实未命中或已过期
// 调用loader加载数据(如查库)
V value = loader.apply(key);
if (value == null) {
// 避免缓存null值导致的缓存穿透(可根据业务调整)
return null;
}
// 存入缓存(带过期时间)
long expireTime = System.currentTimeMillis() + defaultExpireSeconds * 1000;
entry = new CacheEntry<>(value, expireTime);
cache.put(key, entry);
}
return entry.value;
}
}
/**
* 手动存入缓存
*/
public void put(K key, V value) {
put(key, value, defaultExpireSeconds);
}
/**
* 手动存入缓存(指定过期时间)
*/
public void put(K key, V value, long expireSeconds) {
Objects.requireNonNull(key, "key cannot be null");
Objects.requireNonNull(value, "value cannot be null");
long expireTime = System.currentTimeMillis() + expireSeconds * 1000;
cache.put(key, new CacheEntry<>(value, expireTime));
}
/**
* 移除缓存
*/
public void remove(K key) {
cache.remove(key);
}
/**
* 清理所有缓存
*/
public void clear() {
cache.clear();
}
/**
* 定时清理过期缓存(懒清理补充,避免过期数据堆积)
*/
private void cleanExpiredEntries() {
long now = System.currentTimeMillis();
// 遍历并移除过期条目
cache.entrySet().removeIf(entry -> {
CacheEntry<V> cacheEntry = entry.getValue();
return cacheEntry.expireTime < now;
});
}
/**
* 检查缓存条目是否有效(非空且未过期)
*/
private boolean isValid(CacheEntry<V> entry) {
if (entry == null) {
return false;
}
return System.currentTimeMillis() < entry.expireTime;
}
/**
* 缓存条目内部类:存储值和过期时间
*/
private static class CacheEntry<V> {
final V value; // 缓存值
final long expireTime; // 过期时间(毫秒时间戳)
CacheEntry(V value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}
}
/**
* 关闭缓存(释放资源)
*/
public void close() {
cleaner.shutdown();
}
}
三、关键设计解析
-
并发安全基础 底层依赖
ConcurrentHashMap,其get方法无锁,put/remove方法通过 CAS 和 synchronized 实现线程安全,避免全局锁导致的性能瓶颈。 -
防缓存击穿(双重检查锁定)
- 第一重检查:无锁快速判断缓存是否命中,减少锁竞争。
- 第二重检查:加锁(锁粒度为 key)后再次确认,确保同一 key 仅被一个线程加载数据(避免高并发下重复查库)。
- 锁粒度为
key而非整个缓存,大幅降低不同 key 间的锁竞争。
-
过期策略
- 懒清理 :
get方法获取数据时,自动检查是否过期,过期则重新加载(避免无效数据返回)。 - 定时清理:后台线程定期删除过期条目,防止过期数据长期占用内存(补充懒清理的不足)。
- 懒清理 :
-
避免缓存穿透 代码中对
loader返回的null值不缓存(可根据业务调整,如缓存null并设置短过期时间,避免频繁穿透)。
四、使用示例
java
public class CacheDemo {
public static void main(String[] args) {
// 模拟从数据库加载数据的函数(此处用简单逻辑代替)
Function<String, String> dbLoader = key -> {
System.out.println("Loading data from DB for key: " + key);
try {
Thread.sleep(100); // 模拟DB查询耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "value_" + key;
};
// 创建缓存(默认过期时间30秒)
ThreadSafeCache<String, String> cache = new ThreadSafeCache<>(dbLoader, 30);
// 多线程并发测试
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
final int num = i;
executor.submit(() -> {
String key = "key_" + (num % 3); // 模拟3个key的并发请求
String value = cache.get(key);
System.out.println("Thread " + Thread.currentThread().getId() + " get " + key + ": " + value);
});
}
executor.shutdown();
cache.close();
}
}
输出说明 :每个 key 仅会打印一次 Loading data from DB,证明并发请求下数据加载仅执行一次,缓存生效且线程安全。
五、扩展优化方向
- 缓存雪崩防护 :对不同 key 设置随机过期时间(如
defaultExpireSeconds ± 5),避免大量缓存同时过期导致的数据库压力。 - 最大容量限制 :结合 LRU(最近最少使用)算法,当缓存达到最大容量时淘汰不常用数据(可基于
LinkedHashMap实现)。 - 异步加载 :对耗时较长的
loader,改用异步加载(如CompletableFuture),避免线程阻塞。 - 监控统计:增加缓存命中率、加载耗时等指标统计,便于性能调优。
总结
该实现通过 ConcurrentHashMap 保证基础并发安全,双重检查锁定解决缓存击穿,结合懒清理与定时清理实现过期策略,在安全性、性能和可用性之间取得平衡,适合高并发场景下的缓存需求。