【设计题】如何实现一个线程安全的缓存?

实现线程安全的缓存需解决并发读写一致性 (避免脏读、幻读)、缓存穿透 / 击穿 / 雪崩 (提升稳定性)、性能与锁竞争平衡 (减少开销开销)三大核心问题。以下是基于 Java 的完整实现方案,结合 ConcurrentHashMap、双重检查锁定和过期策略,兼顾安全性与高效性。

一、核心设计思路

  1. 底层容器选择 :用 ConcurrentHashMap 作为基础存储,其天然支持并发读写(分段锁优化,JDK 8 后为 CAS + synchronized),避免手动实现复杂同步逻辑。
  2. 缓存加载原子性 :通过双重检查锁定(Double-Checked Locking)解决 "缓存击穿"(同一 key 并发请求穿透到数据库),确保同一 key 仅被一个线程加载数据。
  3. 过期策略:支持缓存过期自动清理,避免无效数据占用内存(基于定时任务 + 懒清理)。
  4. 缓存更新与失效 :提供安全的 putremove 方法,确保并发场景下数据一致性。

二、代码实现

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();
    }
}

三、关键设计解析

  1. 并发安全基础 底层依赖 ConcurrentHashMap,其 get 方法无锁,put/remove 方法通过 CAS 和 synchronized 实现线程安全,避免全局锁导致的性能瓶颈。

  2. 防缓存击穿(双重检查锁定)

    • 第一重检查:无锁快速判断缓存是否命中,减少锁竞争。
    • 第二重检查:加锁(锁粒度为 key)后再次确认,确保同一 key 仅被一个线程加载数据(避免高并发下重复查库)。
    • 锁粒度为 key 而非整个缓存,大幅降低不同 key 间的锁竞争。
  3. 过期策略

    • 懒清理get 方法获取数据时,自动检查是否过期,过期则重新加载(避免无效数据返回)。
    • 定时清理:后台线程定期删除过期条目,防止过期数据长期占用内存(补充懒清理的不足)。
  4. 避免缓存穿透 代码中对 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,证明并发请求下数据加载仅执行一次,缓存生效且线程安全。

五、扩展优化方向

  1. 缓存雪崩防护 :对不同 key 设置随机过期时间(如 defaultExpireSeconds ± 5),避免大量缓存同时过期导致的数据库压力。
  2. 最大容量限制 :结合 LRU(最近最少使用)算法,当缓存达到最大容量时淘汰不常用数据(可基于 LinkedHashMap 实现)。
  3. 异步加载 :对耗时较长的 loader,改用异步加载(如 CompletableFuture),避免线程阻塞。
  4. 监控统计:增加缓存命中率、加载耗时等指标统计,便于性能调优。

总结

该实现通过 ConcurrentHashMap 保证基础并发安全,双重检查锁定解决缓存击穿,结合懒清理与定时清理实现过期策略,在安全性、性能和可用性之间取得平衡,适合高并发场景下的缓存需求。

相关推荐
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于Java的相机专卖网的设计与实现为例,包含答辩的问题和答案
java·开发语言
CodeAmaz2 小时前
Zookeeper 分布式锁实战版
java·分布式·后端·zookeeper
海域云SeaArea_2 小时前
CentOS7 单机安装 Zookeeper 3.5.8(JDK 1.8 环境)
java·zookeeper·java-zookeeper
后端小张2 小时前
【JAVA 进阶】SpringAI人工智能框架深度解析:从理论到实战的企业级AI应用开发指南
java·开发语言·人工智能
麦烤楽鸡翅2 小时前
小红书推荐系统(牛客)
java·python·算法·秋招·春招·牛客·面试算法题
java水泥工2 小时前
大学城水电管理系统|基于SpringBoot和Vue的大学城水电管理系统(源码+数据库+文档)
spring boot·vue·计算机毕业设计·大学生毕业设计·水电管理系统
星光一影2 小时前
基于Spring Boot电子签平台,实名认证+CA证书
大数据·spring boot·开源·vue·html5
C++业余爱好者2 小时前
.NET线程池ThreadPool.QueueUserWorkItem
java·数据库·.net
.豆鲨包3 小时前
【Android】Android内存缓存LruCache与DiskLruCache的使用及实现原理
android·java·缓存