注册

线程安全过期缓存:手写Guava Cache🗄️


缓存是性能优化的利器,但如何保证线程安全、支持过期、防止内存泄漏?让我们从零开始,打造一个生产级缓存!



一、开场:缓存的核心需求🎯


基础需求



  1. 线程安全:多线程并发读写
  2. 过期淘汰:自动删除过期数据
  3. 容量限制:防止内存溢出
  4. 性能优化:高并发访问

生活类比:


缓存像冰箱🧊:



  • 存储食物(数据)
  • 定期检查过期(过期策略)
  • 空间有限(容量限制)
  • 多人使用(线程安全)



二、版本1:基础线程安全缓存


public class SimpleCache<K, V> {

private final ConcurrentHashMap<K, CacheEntry<V>> cache =
new ConcurrentHashMap<>();

// 缓存项
static class CacheEntry<V> {
final V value;
final long expireTime; // 过期时间戳

CacheEntry(V value, long ttl) {
this.value = value;
this.expireTime = System.currentTimeMillis() + ttl;
}

boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}

/**
* 存入缓存
*/

public void put(K key, V value, long ttlMillis) {
cache.put(key, new CacheEntry<>(value, ttlMillis));
}

/**
* 获取缓存
*/

public V get(K key) {
CacheEntry<V> entry = cache.get(key);

if (entry == null) {
return null;
}

// 检查是否过期
if (entry.isExpired()) {
cache.remove(key); // 惰性删除
return null;
}

return entry.value;
}

/**
* 删除缓存
*/

public void remove(K key) {
cache.remove(key);
}

/**
* 清空缓存
*/

public void clear() {
cache.clear();
}

/**
* 缓存大小
*/

public int size() {
return cache.size();
}
}

使用示例:


SimpleCache<String, User> cache = new SimpleCache<>();

// 存入缓存,5秒过期
cache.put("user:1", new User("张三"), 5000);

// 获取缓存
User user = cache.get("user:1"); // 5秒内返回User对象

Thread.sleep(6000);

User expired = cache.get("user:1"); // 返回null(已过期)

问题:



  • ❌ 过期数据需要访问时才删除(惰性删除)
  • ❌ 没有容量限制,可能OOM
  • ❌ 没有定时清理,内存泄漏



三、版本2:支持定时清理🔧


public class CacheWithCleanup<K, V> {

private final ConcurrentHashMap<K, CacheEntry<V>> cache =
new ConcurrentHashMap<>();

private final ScheduledExecutorService cleanupExecutor;

static class CacheEntry<V> {
final V value;
final long expireTime;

CacheEntry(V value, long ttl) {
this.value = value;
this.expireTime = System.currentTimeMillis() + ttl;
}

boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}

public CacheWithCleanup() {
this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder()
.setNameFormat("cache-cleanup-%d")
.setDaemon(true)
.build()
);

// 每秒清理一次过期数据
cleanupExecutor.scheduleAtFixedRate(
this::cleanup,
1, 1, TimeUnit.SECONDS
);
}

public void put(K key, V value, long ttlMillis) {
cache.put(key, new CacheEntry<>(value, ttlMillis));
}

public V get(K key) {
CacheEntry<V> entry = cache.get(key);

if (entry == null || entry.isExpired()) {
cache.remove(key);
return null;
}

return entry.value;
}

/**
* 定时清理过期数据
*/

private void cleanup() {
cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
}

/**
* 关闭缓存
*/

public void shutdown() {
cleanupExecutor.shutdown();
cache.clear();
}
}

改进:



  • ✅ 定时清理过期数据
  • ✅ 不依赖访问触发删除

问题:



  • ❌ 还是没有容量限制
  • ❌ 没有LRU淘汰策略



四、版本3:完整的缓存实现(LRU+过期)⭐


import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class AdvancedCache<K, V> {

// 缓存容量
private final int maxSize;

// 存储:ConcurrentHashMap + LinkedHashMap(LRU)
private final ConcurrentHashMap<K, CacheEntry<V>> cache;

// 定时清理线程
private final ScheduledExecutorService cleanupExecutor;

// 统计信息
private final AtomicInteger hitCount = new AtomicInteger(0);
private final AtomicInteger missCount = new AtomicInteger(0);

// 缓存项
static class CacheEntry<V> {
final V value;
final long expireTime;
volatile long lastAccessTime; // 最后访问时间

CacheEntry(V value, long ttl) {
this.value = value;
this.expireTime = System.currentTimeMillis() + ttl;
this.lastAccessTime = System.currentTimeMillis();
}

boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}

void updateAccessTime() {
this.lastAccessTime = System.currentTimeMillis();
}
}

public AdvancedCache(int maxSize) {
this.maxSize = maxSize;
this.cache = new ConcurrentHashMap<>(maxSize);

this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder()
.setNameFormat("cache-cleanup-%d")
.setDaemon(true)
.build()
);

// 每秒清理过期数据
cleanupExecutor.scheduleAtFixedRate(
this::cleanup,
1, 1, TimeUnit.SECONDS
);
}

/**
* 存入缓存
*/

public void put(K key, V value, long ttlMillis) {
// 检查容量
if (cache.size() >= maxSize) {
evictLRU(); // LRU淘汰
}

cache.put(key, new CacheEntry<>(value, ttlMillis));
}

/**
* 获取缓存
*/

public V get(K key) {
CacheEntry<V> entry = cache.get(key);

if (entry == null) {
missCount.incrementAndGet();
return null;
}

// 检查过期
if (entry.isExpired()) {
cache.remove(key);
missCount.incrementAndGet();
return null;
}

// 更新访问时间
entry.updateAccessTime();
hitCount.incrementAndGet();

return entry.value;
}

/**
* 带回调的获取(类似Guava Cache)
*/

public V get(K key, Callable<V> loader, long ttlMillis) {
CacheEntry<V> entry = cache.get(key);

// 缓存命中且未过期
if (entry != null && !entry.isExpired()) {
entry.updateAccessTime();
hitCount.incrementAndGet();
return entry.value;
}

// 缓存未命中,加载数据
try {
V value = loader.call();
put(key, value, ttlMillis);
return value;
} catch (Exception e) {
throw new RuntimeException("加载数据失败", e);
}
}

/**
* LRU淘汰:移除最久未访问的
*/

private void evictLRU() {
K lruKey = null;
long oldestAccessTime = Long.MAX_VALUE;

// 找出最久未访问的key
for (Map.Entry<K, CacheEntry<V>> entry : cache.entrySet()) {
long accessTime = entry.getValue().lastAccessTime;
if (accessTime < oldestAccessTime) {
oldestAccessTime = accessTime;
lruKey = entry.getKey();
}
}

if (lruKey != null) {
cache.remove(lruKey);
}
}

/**
* 定时清理过期数据
*/

private void cleanup() {
cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
}

/**
* 获取缓存命中率
*/

public double getHitRate() {
int total = hitCount.get() + missCount.get();
return total == 0 ? 0 : (double) hitCount.get() / total;
}

/**
* 获取统计信息
*/

public String getStats() {
return String.format(
"缓存统计: 大小=%d, 命中=%d, 未命中=%d, 命中率=%.2f%%",
cache.size(),
hitCount.get(),
missCount.get(),
getHitRate() * 100
);
}

/**
* 关闭缓存
*/

public void shutdown() {
cleanupExecutor.shutdown();
cache.clear();
}
}



五、完整使用示例📝


public class CacheExample {

public static void main(String[] args) throws InterruptedException {

// 创建缓存:最大100个,5秒过期
AdvancedCache<String, User> cache = new AdvancedCache<>(100);

// 1. 基本使用
cache.put("user:1", new User("张三", 20), 5000);
User user = cache.get("user:1");
System.out.println("获取缓存: " + user);

// 2. 带回调的获取(自动加载)
User user2 = cache.get("user:2", () -> {
// 模拟从数据库加载
System.out.println("从数据库加载 user:2");
return new User("李四", 25);
}, 5000);
System.out.println("加载数据: " + user2);

// 3. 再次获取(命中缓存)
User cached = cache.get("user:2");
System.out.println("命中缓存: " + cached);

// 4. 等待过期
Thread.sleep(6000);
User expired = cache.get("user:1");
System.out.println("过期数据: " + expired); // null

// 5. 查看统计
System.out.println(cache.getStats());

// 6. 关闭缓存
cache.shutdown();
}
}

输出:


获取缓存: User{name='张三', age=20}
从数据库加载 user:2
加载数据: User{name='李四', age=25}
命中缓存: User{name='李四', age=25}
过期数据: null
缓存统计: 大小=1, 命中=2, 未命中=1, 命中率=66.67%



六、实战:用户Session缓存🔐


public class SessionCache {

private final AdvancedCache<String, UserSession> cache;

public SessionCache() {
this.cache = new AdvancedCache<>(10000); // 最大1万个session
}

/**
* 创建Session
*/

public String createSession(Long userId) {
String sessionId = UUID.randomUUID().toString();
UserSession session = new UserSession(userId, LocalDateTime.now());

// 30分钟过期
cache.put(sessionId, session, 30 * 60 * 1000);

return sessionId;
}

/**
* 获取Session
*/

public UserSession getSession(String sessionId) {
return cache.get(sessionId);
}

/**
* 刷新Session(延长过期时间)
*/

public void refreshSession(String sessionId) {
UserSession session = cache.get(sessionId);
if (session != null) {
// 重新设置30分钟过期
cache.put(sessionId, session, 30 * 60 * 1000);
}
}

/**
* 删除Session(登出)
*/

public void removeSession(String sessionId) {
cache.remove(sessionId);
}

static class UserSession {
final Long userId;
final LocalDateTime createTime;

UserSession(Long userId, LocalDateTime createTime) {
this.userId = userId;
this.createTime = createTime;
}
}
}



七、与Guava Cache对比📊


Guava Cache的使用


LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 最大容量
.expireAfterWrite(5, TimeUnit.MINUTES) // 写入后过期
.expireAfterAccess(10, TimeUnit.MINUTES) // 访问后过期
.recordStats() // 记录统计
.build(new CacheLoader<String, User>() {
@Override
public User load(String key) throws Exception {
return loadUserFromDB(key); // 加载数据
}
});

// 使用
User user = cache.get("user:1"); // 自动加载

功能对比


功能自定义CacheGuava Cache
线程安全
过期时间
LRU淘汰
自动加载
弱引用
统计信息
监听器
刷新

建议:



  • 简单场景:自定义实现
  • 生产环境:用Guava Cache或Caffeine



八、性能优化技巧⚡


技巧1:分段锁


public class SegmentedCache<K, V> {

private final int segments = 16;
private final AdvancedCache<K, V>[] caches;

@SuppressWarnings("unchecked")
public SegmentedCache(int totalSize) {
this.caches = new AdvancedCache[segments];
int sizePerSegment = totalSize / segments;

for (int i = 0; i < segments; i++) {
caches[i] = new AdvancedCache<>(sizePerSegment);
}
}

private AdvancedCache<K, V> getCache(K key) {
int hash = key.hashCode();
int index = (hash & Integer.MAX_VALUE) % segments;
return caches[index];
}

public void put(K key, V value, long ttl) {
getCache(key).put(key, value, ttl);
}

public V get(K key) {
return getCache(key).get(key);
}
}

技巧2:异步加载


public class AsyncCache<K, V> {

private final AdvancedCache<K, CompletableFuture<V>> cache;
private final ExecutorService loadExecutor;

public CompletableFuture<V> get(K key, Callable<V> loader, long ttl) {
return cache.get(key, () ->
CompletableFuture.supplyAsync(() -> {
try {
return loader.call();
} catch (Exception e) {
throw new CompletionException(e);
}
}, loadExecutor),
ttl
);
}
}



九、常见陷阱⚠️


陷阱1:缓存穿透


// ❌ 错误:不存在的key反复查询数据库
public User getUser(String userId) {
User user = cache.get(userId);
if (user == null) {
user = loadFromDB(userId); // 每次都查数据库
if (user != null) {
cache.put(userId, user, 5000);
}
}
return user;
}

// ✅ 正确:缓存空对象
public User getUser(String userId) {
User user = cache.get(userId);
if (user == null) {
user = loadFromDB(userId);
// 即使是null也缓存,但设置短过期时间
cache.put(userId, user != null ? user : NULL_USER, 1000);
}
return user == NULL_USER ? null : user;
}

陷阱2:缓存雪崩


// ❌ 错误:所有key同时过期
for (String key : keys) {
cache.put(key, value, 5000); // 5秒后同时过期
}

// ✅ 正确:过期时间随机化
for (String key : keys) {
long ttl = 5000 + ThreadLocalRandom.current().nextInt(1000);
cache.put(key, value, ttl); // 5-6秒随机过期
}



十、面试高频问答💯


Q1: 如何保证缓存的线程安全?


A:



  • 使用ConcurrentHashMap
  • volatile保证可见性
  • CAS操作保证原子性

Q2: 如何实现过期淘汰?


A:



  • 惰性删除:访问时检查过期
  • 定时删除:定时任务扫描
  • 两者结合

Q3: 如何实现LRU?


A:



  • 记录访问时间
  • 容量满时淘汰最久未访问的

Q4: 缓存穿透/击穿/雪崩的区别?


A:



  • 穿透:查询不存在的key,缓存和DB都没有
  • 击穿:热点key过期,大量请求打到DB
  • 雪崩:大量key同时过期



十一、总结🎯


核心要点



  1. 线程安全:ConcurrentHashMap
  2. 过期策略:定时清理+惰性删除
  3. 容量限制:LRU淘汰
  4. 性能优化:分段锁、异步加载
  5. 监控统计:命中率、容量

生产建议



  • 简单场景:自己实现
  • 复杂场景:用Guava Cache
  • 极致性能:用Caffeine



下期预告: 为什么双重检查锁定(DCL)是错误的?指令重排序的陷阱!🔐


作者:用户6854537597769
来源:juejin.cn/post/7563511077180473386

0 个评论

要回复文章请先登录注册