目录
一.什么是缓存
二.使用Redis作为缓存
2.1.关系型数据库的缺点
2.2.使用Redis作为MySQL的缓存
三. 缓存更新策略:识别热点数据
3.1.定期更新
3.2.实时生成
四.缓存的使用注意事项
4.1.缓存预热(Cache preheating)
4.2.关于缓存穿透 (Cache penetration)
4.3..关于缓存雪崩(Cache avalanche)
4.4.关于缓存击穿(Cache breakdown)
Redis 最主要的用途, 三个方面:
- 1. 存储数据(内存数据库)
- 2. 缓存 [redis 最常用的场景]
- 3. 消息队列
现在我们就来讲解一下,什么是缓存?
一.什么是缓存
缓存(Cache) 是计算机科学中一个非常核心且应用广泛的经典概念。它的核心思路在于:将那些访问频率较高或需要快速获取的数据,存储在一个访问速度更快的临时区域中,以便在需要时能够更快地读取,从而提升系统的整体效率和响应速度。简单来说,缓存就是用更快的存储空间来存放“热点数据”,以空间换取时间。
🧠 理解缓存的本质:一个生活化的例子
想象一下你需要乘坐高铁的经历。我们知道,乘坐高铁需要多次刷身份证(进入高铁站、检票、上车、乘车过程中可能查验、出站等)。正常情况下,你的身份证可能存放在行李箱里(行李箱容量大,能装很多东西)。但问题是,每次刷身份证都需要打开行李箱翻找,效率极低,非常不便。
为了优化这个过程,你提前把身份证从行李箱取出,放进了衣服口袋。口袋虽然空间有限(远小于行李箱),但其访问速度却快得多——你只需伸手就能立刻拿到。这样,每次刷身份证时,直接从口袋掏出即可,省去了反复开箱的麻烦。
在这个例子中:
-
行李箱 代表了 主存储(如硬盘、数据库或网络资源)。特点是:容量大,但访问速度相对较慢。
-
衣服口袋 就充当了 缓存。特点是:容量小,但访问速度极快。
-
身份证 就是当前需要频繁访问的 热点数据。
-
提前将身份证放入口袋 的过程,类似于 数据加载到缓存。
-
直接从口袋拿身份证 的过程,就是 缓存命中(Cache Hit),带来了显著的效率提升。
-
开行李箱找身份证 则相当于 缓存未命中(Cache Miss),需要去访问速度更慢的主存储,效率低下。
使用缓存的核心价值就在于它能大大减少访问慢速存储介质的次数,从而显著提升访问效率。
⚡ “触手可及”的相对性与硬件层级
缓存中提到的“触手可及”或“访问速度更快”是一个相对的概念。在计算机体系结构中,不同存储介质的访问速度存在明显的层级关系,通常遵循:
CPU寄存器 > CPU缓存(L1, L2, L3) > 内存(RAM) > 固态硬盘(SSD)/机械硬盘(HDD) > 网络存储/远程数据库
基于这种速度差异,我们可以构建多级缓存体系:
-
硬盘/网络作为最慢层: 硬盘(尤其是机械硬盘)或网络访问延迟很高。
-
内存作为硬盘/网络的缓存: 内存(RAM)的速度远快于硬盘和网络。因此,操作系统和应用程序会将经常需要读写的数据(如运行中的程序、打开的文件)从硬盘加载到内存中。浏览器也会将访问过的网页资源(如图片、脚本)缓存到内存或本地硬盘上,避免重复从网络下载。
-
CPU缓存作为内存的缓存: CPU内部的缓存(L1, L2, L3)速度又远快于内存。CPU会将当前正在处理的内存中的数据块预取到各级缓存中,供CPU核心高速访问。
-
寄存器作为CPU缓存的延伸: 寄存器是CPU内部最快最小的存储单元,用于存放当前指令直接操作的数据。
📌 缓存的关键特性与挑战
缓存机制带来了巨大的性能收益,但也伴随着两个关键特性及其带来的挑战:
-
速度与成本的权衡: 访问速度越快的存储设备,其单位容量的制造成本通常越高。
-
容量限制: 因此,缓存的空间总是有限的,远远小于它所缓存的主存储空间(如内存容量远小于硬盘,CPU缓存容量远小于内存)。
正是由于缓存空间有限,它不可能存放所有数据。 这就需要精明的策略来决定缓存中存放什么。缓存的核心价值在于存放“热点数据”(Hot Data),即那些被频繁访问的数据。通过将热点数据保留在快速访问的缓存层,系统就能在绝大多数请求中获得显著的性能提升。为了管理有限的缓存空间,需要缓存淘汰策略(如LRU - 最近最少使用、LFU - 最不经常使用等) 来决定当缓存满时,哪些旧数据应该被移除,以便为新数据腾出空间。
二八定律
在计算机科学,特别是缓存优化领域,一个广为人知且极具指导性的经验法则就是“二八定律”(也称为帕累托法则)。其核心思想可以精炼地表述为:
大约 20% 的数据(热点数据),往往承载了 80% 甚至更高的访问请求。
这个看似简单的比例关系,深刻地揭示了数据访问模式中普遍存在的不均衡性:并非所有数据被访问的频率都是均等的。系统中总存在一小部分数据,由于其重要性、时效性或关联性,会被用户或系统进程反复、高频地访问。
二.使用Redis作为缓存
2.1.关系型数据库的缺点
虽然关系型数据库(如 MySQL, PostgreSQL, SQL Server)因其强大的功能、数据一致性和成熟的生态系统而被广泛应用,但在处理某些特定场景,尤其是高并发、低延迟或海量数据的读写请求时,其性能可能会成为瓶颈。这主要源于其架构设计和工作机制带来的固有开销:
-
磁盘 I/O 瓶颈:访问速度的物理限制
-
核心问题: 关系型数据库的核心数据通常持久化存储在硬盘(HDD/SSD) 上,以确保数据的持久性(Durability)。然而,硬盘的 I/O 速度(尤其是随机读写) 相比 内存(RAM) 存在数量级的差距(内存访问速度通常是纳秒级,而磁盘是毫秒级)。
-
性能影响: 任何需要访问磁盘数据的操作(查询、插入、更新、删除)都可能受到磁盘 I/O 速度的限制。在高并发场景下,大量请求排队等待磁盘 I/O,会迅速成为系统性能的主要瓶颈。
-
随机访问更甚: 顺序读写磁盘尚可接受,但数据库操作(特别是根据非连续索引或条件查询)常常涉及随机 I/O,这对传统机械硬盘(HDD)尤其不友好,性能急剧下降。即使使用 SSD,其随机 I/O 性能也远低于内存。
-
-
全表扫描:索引失效的代价
-
核心机制: 索引是关系型数据库提升查询效率的关键。如果查询条件能够有效命中合适的索引,数据库可以快速定位到目标数据行。
-
性能陷阱: 一旦查询条件无法命中任何索引(例如对非索引列进行过滤、使用函数处理索引列、或使用了不合适的运算符),数据库就不得不进行 全表扫描(Full Table Scan)。
-
严重后果: 全表扫描意味着数据库需要逐行读取整个表的所有数据块。对于大表,这会产生海量的磁盘 I/O 操作(将表数据从磁盘读入内存缓冲区),消耗大量 CPU 资源进行比较运算,导致查询响应时间呈指数级增长。这是导致“慢查询”的最常见原因之一。
-
-
SQL 执行引擎的开销:解析、优化与执行的代价
-
处理流程: 一条 SQL 语句在数据库内部执行并非简单的“直达”,而是要经历一个相对复杂的流程:
-
语法解析(Parsing): 检查 SQL 语法是否正确。
-
语义分析(Semantic Analysis): 验证表名、列名是否存在,用户是否有权限等。
-
查询优化(Optimization): 这是最核心也最耗资源的环节之一。优化器需要分析表结构、索引、数据分布统计信息,估算不同执行路径(如选择哪个索引、使用哪种连接算法)的成本,并生成一个理论上最优的执行计划(Execution Plan)。这个过程本身就需要消耗可观的 CPU 计算资源。
-
执行(Execution): 根据生成的执行计划,调用存储引擎读取数据并进行计算(排序、分组、连接等)。
-
-
性能影响: 对于简单查询,这部分开销相对可控。但对于复杂查询或超高并发的短小查询(如简单的主键查询),SQL 解析和优化的时间成本可能接近甚至超过实际数据检索的时间,成为显著的性能负担。
-
-
复杂查询的沉重代价:连接、聚合与排序
-
操作类型: 关系型数据库的核心优势在于处理复杂的关系操作,如多表连接(JOIN)、分组聚合(GROUP BY)、排序(ORDER BY)、子查询、窗口函数等。
-
性能挑战:
-
连接操作: 多表连接(尤其是大表连接)可能需要在内存或临时磁盘空间中进行巨大的笛卡尔积计算(即使有索引,连接操作本身也可能很重),消耗大量 CPU 和内存资源。
-
聚合与排序: 对海量结果集进行分组、求和、求平均、排序等操作,同样需要巨大的内存和 CPU 计算量。当内存不足时,数据库会使用临时磁盘空间(TempDB),这又会引入额外的磁盘 I/O 开销。
-
资源消耗: 复杂查询通常需要占用大量的内存来存放中间结果,并长时间占用 CPU 进行计算。在高并发环境下,几个这样的复杂查询就可能耗尽数据库资源,拖慢整个系统。
-
-
-
事务与锁的并发控制开销
-
ACID 保障: 关系型数据库的核心特性之一是提供 ACID(原子性、一致性、隔离性、持久性) 事务保证。
-
性能权衡: 为了实现高隔离级别(如可重复读、串行化),数据库需要复杂的锁机制(Locking) 或 多版本并发控制(MVCC)。
-
性能影响:
-
锁竞争: 当多个事务试图同时访问或修改同一数据时,会发生锁竞争。等待锁释放会阻塞后续操作,增加延迟,严重时甚至导致死锁(需要数据库检测并解除)。
-
MVCC 开销: MVCC 通过维护数据版本来避免读阻塞写和写阻塞读,但这增加了存储历史版本数据的开销(占用更多空间),以及垃圾回收(清理旧版本) 的 CPU 和 I/O 成本。在更新频繁的场景下,MVCC 的开销可能很大。
-
-
-
网络与连接管理开销(对于远程访问)
-
应用场景: 当应用程序与数据库部署在不同的服务器上时,所有数据库操作都需要通过网络进行通信。
-
性能影响: 网络延迟(Latency) 和带宽限制(Bandwidth) 会成为额外的性能瓶颈。建立和销毁数据库连接(Connection)本身也有一定开销。频繁建立短连接或连接池配置不当都会显著影响性能。
-
2.2.使用Redis作为MySQL的缓存
在现代网站架构中,关系型数据库(如 MySQL、PostgreSQL) 通常是存储核心、结构化数据的基石,提供强大的事务支持、复杂查询能力和数据一致性保障。然而,其性能局限在高并发场景下尤为突出:执行一次查询操作往往消耗较多的系统资源(CPU、内存、I/O),响应时间相对较长。
⚡ 高并发下的数据库压力与风险
当网站访问量激增,大量请求同时涌向数据库时:
-
资源消耗剧增: 每个查询都需要数据库进行磁盘 I/O、SQL 解析优化、执行计算等操作,消耗 CPU、内存、磁盘和网络资源。
-
瓶颈效应显现: 数据库服务器的硬件资源(CPU核心数、内存容量、磁盘I/O吞吐、网络带宽)是有限的。高并发下,这些资源迅速被消耗殆尽。
-
宕机风险陡升: 资源耗尽(尤其是 CPU 100%、内存 OOM、磁盘 I/O 饱和或连接数爆满)将直接导致数据库响应时间飙升、拒绝新连接,甚至进程崩溃(OOM Killer触发或程序异常),引发服务不可用。
🛡 提升数据库承载能力的核心策略
为了应对高并发挑战,保护数据库稳定运行,核心思路围绕“开源”与“节流”展开,通常需要双管齐下:
-
开源:横向/纵向扩展数据库能力
-
核心思想: 通过增加硬件资源或部署更多数据库实例来分摊负载。
-
主要手段:
-
垂直扩展 (Scale Up): 升级单台数据库服务器的硬件(更强的 CPU、更大的内存、更快的 SSD)。优点:简单直接。缺点:成本高昂,有物理上限,单点故障风险。
-
水平扩展 (Scale Out): 部署数据库集群。
-
读写分离 (主从复制): 设置一个主库(Master)负责写操作,多个从库(Slave)复制主库数据并分担读操作。显著提升读吞吐量。
-
分库分表: 将庞大的数据库/表水平拆分成多个较小的、分布在不同的物理服务器上的库/表。根据特定规则(如用户ID范围、地域)路由请求。大幅提升整体读写能力和存储上限。技术复杂度高,需解决分布式事务、跨库查询等问题。
-
-
-
目标: 直接提升数据库集群整体的处理容量。
-
-
节流:引入缓存,减少数据库直接访问
-
核心思想: 在数据库前方设置一道高速缓冲层,拦截并处理大量重复的、对热点数据的读请求,避免它们“穿透”到后端的数据库,从而显著降低数据库的访问压力和资源消耗。
-
关键利器:Redis
-
定位: Redis 是实现数据库缓存方案的首选工具。
-
性能优势显著:
-
内存速度: Redis 核心数据存储在内存(RAM)中,访问速度(微秒级)比基于磁盘的关系型数据库(毫秒级)快几个数量级。
-
简单高效: Redis 主要提供简单的 Key-Value 数据结构操作,避免了关系型数据库复杂的 SQL 解析、优化器、执行引擎、连接计算(如 JOIN)等带来的巨大开销。处理一个简单的键查询请求,Redis 消耗的系统资源远低于 MySQL。
-
高并发支撑: 得益于内存操作和轻量级架构,Redis 能够轻松支撑极高的读写并发量。
-
-
作用比喻: Redis 犹如数据库前方的 “护盾” 或 “高速闸门”,将大量请求“拒之门外”(在缓存层解决),保护后端的 MySQL 免受洪峰冲击。
-
-
🔄 Redis缓存工作机制:命中与回填
典型的使用Redis作为数据库缓存的请求处理流程如下:
-
客户端发起请求: 用户通过客户端(如浏览器、App)向业务服务器发起数据查询请求。
-
业务服务器查询Redis: 业务服务器首先尝试从 Redis 缓存中根据请求的 Key 查找所需数据。
-
缓存命中 (Cache Hit): 如果数据在 Redis 中存在且有效(未过期),业务服务器直接从 Redis 获取数据并返回给客户端。此过程完全绕过数据库,响应最快。
-
缓存未命中 (Cache Miss): 如果数据在 Redis 中不存在或已过期,业务服务器则转向查询后端数据库 (如 MySQL)。
-
-
查询数据库并回填缓存:
-
业务服务器从 MySQL 中获取所需数据。
-
业务服务器将查询到的数据按约定格式(通常也是 Key-Value)写入 Redis 缓存,并设置一个合理的过期时间 (TTL)。这个过程称为缓存回填 (Cache Population)。
-
业务服务器将数据返回给客户端。
-
-
后续请求受益: 对于相同 Key 的后续请求,只要缓存数据未过期,都将直接从 Redis 中快速获取(命中),极大地减轻了数据库压力。
📊 “二八定律”与缓存的价值
缓存机制有效性的核心理论基础正是“二八定律”(帕累托法则):在大多数系统中,大约 20% 的热点数据承载了 80% 的访问请求。
-
策略精髓: Redis 缓存无需存储所有数据。它聚焦于识别并存储那关键的 20% 的热点数据 (Hot Data)。
-
效果显著: 通过成功缓存这部分少量但访问极其频繁的数据,就能有效拦截并处理掉 80% 甚至更高比例的查询请求,使其无需穿透到数据库层。
-
实践考量: 实际业务中,“二八”比例可能因场景不同有所浮动(可能是“一九”、“三七”等),但“少数数据支撑多数访问”的核心规律普遍成立。精心设计和维护的缓存系统,通常只需配置相对总数据量很小一部分的内存(如 1%-5%),就能将整体缓存命中率提升至 80%-95% 以上。
-
核心收益:
-
大幅提升用户体验: 响应速度更快(毫秒级 vs 几十甚至几百毫秒)。
-
显著降低数据库负载: 保护数据库免受流量洪峰冲击,提升其稳定性。
-
增强系统整体吞吐量: 用更少的数据库资源处理更多用户请求。
-
优化成本效益: 利用相对经济的缓存资源(内存)有效缓解了扩展数据库(尤其是分库分表)的复杂度和成本。
-
三. 缓存更新策略:识别热点数据
在实施缓存策略时,一个核心挑战是:如何准确识别哪些数据属于需要重点缓存的“热点数据”?
3.1.定期更新
一种常见策略是定期生成热点数据集。系统会设定一个固定的时间周期(例如一天、一周或一个月),对该周期内所有被访问数据的频次进行统计和分析。最终,筛选出访问频率最高的前 N% 的数据,认定它们为当前阶段的热点数据。
🥇 搜索引擎案例:
以搜索引擎为例,用户每次输入的搜索词(查询词)都会被服务器详细记录在日志中,包含用户信息、时间戳和查询词本身。这些日志数据量通常极其庞大。
-
高频词: 某些查询词被大量用户频繁搜索(例如,日常热门商品、本地服务、流行话题等)。
-
低频词: 而另一些查询词则相对冷门,搜索次数很少。
-
统计过程: 搜索引擎会定期(如每天或每周)利用大数据处理框架(如 Hadoop 或 Spark)对这些海量日志进行离线批量分析,计算每个查询词出现的总次数或频率。通过这种统计分析,就能生成一份反映该统计周期内用户搜索热度的“高频词表”。
优点:
-
实现相对简单: 利用成熟的离线批处理技术即可完成。
-
结果稳定: 统计周期较长时,结果能反映稳定的、长期的热点趋势。
缺点与挑战:
-
实时性差: 这是该方法最主要的短板。统计结果是基于过去一个周期的历史数据,无法即时反映当前发生的变化。新产生的热点或突发热点无法被及时识别和缓存。
-
应对突发流量不足: 对于由突发事件、节日活动或营销推广等引起的瞬时流量高峰,这种延迟的统计方式往往反应滞后。
-
示例: 在春节临近和期间,“春晚”、“春运”、“春节祝福语”等词汇的搜索量会急剧飙升,成为绝对的热点。然而,在平时,这些词的搜索频率则相对较低。基于上周或上月数据生成的“高频词表”很可能完全未包含这些词,导致缓存系统在春节流量洪峰来临时未能有效缓存相关结果,加重后端数据库压力,影响用户体验。
-
-
资源消耗: 处理海量日志进行全量统计本身需要消耗可观的计算和存储资源。
-
数据新鲜度: 统计周期结束时,数据已经“过时”了一段时间。
3.2.实时生成
与定期生成不同,实时生成策略的核心思想是:让缓存系统在运行过程中,根据用户的实际访问行为,动态地识别和保留热点数据。 这种策略通常结合缓存淘汰机制来实现。
实现原理:
-
设定缓存容量: 首先,为缓存设定一个明确的容量上限。例如,在 Redis 中,可以通过配置文件中的
maxmemory
参数来设置最大内存使用量。 -
处理用户查询:
-
当用户发起查询请求时,系统首先在 Redis 缓存中查找。
-
缓存命中: 如果在 Redis 中找到数据,则直接返回结果给用户,效率最高。
-
缓存未命中: 如果在 Redis 中未找到数据,则系统需要访问后端数据库(如 MySQL)获取结果。
-
将数据库查询结果返回给用户。
-
同时,将这份查询结果写入 Redis 缓存,以便后续相同的请求能直接从缓存中获取,加速响应。
-
-
-
动态淘汰: 当持续写入数据导致缓存达到预设的容量上限时,Redis 会自动触发其缓存淘汰策略。这时,系统会根据设定的策略算法,选择淘汰掉一部分“相对不那么热门”的数据,腾出空间来容纳新写入的数据。
-
热点数据自然沉淀: 按照上述过程持续运行一段时间后,那些被频繁访问的数据因为不断地被访问(命中)而得以保留在缓存中。相反,那些访问频率低的数据,在新数据写入或根据淘汰策略计算后被淘汰的概率更大。最终,缓存中留存下来的数据,自然就趋向于当前最热门的“热点数据”。这是一个动态、自适应的过程。
常见的通用缓存淘汰策略:
以下策略不仅适用于 Redis,也是许多缓存系统支持的核心淘汰机制:
-
FIFO (First In First Out - 先进先出):
-
原理: 淘汰最早进入缓存的数据,类似于队列。它认为“先来的数据”被再次访问的可能性较低。
-
优点: 实现简单。
-
缺点: 可能淘汰掉仍然热门但只是较早进入的旧数据,无法有效反映数据的实际热度。对突发的新热点不友好。
-
-
LRU (Least Recently Used - 最近最少使用):
-
原理: 系统记录每个缓存项(Key)的最后一次被访问的时间戳。当需要淘汰时,选择最久未被访问的那个数据项淘汰。它认为“最近没被用过的数据”将来被用的可能性也低。
-
优点: 相比 FIFO,能更好地反映数据的近期访问热度,保护了最近被访问过的数据。
-
缺点: 对突发性的、周期性的或“扫描式”访问模式可能不够友好(例如,一个很久没访问但突然被大量访问的数据会被保留,而一个周期性访问但恰好在淘汰检查前很久没访问的数据会被淘汰)。需要维护访问时间戳,有一定开销。容易受到偶发大量访问的影响(可能把真正的老热点挤出)。
-
-
LFU (Least Frequently Used - 最不经常使用):
-
原理: 系统记录每个缓存项(Key)在一段时间内的访问频率(次数)。当需要淘汰时,选择访问频率最低的那个数据项淘汰。它认为“总访问次数少的数据”是冷数据。
-
优点: 从长期累积的角度识别冷数据,能更稳定地保护真正的热点(高频访问数据)。
-
缺点: 新加入的数据初始频率为0或很低,容易被立即淘汰,即使它可能很快会变成热点(“新生劣势”)。需要维护和更新访问计数器,开销通常比 LRU 更大。对突发的新热点响应较慢。旧热点如果访问频率下降,可能长期占据缓存(需要结合老化机制)。Redis 实现的 LFU 是近似计数。
-
-
Random (随机淘汰):
-
原理: 当缓存满时,随机选择一个数据项进行淘汰。
-
优点: 实现极其简单,开销最小。
-
缺点: 完全无法保证淘汰的是冷数据,可能误淘汰热点数据,导致缓存命中率不稳定且通常较低。实际生产环境中较少单独使用。
-
🏕 理解淘汰策略(比喻延续):
想象缓存如同皇帝有限的“精力”(缓存空间),需要专注于处理最重要的国事(热点数据)。数据库则如同庞大的“后宫”(全量数据),记录了所有妃嫔(数据项)。
-
新数据入缓存: 如同皇帝看中(查询)了一位新入宫的秀女(新数据),决定召见(写入缓存)。
-
缓存已满(精力有限): 要召见新人,就必须有旧人暂时移出视线(淘汰出缓存)。选择移出谁(淘汰谁)就是淘汰策略解决的问题:
-
FIFO (先进先出): 最早入宫(最早写入缓存)的皇后(数据A)被移出。不管她是否依然重要。
-
LRU (最近最少使用): 查看妃嫔们最近一次被召见(访问)的时间。假设:
-
皇后:1周前
-
熹妃:昨天
-
安答应:2周前
-
华妃:1个月前
则 华妃(最久未被召见) 被移出。
-
-
LFU (最不经常使用): 统计妃嫔们最近一个月被召见(访问)的次数。假设:
-
皇后:3次
-
熹妃:15次
-
安答应:1次
-
华妃:10次
则 安答应(访问次数最少) 被移出。
-
-
Random (随机淘汰): 闭着眼睛随便指一位妃嫔(如纯元皇后画像...)移出。
-
Redis 的淘汰策略
除了理解通用的缓存淘汰原理,Redis 自身提供了丰富的、可直接配置使用的内存淘汰策略。这些策略在内存达到 maxmemory
限制时自动触发,用于为新数据腾出空间。
Redis 的淘汰策略名称通常包含两部分:
-
作用范围 (
volatile
vsallkeys
):-
volatile-
:策略仅作用于设置了过期时间 (expire TTL) 的 key。 -
allkeys-
:策略作用于所有 key,无论是否设置了过期时间。
-
-
淘汰算法 (
lru
,lfu
,random
,ttl
): 决定在指定范围内依据什么规则选择淘汰哪些 key。
以下是 Redis 支持的主要淘汰策略:
-
volatile-lru
(Least Recently Used on volatile keys):-
当内存不足时,仅从设置了过期时间的 key 中,淘汰 最近最久未使用 (LRU) 的 key。
-
适用场景: 明确区分缓存数据(设TTL)和持久数据(不设TTL)的场景,只希望淘汰缓存数据。
-
-
allkeys-lru
(Least Recently Used on all keys):-
当内存不足时,从所有 key 中(无论是否过期),淘汰 最近最久未使用 (LRU) 的 key。
-
适用场景: 最常见的选择之一。适用于希望将所有数据都视为潜在缓存,并优先淘汰最不活跃数据的场景。对整体缓存命中率提升效果较好。
-
-
volatile-lfu
(Least Frequently Used on volatile keys) [Redis 4.0+]:-
当内存不足时,仅从设置了过期时间的 key 中,淘汰 访问频率最低 (LFU) 的 key。
-
适用场景: 需要更精准识别长期冷门缓存数据(设TTL)的场景,LFU 比 LRU 更能保护长期热点。
-
-
allkeys-lfu
(Least Frequently Used on all keys) [Redis 4.0+]:-
当内存不足时,从所有 key 中(无论是否过期),淘汰 访问频率最低 (LFU) 的 key。
-
适用场景: 另一个强推荐策略。适用于希望基于长期访问频率来保留最热数据,对所有数据一视同仁的场景。能更好地保护持久的热点数据。
-
-
volatile-random
(Random on volatile keys):-
当内存不足时,仅从设置了过期时间的 key 中,随机淘汰一个 key。
-
适用场景: 对淘汰数据要求不高,且主要淘汰缓存数据的简单场景。效率高但命中率不稳定。
-
-
allkeys-random
(Random on all keys):-
当内存不足时,从所有 key 中(无论是否过期),随机淘汰一个 key。
-
适用场景: 极少使用。对数据重要性无区分,或仅作为测试/基准比较。生产环境慎用,可能误淘汰关键数据。
-
-
volatile-ttl
(Time To Live on volatile keys):-
当内存不足时,仅从设置了过期时间的 key 中,淘汰 剩余生存时间 (TTL) 最短的 key(即最快过期的)。
-
适用场景: 可以理解为在设置了TTL的key中实现了 FIFO(先进先出) 的近似效果(早过期的先淘汰)。适用于希望优先淘汰即将自然过期的缓存数据,减少主动淘汰开销的场景。有助于缓解缓存雪崩风险(避免大量key同时过期)。
-
-
noeviction
(No Eviction - 默认策略):-
当内存不足时,新写入的操作(会占用更多内存的命令,如 SET, LPUSH 等)将直接返回错误(通常是
(error) OOM command not allowed when used memory > 'maxmemory'
)。读取命令(如 GET)通常不受影响。 -
适用场景: 不推荐用于缓存场景! 仅适用于Redis存储的数据绝对不允许丢失,且应用层能妥善处理写入失败的情况(例如作为唯一的主存储)。在需要缓存的系统中使用此策略会导致写入失败,严重影响服务可用性。
-
四.缓存的使用注意事项
4.1.缓存预热(Cache preheating)
什么是缓存预热?
在使用 Redis 作为 MySQL 的前置缓存时,会遇到一个典型问题:缓存空窗期压力。这通常发生在以下两种场景:
-
Redis 服务刚启动:此时 Redis 缓存是完全空的。
-
Redis 中大批量 Key 同时失效:例如,大量缓存设置了相同的过期时间,导致在某个时刻集中失效。
问题后果:
在这段空窗期内,用户的查询请求会直接穿透 Redis(因为找不到数据),全部落到后端的 MySQL 数据库上。由于 MySQL 处理请求的能力通常远低于缓存,瞬间激增的请求会对其造成巨大的访问压力,可能导致数据库响应变慢甚至崩溃,严重影响服务的可用性和用户体验。
缓存预热的定义与目的:
缓存预热(Cache Warming)就是为了解决上述“空窗期”问题而采取的一种主动策略。 其核心思想是:在缓存服务(如 Redis)正式投入使用或预期可能面临大批缓存失效之前,提前将预估的“热点数据”加载到缓存中。
如何实现缓存预热?
-
识别热点数据: 利用之前介绍的方法(如定期统计分析历史访问日志)离线计算出一批最可能被频繁访问的数据。这份热点数据列表不要求绝对精确,其目标是覆盖大部分高概率请求即可。
-
主动加载数据:
-
在 Redis 启动后、正式接入流量之前。
-
或者在预期大批缓存失效(如缓存刷新、大促前)之前。
-
通过脚本、后台任务或专门的数据加载工具,将识别出的热点数据及其查询结果批量写入 Redis。
-
缓存预热的意义与效果:
-
立即提供保护: 预热完成后,当用户请求到达时,Redis 缓存中已经存在了大量热点数据,能够直接响应这些请求,显著减少甚至避免了穿透到 MySQL 的请求数量,为数据库撑起了“保护伞”。
-
平滑启动与过渡: 避免了服务启动或缓存刷新瞬间对数据库造成的巨大冲击,保障了系统启动和运行的稳定性。
-
结合动态调整: 预热加载的初始热点数据是基于历史统计的预测。随着系统正式运行,用户的实时访问行为会驱动 Redis 的淘汰机制(如 LRU, LFU)持续工作。访问真正频繁的新热点数据会逐渐被写入缓存并保留下来,而访问较少的预热数据则会根据策略被逐步淘汰。这样,缓存的内容会动态演化,越来越贴合当前实际的访问模式。
4.2.关于缓存穿透 (Cache penetration)
什么是缓存穿透?
缓存穿透是指用户查询一个在缓存(如 Redis)和底层数据库(如 MySQL)中都不存在的数据。由于缓存中找不到该数据(称为缓存未命中),系统会转而查询数据库。数据库同样返回“不存在”的结果。关键问题在于:这个无效的查询结果通常不会被存储到缓存中。
为什么缓存穿透是个问题?
-
数据库压力骤增: 由于这个无效的查询结果没有被缓存起来,后续对该同一个无效 Key 的重复查询,依然会穿透缓存,直接打到数据库上。
-
放大效应: 如果存在大量不同的无效 Key(例如,大量随机生成的、不存在的 ID),并且这些 Key 被频繁查询(可能是恶意攻击或业务逻辑缺陷导致),数据库将承受巨大的、本可避免的查询压力,严重时可能导致数据库性能下降甚至崩溃。
-
缓存失效: 缓存层在这种场景下完全失去了保护数据库的作用。
缓存穿透是如何产生的?
主要原因包括:
-
业务设计不合理:
-
缺少必要的参数校验环节。例如,查询用户信息时,未对输入的 ID 进行格式校验(如长度、字符类型),导致非法或无效的 ID 被直接提交到后端进行查询。
-
-
开发/运维误操作:
-
开发或运维人员不小心误删了数据库中的部分有效数据,导致原本存在的 Key 变成了“不存在”。(这种情况虽非典型穿透的起因,但结果表现相同)。
-
-
恶意攻击:
-
黑客恶意攻击是缓存穿透最常见且危害最大的来源。攻击者会故意构造并大量请求根本不存在的 Key(如随机生成的用户ID、商品ID等),旨在耗尽数据库资源,使服务不可用。
-
补充说明: 面对网络上海量的恶意扫描和攻击尝试,单纯依赖预先设定的规则(如参数校验规则)不一定能及时发现或完全拦截所有恶意请求。
-
如何解决缓存穿透?
解决思路主要围绕:识别并拦截无效请求 和 减轻无效请求对数据库的冲击。
-
加强参数校验(业务层防御):
-
最根本的解决方案。 在请求到达缓存和数据库查询逻辑之前,在业务层对查询参数进行严格的合法性校验。例如:
-
查询用户手机号:校验是否符合手机号格式(长度、数字组成、有效号段)。
-
查询商品ID:校验是否为有效格式(如纯数字、特定长度范围)。
-
查询订单号:校验结构是否符合规则。
-
-
尽早过滤掉明显非法的请求,避免它们穿透到后端存储。
-
-
缓存空对象(Null Object Caching / Cache Empty Results):
-
核心思想: 即使数据库查询结果为“不存在”,也将这个“空结果”写入缓存。
-
实现: 当系统查询某个 Key,在数据库确认不存在后,在 Redis 中设置该 Key 对应的 Value 为一个特殊的、表示“空”或“不存在”的值(例如空字符串
""
、特殊标识字符串"NULL"
、或一个具有特定含义的对象)。 -
优点: 后续相同的无效 Key 查询会直接从缓存中获取到这个“空值”,避免了重复穿透到数据库。
-
关键点:
-
需要为这些“空值”Key 设置一个合理的过期时间 (TTL)。防止存储大量永不失效的无效 Key 占用过多缓存空间,也应对未来该 Key 可能变为有效的情况(比如新用户注册了这个手机号)。
-
选择何种“空值”形式需结合业务逻辑,确保应用层能正确识别和处理。
-
-
-
使用布隆过滤器 (Bloom Filter):
-
核心思想: 在查询缓存和数据库之前,增加一个快速预检层,用于高概率地判断一个 Key 是否可能存在于数据库中。如果布隆过滤器判断 Key 肯定不存在,则直接返回空结果,无需查询缓存或数据库。
-
原理简述: 布隆过滤器是一个基于哈希 (Hash) 和位数组 (Bitmap) 的概率型数据结构。
-
将所有可能存在于数据库的有效 Key 预先加载(“添加”)到布隆过滤器中。
-
当需要查询一个 Key 时,布隆过滤器通过多个哈希函数计算该 Key 在位数组中的对应位置。
-
如果这些位置有任何一位是 0,则肯定可以断定该 Key 不存在于数据库中。
-
如果这些位置全都是 1,则该 Key 可能存在(存在一定的误判率,即布隆过滤器说“可能存在”时,实际可能不存在)。
-
-
优点:
-
空间效率极高: 相对于存储所有 Key 本身,布隆过滤器仅使用一个位数组,占用内存极小。
-
查询速度极快: 计算几个哈希并检查位数组,时间复杂度是常数级 O(k)。
-
-
缺点:
-
概率性: 存在误判率 (False Positive)。即可能将数据库中不存在的 Key 误判为“可能存在”(但不会将存在的 Key 误判为不存在)。这会导致少量无效请求仍然可能穿透到缓存/数据库。
-
不支持删除: 标准的布隆过滤器不支持直接删除 Key(删除会影响其他 Key 的判断)。需要删除的场景可考虑变种如计数布隆过滤器(Counting Bloom Filter),但空间开销增大。
-
-
适用场景: 非常适合解决恶意攻击产生的大量、随机的无效 Key 查询问题。它能拦截掉绝大部分明显无效的请求。
-
补充说明: 布隆过滤器通常需要独立维护(如使用 Redis Module 提供的布隆过滤器功能,或使用独立的 Bloom Filter 库/服务),并需要机制将数据库的有效 Key 同步到过滤器中(如启动时全量加载,增量更新)。
-
4.3..关于缓存雪崩(Cache avalanche)
缓存雪崩是指在极短的时间内,缓存系统(如 Redis)中有大量的缓存 Key 集中过期失效,或者缓存服务本身发生大规模故障(如整个 Redis 集群宕机)。
为什么缓存雪崩是个严重问题?
-
缓存保护失效: 缓存的核心作用是作为数据库(如 MySQL)的“护盾”,吸收并处理大部分查询请求,减轻数据库压力。当海量 Key 瞬间失效或缓存服务不可用时,这面“护盾”就瞬间崩塌了。
-
数据库压力海啸: 所有原本可以由缓存响应的请求,此时都直接涌向底层数据库。数据库瞬时需要处理远超其设计负载的并发查询。
-
级联崩溃风险: 数据库很可能因无法承受这种瞬时洪峰压力而导致响应变慢、连接耗尽,甚至完全崩溃宕机。
-
服务不可用: 数据库的崩溃或过载会直接导致依赖它的应用服务对外不可用或响应极慢,形成大规模的服务故障。
-
恢复困难: 即使数据库重启,如果缓存仍未恢复或大量 Key 仍处于失效状态,重启后的数据库可能立即再次被蜂拥而至的请求压垮,形成恶性循环。
缓存雪崩是如何产生的?
主要原因可以分为两大类:
-
缓存服务大规模故障:
-
Redis 单点/主节点宕机: 单节点部署下,该节点宕机即导致整个缓存服务不可用。
-
Redis 集群故障: 在集群模式下,如果发生大规模节点宕机(如网络分区、硬件故障、软件Bug导致批量崩溃),或者集群无法完成故障转移(Failover),导致大部分或整个集群服务不可用。
-
网络问题: 连接缓存服务的网络出现严重中断或拥塞。
-
-
大量 Key 集中同时过期:
-
批量初始化/预热: 常见于系统启动、数据迁移或缓存预热时,一次性向缓存中加载了大量数据,并且为这些 Key 设置了相同或非常接近的过期时间 (TTL)。例如,在凌晨进行数据预热,所有 Key 都设置了 24 小时过期,那么第二天凌晨这些 Key 就会同时失效。
-
定时任务刷新: 某些定时任务定期刷新缓存数据,如果刷新逻辑是删除旧 Key 并重新加载且设置相同 TTL,也可能导致下一周期 Key 集中过期。
-
业务特性: 特定业务场景下自然产生了大量具有相同生命周期的数据(虽然相对少见)。
-
如何解决和预防缓存雪崩?
解决思路的核心是:避免大规模失效集中发生 和 增强系统对失效的韧性。
-
构建高可用的缓存架构:
-
集群化部署: 使用 Redis Cluster、Codis 或基于 Sentinel 的主从复制等高可用方案,确保单个或多个节点故障时,服务能自动切换并持续可用。
-
异地多活/容灾: 对于关键业务,考虑部署跨机房的缓存集群,提升整体容灾能力。
-
监控与报警: 至关重要! 建立完善的监控体系,实时监控 Redis 节点状态、集群健康度、内存使用率、连接数、QPS、缓存命中率等关键指标。设置严格的报警阈值(如节点宕机、命中率骤降),确保问题能第一时间被发现和处理。
-
-
优化 Key 过期策略:
-
差异化过期时间: 避免为大量 Key 设置完全相同的过期时间。在设置 TTL 时,引入随机因子。例如,基础过期时间为 24 小时,实际设置的 TTL = 24小时 + 随机(0 到 30分钟)。这样可以将 Key 的失效时间点打散,避免瞬间雪崩。
-
热点数据永不过期 + 异步更新: 对于访问极其频繁的核心热点数据,可以考虑不设置过期时间。通过独立的异步逻辑(如后台任务、监听数据库变更)来更新缓存,确保数据相对新鲜的同时避免被动失效。需要配合内存淘汰策略(如 LRU)管理内存。
-
-
采用多级缓存策略:
-
在应用层(如 JVM 内)使用本地缓存(如 Caffeine、Guava Cache),在分布式缓存(Redis)之上再增加一层保护。即使 Redis 崩溃或大量 Key 失效,部分请求仍可由本地缓存响应。需要注意本地缓存的一致性和容量管理,通常设置较短的 TTL。
-
-
服务降级与熔断:
-
熔断机制: 当检测到数据库压力过大或错误率飙升时,自动触发熔断,短时间内直接拒绝部分或全部请求(返回预设的兜底值、错误提示或排队中状态),保护数据库不被压垮。
-
服务降级: 在极端情况下,暂时关闭非核心功能或返回简化数据,优先保障核心流程和数据库的可用性。
-
-
缓存预热与延时加载:
-
预热: 在预期的高峰期来临前(或系统启动后),提前加载热点数据到缓存中,并设置合理的、带有随机因子的 TTL。
-
延时加载: 对于缓存失效后的重建,可以使用互斥锁(如 Redis 的
SETNX
)或分布式锁,确保只有一个线程去数据库查询并重建缓存,其他线程等待或短暂返回旧值/默认值,避免大量线程同时击穿缓存访问数据库。
-
4.4.关于缓存击穿(Cache breakdown)
什么是缓存击穿?
缓存击穿(也称为热点 Key 失效)是缓存雪崩的一个特例,但其影响往往更为集中和剧烈。它特指在缓存系统中,一个或多个访问极其频繁的热点 Key 在某个瞬间同时过期失效。
为什么缓存击穿危害巨大?
-
单点失效,压力集中: 与雪崩的大面积失效不同,击穿通常只影响少数甚至单个 Key。但正因为这些 Key 是热点(被海量并发请求访问),其失效的影响被急剧放大。
-
瞬间洪峰冲击: 当热点 Key 失效的瞬间,所有原本依赖缓存响应的、针对该 Key 的海量并发请求,会像洪水一样瞬间涌向底层数据库(如 MySQL),试图查询并重建缓存。
-
数据库不堪重负: 数据库可能完全无法承受这种针对单点数据的超高并发查询压力,导致响应延迟飙升、连接资源耗尽,甚至引发数据库崩溃宕机。
-
连锁反应: 数据库的崩溃或过载会进一步导致依赖该数据的应用服务大面积不可用或响应超时,影响范围迅速扩大。
-
恢复困难: 即使数据库重启或缓存重建完成,如果热点访问模式未改变,重建过程本身或后续的首次访问高峰仍可能再次压垮数据库。
缓存击穿是如何产生的?
核心原因:
-
热点 Key 集中过期: 一个或多个访问量巨大的 Key 被设置了相同的过期时间,并且在同一时刻失效。例如:
-
一个首页置顶的爆款商品信息 Key。
-
一个重要的系统配置项 Key。
-
一个顶流明星/主播的个人信息 Key。
-
-
缺乏失效保护机制: 在热点 Key 失效时,没有有效的机制来平滑处理海量并发重建请求。
如何解决和预防缓存击穿?
解决思路的核心是:防止热点 Key 被动失效引发瞬时洪峰 和 控制缓存重建时的并发访问。
-
热点 Key 永不过期 + 异步更新:
-
核心策略: 对于识别出的热点 Key,在缓存中不设置过期时间 (TTL),使其理论上“永不过期”。
-
保证数据新鲜度: 配合使用异步更新机制:
-
后台定时任务: 启动一个独立的任务,定期(如每分钟)检查数据库中的数据是否有更新,如有则主动刷新缓存。
-
监听数据库变更: 利用数据库的 Binlog 或变更数据捕获 (CDC) 技术,当检测到热点 Key 对应的源数据发生变化时,立即触发缓存更新。
-
-
优点: 完全避免了被动过期导致的瞬时击穿风险。
-
关键点: 需要有效的热点 Key 发现机制(如通过监控访问频率、QPS 统计、业务经验识别)和可靠的后台更新逻辑。同时需配合内存淘汰策略管理内存。
-
-
互斥锁 (Mutex Lock) 控制并发重建:
-
核心思想: 当热点 Key 失效时,只允许一个请求线程去数据库查询数据并重建缓存,其他并发请求等待或短暂返回旧值/默认值,重建完成后再恢复正常访问。
-
实现方式: 使用分布式锁(如基于 Redis 的
SETNX
+EXPIRE
,或 RedLock 算法,或 ZooKeeper)来实现互斥访问。 -
流程简述:
-
请求 A 发现 Key 在缓存中失效。
-
请求 A 尝试获取该 Key 对应的分布式锁。
-
如果获取锁成功,请求 A 负责查询数据库、重建缓存、设置新值(可能带新的TTL)、然后释放锁。
-
在请求 A 持有锁并重建缓存期间:
-
其他并发请求 (B, C, D...) 发现 Key 失效且获取锁失败。
-
这些请求可以选择:
-
等待重试: 短暂休眠后重试获取缓存(可能已由 A 重建好)。
-
返回兜底值: 直接返回一个业务上可接受的默认值或空值(服务降级的一种形式)。
-
-
-
请求 A 释放锁后,后续请求可以正常访问新缓存。
-
-
优点: 有效防止了海量线程同时冲击数据库。
-
关键点:
-
锁操作本身需要高效、可靠,避免成为瓶颈或单点。
-
锁需要设置合理的超时时间,防止持有锁的线程意外挂死导致死锁。
-
等待的线程需要合理的等待/重试策略,避免无限等待或过度消耗资源。
-
兜底值的设计需要符合业务逻辑。
-
-
-
逻辑过期时间 (Logical Expiration):
-
核心思想: 在缓存的 Value 中,额外存储一个逻辑过期时间戳,而 Key 本身的物理 TTL 设置得足够长(甚至永不过期)。
-
流程简述:
-
应用从缓存中读取 Value。
-
检查 Value 中的逻辑过期时间戳:
-
如果逻辑上未过期,直接使用缓存数据。
-
如果逻辑上已过期:
-
尝试获取分布式锁(类似方案2)。
-
获取锁成功的线程负责异步更新数据(查询数据库、更新缓存 Value 和逻辑过期时间戳)。
-
其他线程在锁被占用期间,继续返回“过期”但可用的旧数据(牺牲一定的实时性,保证可用性),直到新数据更新完成。
-
-
-
-
优点: 结合了永不过期(无被动失效洪峰)和可更新(保证数据相对新鲜)的优点。在逻辑过期期间,用户看到的是稍旧数据,但服务是正常的。
-
关键点: 增加了 Value 结构的复杂性,需要应用层处理逻辑过期判断。
-
-
服务降级与熔断:
-
作为最后防线或配合上述策略使用:
-
熔断: 当检测到针对特定 Key 或数据库的访问异常激增或错误率飙升时,触发熔断机制,暂时拒绝部分针对该 Key 的请求,保护数据库。
-
服务降级: 在极端高压或缓存/数据库故障时,可以暂时关闭非核心功能,或者对于缓存失效的请求,直接返回简化数据、默认值或排队提示(“省电模式”),优先保障核心服务的可用性和数据库的生存能力。
-
-