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

缓存系统-基本概述

目录

一、系统概述

二、名词解释

三、淘汰策略

1、LRU

2、LFU

3、FIFO

4、TTL

5、Random

四、读写模式

1、Cache Aside(旁路缓存)

2、Write Through(直写)

3、Write Back(回写)

五、问题方案

1、缓存穿透

2、缓存雪崩

3、缓存击穿

4、最佳实践


一、系统概述

缓存(Cache)是通过临时存储高频访问数据来加速数据访问的技术组件,本质是"空间换时间"的典型实践。其核心价值体现在:

  • 性能加速:缩短数据访问路径(CPU缓存 vs 内存访问速度差达100倍)
  • 资源节约:减少对底层数据源的访问压力(数据库查询成本降低80%+)
  • 系统稳定:应对突发流量冲击(如秒杀场景QPS可达10万级)

二、名词解释

  1. 缓存命中率:请求缓存时,数据存在的概率(命中次数 / 总请求次数),命中率>90%为高效,<70%需优化(如调整淘汰策略或容量);
  2. 缓存穿透:请求不存在的数据(如恶意攻击或无效ID),绕过缓存直击后端;
  3. 缓存击穿:热点数据过期瞬间,高并发请求压垮数据库;
  4. 缓存雪崩:大量缓存同时过期,导致请求洪峰冲击后端;
  5. 缓存污染:缓存中存储了大量非高频访问或无效数据,导致缓存命中率下降,进而降低系统整体性能的现象。本质是缓存资源被低价值数据占用,无法有效服务高频请求。
  6. TTL(Time To Live):缓存数据存活时间;
  7. 冷热数据分离:高频/低频数据分区存储
  8. 缓存预热:系统启动时加载热点数据

三、淘汰策略

缓存系统通过淘汰策略在容量不足时决定移除哪些数据,核心目标是最大化缓存命中率。

策略

时间复杂度

空间开销

适用场景

命中率

LRU

O(1)

通用场景(Web缓存)

★★★★☆

LFU

O(1)~O(log n)

热点数据集中(视频推荐)

★★★★★

FIFO

O(1)

简单顺序访问(日志缓冲)

★★☆☆☆

TTL

O(log n)

时效性数据(会话/验证码)

★★★☆☆

Random

O(1)

低成本容忍场景

★★☆☆☆

1、LRU

核心思想:优先淘汰最久未被访问的数据
数据结构:哈希表 + 双向链表(哈希表存储键值对,链表维护访问顺序)
操作流程

  • 数据访问
    • 命中缓存:将节点移到链表头部
    • 未命中:从数据库加载,新节点插入链表头部
  • 淘汰触发
    • 链表尾部节点(最久未访问)被移除
    • 同步删除哈希表中对应键

优点

  • 高效反映时间局部性(最近访问的数据更可能再被访问)
  • 时间复杂度:O(1)(哈希表定位 + 链表移动)

缺点

  • 突发批量访问可能污染缓存(如全表扫描)
  • 链表维护增加内存开销

2、LFU

核心思想:优先淘汰访问频率最低的数据
数据结构:双哈希表(键值存储 + 频率-键列表) + 最小堆/双向链表
操作流程

  • 数据访问
    • 命中缓存:增加计数,调整在频率链表中的位置
  • 淘汰触发
    • 移除最低频率链表中的最早节点(LRU作为次级策略)
    • 更新最小频率值

优点

  • 精准保护高频访问数据
  • 适合长期热点数据场景(如电商热门商品)

缺点

  • 新数据易被淘汰(初始频率低)
  • 维护成本高(需频率排序)
  • 历史高频但不再访问的数据可能滞留

3、FIFO

核心思想:按进入缓存的顺序淘汰
数据结构:队列(数组/链表实现)
操作流程

(1)数据写入:新数据加入队尾

(2)淘汰触发:移除队首数据

优点

  • 实现简单(仅需队列)
  • 零额外内存开销

缺点

  • 忽略访问模式(可能淘汰热点数据)
  • 缓存命中率通常最低

4、TTL

核心思想:基于过期时间自动淘汰
数据结构:哈希表 + 时间堆(或轮询检查)
操作流程

  • 数据写入:设置过期时间戳(当前时间 + TTL)
  • 淘汰触发
    • 主动:定期扫描过期数据(定时器)
    • 被动:访问时检查过期并删除

优点

  • 保证数据时效性(适合会话缓存)
  • 避免手动清理

缺点

  • 可能提前移除仍有价值的数据
  • 扫描机制消耗CPU(大缓存需优化)

5、Random

核心思想:随机选择数据淘汰
数据结构:动态数组(如Python list)
操作流程

  • 淘汰触发:随机选择键删除
  • 数据维护:数组动态调整

优点

  • 实现极其简单
  • 无状态维护成本

缺点

  • 可能误删高频数据
  • 性能波动不可预测

四、读写模式

主要是要保证数据的一致性。

1、Cache Aside(旁路缓存)

旁路缓存模式,也称为懒加载(Lazy Loading),是最常见的缓存模式。

应用程序直接与缓存和数据库(或主数据存储)交互。

优点:实现简单,缓存只保存实际被请求的数据,节省内存。
缺点:缓存未命中时,需要访问数据库,可能导致延迟。另外,在写操作后立即读,可能会因为缓存失效而读到旧数据(需要等到下次加载),但通常通过删除缓存保证一致性。

读流程

  • 缓存命中:应用程序首先检查缓存。如果数据存在(命中),则直接返回缓存数据。
  • 缓存未命中

        (1)应用程序从数据库中读取数据。

        (2)将读取到的数据写入缓存(以便后续读取命中)。

        (3)返回数据。

写流程

  • 应用程序直接更新数据库。
  • 同时,使缓存中对应的数据失效(删除缓存项)。这样,下次读取时,会触发缓存未命中,从而从数据库加载最新数据并重新填充缓存。


 

2、Write Through(直写)

在直写模式中,缓存作为数据库的代理层。

写操作总是先经过缓存,然后由缓存同步更新到数据库。

优点:缓存和数据库始终保持一致(强一致性)。读操作很少会访问数据库,因为写操作已经更新了缓存。
缺点:写操作延迟较高,因为需要等待数据库写入完成。如果数据不经常被读取,那么写入缓存可能造成资源浪费(因为每次写都更新缓存,即使很少读)。

读流程

  • 缓存命中:直接返回缓存数据。
  • 缓存未命中

        (1)缓存从数据库中加载数据(或由应用程序触发加载)。

        (2)将数据放入缓存。

        (3)返回数据。

写流程

(1)应用程序更新缓存(如果数据在缓存中不存在,则创建缓存项)。

(2)缓存立即将数据同步写入数据库(通常在一个事务内)。

(3)只有在数据库写入成功后,写操作才算完成。


 

3、Write Back(回写)

回写模式(也称为Write-Behind)中,写操作首先写入缓存,然后异步批量写入数据库。缓存作为写操作的缓冲区。

优点:写操作非常快,因为应用程序不需要等待数据库写入。可以合并多次写操作,减少数据库压力。
缺点:数据不一致的风险(缓存和数据库在异步同步前不一致)。如果缓存崩溃,尚未写入数据库的数据会丢失。因此,通常需要额外的机制(如写日志)来保证数据持久性。

读流程

  • 缓存命中:直接返回缓存数据。
  • 缓存未命中

        (1)从数据库中加载数据到缓存。

        (2)返回数据。

写流程

(1)应用程序更新缓存(如果数据不在缓存中,则先加载到缓存再更新,或者直接创建新的缓存项)。

(2)缓存标记数据为“脏”(dirty),表示需要同步到数据库。

(3)缓存立即返回成功给应用程序(无需等待数据库写入)。

(4)缓存会在之后的某个时间点(例如,缓存满时、定时任务、或者低负载时)将“脏”数据批量写入数据库。

 

五、问题方案

1、缓存穿透

问题原因

        当查询不存在的数据时(如无效ID、不存在的用户名),每次请求都会穿透缓存层直接访问数据库。在恶意攻击场景下(如脚本批量请求随机ID),数据库会持续承受无效查询压力,导致性能急剧下降甚至崩溃。这种现象与正常缓存未命中的区别在于:正常未命中是偶发的,而穿透是持续性的无效查询。

// 大量恶意调用
getData("invalid_id_1");
getData("invalid_id_2");
...// 缓存访问接口
std::string getData(const std::string& key) 
{auto data = cache.get(key); // 缓存查询if (data.empty()) {data = db.query(key);   // 缓存未命中,查询数据库cache.set(key, data);   // 写入缓存}return data;
}

解决方案

(1)布隆过滤器(Bloom Filter)

  • 在缓存层前设置布隆过滤器作为屏障
  • 工作原理:使用多个哈希函数将键映射到位数组中,查询时:
    • 若键不在过滤器中 → 直接返回空(拦截非法请求)
    • 若键可能存在 → 继续查询缓存/数据库
  • 特点:存在误判率(通常<1%),但内存效率极高(1亿键仅需约100MB)

(2)空值缓存(Cache Null Object)

  • 对查询结果为空的键,缓存特殊标记(如"NULL")
  • 设置较短过期时间(5-30秒),防止恶意请求耗尽空间
  • 需配合监控清理机制,避免存储过多无效键
// C++ 布隆过滤器+空值缓存实现
class BloomFilter {
private:std::vector<bool> bits;std::vector<std::hash<std::string>> hashers;public:BloomFilter(size_t size, int hash_count) : bits(size), hashers(hash_count) {}void add(const std::string& key) {for (auto& hash_fn : hashers) {size_t pos = hash_fn(key) % bits.size();bits[pos] = true;}}bool may_contain(const std::string& key) {for (auto& hash_fn : hashers) {size_t pos = hash_fn(key) % bits.size();if (!bits[pos]) return false;}return true;}
};// 使用示例
BloomFilter filter(1000000, 3); // 100万位,3个哈希std::string get_data(const std::string& key) {// 布隆过滤器拦截if (!filter.may_contain(key)) return "";// 缓存查询auto data = cache.get(key);if (data == "NULL") return ""; // 空值标识if (data.empty()) {data = db.query(key);if (data.empty()) {cache.set(key, "NULL", 15); // 缓存空值15秒return "";}cache.set(key, data, 3600); // 缓存有效数据}return data;
}

2、缓存雪崩

问题原因

        当大量缓存在同一时间段集中过期(如缓存初始化时设置相同TTL),瞬时会有海量请求穿透到数据库。典型场景包括:

  • 系统启动时批量加载缓存
  • 定时任务刷新缓存
  • 缓存服务器重启

雪崩效应会导致数据库出现流量尖峰,引发连锁故障(如连接池耗尽、CPU过载)。

解决方案

(1)差异化过期时间

  • 基础过期时间 + 随机偏移(如30分钟 ± 5分钟)
  • 确保缓存失效时间均匀分布,避免集中失效

(2)双层缓存策略

  • 主缓存:设置较短TTL(30分钟),承担日常请求
  • 备份缓存:设置长TTL(24-48小时)
  • 当主缓存失效时:
    • 先返回备份缓存数据
    • 异步重建主缓存
  • 保证即使主缓存失效,仍有备份数据可用

(3)热数据永不过期

  • 对核心热数据(如首页推荐)采用逻辑过期:
    • 物理上永不过期
    • 后台线程定期更新数据
    • 数据对象包含逻辑过期时间戳
// C++ 双层缓存+随机TTL实现
std::string get_data_avalanche_protected(const std::string& key) {// 随机数生成器static thread_local std::mt19937 rng(std::random_device{}());static std::uniform_int_distribution<int> dist(-300, 300);// 优先查询主缓存if (auto data = cache.get("primary_" + key); !data.empty()) return data;// 查询备份缓存if (auto backup = cache.get("backup_" + key); !backup.empty()) {// 异步重建主缓存std::thread([key, backup] {int ttl = 1800 + dist(rng); // 30分钟基础+随机偏移cache.set("primary_" + key, backup, ttl);}).detach();return backup;}// 查询数据库auto data = db.query(key);if (!data.empty()) {int primary_ttl = 1800 + dist(rng);cache.set("primary_" + key, data, primary_ttl);cache.set("backup_" + key, data, 86400); // 备份24小时}return data;
}

3、缓存击穿

问题原因

当某个热点Key突然失效时,瞬时海量并发请求同时涌入数据库。与雪崩的区别在于:

  • 雪崩:大量不同Key同时失效
  • 击穿:单个热点Key失效引发风暴

核心危害

  • 单点数据库压力暴增(万级QPS)
  • 可能引发连接池耗尽
  • 重建缓存时的重复查询浪费资源

解决方案

(1)互斥锁重建(分布式锁)

  • 当缓存失效时,仅允许一个线程执行数据库查询
  • 其他线程阻塞等待或重试
  • 关键点:
    • 锁粒度:Key级别锁而非全局锁
    • 锁超时:防止死锁(通常5-10秒)
    • 双重检查:获取锁后再次验证缓存

(2)逻辑过期永不过期

  • 缓存永不物理删除
  • 数据结构包含逻辑过期时间戳
  • 请求处理流程:
    • 返回当前缓存数据(无论是否过期)
    • 异步检查过期状态
    • 过期则触发重建
  • 优点:零等待时间,保证高并发下的可用性

(3)热点数据监控+预加载

  • 实时监控Key访问频率
  • 识别热点Key后:
    • 延长其TTL
    • 提前异步刷新
    • 在多个缓存节点复制
// C++ 互斥锁+逻辑过期实现
class HotspotProtection {
private:std::shared_mutex global_mutex;std::unordered_map<std::string, std::shared_ptr<std::mutex>> key_mutexes;struct CacheItem {int64_t logical_expire; // 逻辑过期时间戳(毫秒)std::string data;};public:std::string get_data(const std::string& key) {// 获取当前缓存项auto item = cache.get<CacheItem>(key);// 未逻辑过期直接返回if (item.logical_expire > get_system_time_millis()) return item.data;// 获取Key级别锁std::shared_ptr<std::mutex> key_mutex;{std::shared_lock read_lock(global_mutex);auto it = key_mutexes.find(key);if (it != key_mutexes.end()) key_mutex = it->second;}if (!key_mutex) {std::unique_lock write_lock(global_mutex);key_mutex = key_mutexes[key] = std::make_shared<std::mutex>();}// 锁定并重建std::unique_lock lock(*key_mutex);// 双重检查item = cache.get<CacheItem>(key);if (item.logical_expire > get_system_time_millis()) return item.data;// 查询数据库auto new_data = db.query(key);int64_t new_expire = get_system_time_millis() + 3600000; // 1小时后过期// 更新缓存cache.set(key, CacheItem{new_expire, new_data});return new_data;}
};

4、最佳实践

防御策略

适用场景

优点

缺点

布隆过滤器

防恶意请求/无效键

内存高效,拦截精确

存在误判率

空值缓存

处理不存在数据

简单易实现

可能存储大量无效键

随机TTL

防批量缓存同时失效

有效分散压力

无法应对热点Key失效

双层缓存

高可用场景

主备切换平滑

增加内存开销

互斥锁重建

防热点Key击穿

保证数据一致性

增加请求延迟

逻辑过期

超高并发热点数据

零等待时间,极致性能

实现复杂度高

分层防御体系

  • 第一层:布隆过滤器拦截非法请求
  • 第二层:空值缓存处理无效查询
  • 第三层:随机TTL+双层缓存防雪崩
  • 第四层:互斥锁+逻辑过期防击穿

热点数据特殊处理

设置热点阈值,对于热点 key,延长 TTL、多节点复制

// 热点Key识别与预加载
class HotspotManager {
public:void monitor_access(const std::string& key) {access_count[key]++;if (access_count[key] > 1000) {   // 达到热点阈值extend_ttl(key, 3600);        // 延长TTLpreload_replica(key);        // 多节点复制}}
};

熔断降级机制

  • 当数据库压力超过阈值时:
    • 自动触发熔断
    • 返回降级内容(如默认推荐)
    • 记录日志异步补偿

持续监控指标

缓存命中率 > 95% # 低于阈值告警

数据库QPS < 3000 # 超过阈值扩容

穿透请求量 < 100/s # 超过阈值启动防御

http://www.lryc.cn/news/571529.html

相关文章:

  • Ajax 核心知识点全面总结
  • 前端开发面试题总结-vue2框架篇(三)
  • 网络层协议 IP 协议介绍 -- IP 协议,网段划分,私有 IP 和 公网 IP,路由
  • KingbaseES 在线体验平台深度评测
  • 计算机硬件——外设、其他部件
  • CentOS7 安装最新版 Docker
  • 【MySQL】MySQL 数据库操作与设计
  • 【系统设计【4】】设计一个限流器:从理论到实践的完整解决方案
  • 从C++编程入手设计模式——外观模式
  • AI智能体应用市场趋势分析
  • Black自动格式化工具
  • PINA开源程序用于高级建模的 Physics-Informed 神经网络
  • 实验分享|自研局部DIC-GPU算法与开源GPU算法对比实验
  • jenkins打包问题jar问题
  • Layui的table实现鼠标移入单元格后tips弹框提示
  • 【RocketMQ 生产者和消费者】- 消费者重平衡(1)
  • 《开窍》读书笔记9
  • day40- 硬件学习之 51单片机II (中断处理)
  • 开源 Arkts 鸿蒙应用 开发(一)工程文件分析
  • MIT 6.S081 2020 Lab9 File Systems 个人全流程
  • 使用 Java + WebSocket 实现简单实时双人协同 pk 答题
  • 什么是状态机?状态机入门
  • 神奇的bug之docker compose启动mysql失败
  • 服务器带宽小优化建议以及实战操作
  • 【在线五子棋对战】七、数据管理模块实现
  • Java 21 新特性深度解析:虚拟线程、结构化并发来袭!
  • 服务器静态ip,网关不能占用*.*.*.1
  • 【音视频】SIP基础、搭建服务器和客户端
  • Axios 知识点全面总结
  • 详解 MyBatis - Plus 服务层设计:让 CRUD 更高效、业务拓展更灵活——补充