当前位置: 首页 > news >正文

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

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

一、核心设计思路

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

二、代码实现

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 并设置短过期时间,避免频繁穿透)。

四、使用示例

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

http://www.dtcms.com/a/574201.html

相关文章:

  • 网站透明效果wordpress广告插件中文
  • 网站建设费用进会计什么科目界面设计与制作是做什么的
  • 中小企业网站建设如何c 网站开发教程
  • 深度学习-池化层
  • ruoyi-app学习路线
  • 网站群建设意见网站建设+廊坊
  • 数据库关系模式核心概念详解:候选关键字与无损连接判断
  • 做外贸上哪些网站找客户网页设计收费标准
  • 阿里云ALB可编程脚本示例
  • wordpress网站非常慢网站备案协议书
  • Nginx防御HTTP Host头注入漏洞:实战配置漏洞修复教程
  • 南宁手机网站制作公司软件工程学什么课程
  • HTML - 换行标签的 3 种写法(<br>、<br/>、<br />)
  • 做电影网站需要的服务器配置wordpress程序伪静态
  • 是网站建设专业好做如美团式网站要多少钱
  • RPA概念是什么?和AI有哪些区别?
  • NO2A-(t-Bu ester),174137-97-4是一种双功能螯合剂
  • 网站数据分析视频黄金网站app下载免费
  • C++ thread类
  • 人工智能训练师备考——2.1.2题解
  • 网站设置反爬虫的常用方法有哪些附近的灯箱广告制作
  • 基于单片机的太阳能光伏板自动调整系统(论文+源码)
  • 济南网站建设与优化coding.net wordpress
  • 【软件系统信息化项目验收全流程指南】
  • [作品集]-青蛙记账
  • PCB板阻焊层和助焊层理解
  • 电脑鼠标dpi是什么意思?实用设置教程分享
  • 网站开发开票编码归属石家庄工程造价信息网官网
  • Parasoft C/C++test如何解决在VC6环境中单元测试的LNK2005错误
  • 从零打造Godot游戏:丛林探险开发实录