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

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

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

基础需求

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

生活类比:

缓存像冰箱🧊:

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

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

java 复制代码
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();
    }
}

使用示例:

java 复制代码
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:支持定时清理🔧

java 复制代码
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+过期)⭐

java 复制代码
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();
    }
}

五、完整使用示例📝

java 复制代码
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();
    }
}

输出:

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

六、实战:用户Session缓存🔐

java 复制代码
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的使用

java 复制代码
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"); // 自动加载

功能对比

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

建议:

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

八、性能优化技巧⚡

技巧1:分段锁

java 复制代码
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:异步加载

java 复制代码
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:缓存穿透

java 复制代码
// ❌ 错误:不存在的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:缓存雪崩

java 复制代码
// ❌ 错误:所有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)是错误的?指令重排序的陷阱!🔐

相关推荐
用户68545375977694 小时前
🔄 ConcurrentHashMap进化史:从分段锁到CAS+synchronized
后端
程序员小凯4 小时前
Spring Boot API文档与自动化测试详解
java·spring boot·后端
数据小馒头4 小时前
Web原生架构 vs 传统C/S架构:在数据库管理中的性能与安全差异
后端
用户68545375977694 小时前
🔑 AQS抽象队列同步器:Java并发编程的"万能钥匙"
后端
yren4 小时前
Mysql 多版本并发控制 MVCC
后端
回家路上绕了弯4 小时前
外卖员重复抢单?从技术到运营的全链路解决方案
分布式·后端
考虑考虑4 小时前
解决idea导入项目出现不了maven
java·后端·maven
数据飞轮4 小时前
不用联网、不花一分钱,这款开源“心灵守护者”10分钟帮你建起个人情绪疗愈站
后端
Amos_Web4 小时前
Rust实战课程--网络资源监控器(初版)
前端·后端·rust