结合你提供的详细资料,我会从"核心概念→配置细节→操作逻辑→优缺点对比"四个维度,把Redis持久化(RDB+AOF)拆解得更清晰,同时结合你提到的关键配置和实际场景,帮你彻底理解如何用、怎么选。
先明确一个前提:为什么需要持久化?
Redis是内存数据库 ------数据默认只存在内存里。如果服务器断电、进程崩溃,内存里的数据会直接消失。持久化的本质就是:把内存里的数据/操作,"备份"到硬盘上,让重启后能恢复数据。
10.1 RDB方式:给数据拍"定时快照"
RDB的核心是"记录某一时刻的所有数据结果 ",就像给当前内存里的所有key-value拍一张"全家福",存成.rdb
文件到硬盘。
一、RDB的关键配置(你资料里提到的重点)
这些配置决定了RDB文件怎么存、存多久、是否安全,直接影响生产环境使用:
|-----------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------|
| 配置项 | 作用 | 通俗理解 & 生产建议 |
| dbfilename dump.rdb
| 定义RDB快照文件名 | 建议改成dump-端口号.rdb
(比如dump-6379.rdb
),避免多Redis实例用同一个文件冲突 |
| dir
| 定义RDB文件的存储路径 | 选一个存储空间大的目录(比如/data/redis
),别存在系统盘,防止磁盘满了影响服务器 |
| rdbcompression yes
| 是否用LZF算法压缩RDB文件 | - 开(yes):文件小、省磁盘,但会消耗一点CPU; - 关(no):省CPU,但文件会"巨大"(比如10G数据可能变20G),生产默认开 |
| rdbchecksum yes
| 是否用CRC64算法校验RDB文件 | - 开(yes):能发现文件损坏(比如磁盘出错),但读写时多10%时间; - 关(no):快一点,但数据坏了可能不知道,生产默认开 |
| save second changes
| 自动触发RDB的条件(核心) | 比如save 3600 1
=3600秒(1小时)内有1次key修改,就拍快照; 注意 :别设成"包含关系"(比如同时设save 60 100
和save 30 50
),会重复触发;频率要平衡------太频繁占CPU,太稀疏丢数据多 |
二、RDB的触发方式:手动/自动
1. 手动触发(两种命令,选谁?)
|----------|-------------------|-------------------------------------------------------|----------------------------|
| 命令 | 作用 | 优缺点 | 生产建议 |
| save
| 直接在主线程执行RDB,直到完成 | 优点:简单; 缺点:阻塞Redis(期间不能处理任何请求,数据多的话可能卡几秒/几分钟) | 绝对不用!线上用会导致服务不可用 |
| bgsave
| 后台异步执行RDB(主线程不阻塞) | 优点:主线程能正常处理请求; 缺点:需要"fork子进程"(复制一份内存数据),数据量大时fork会耗资源 | 生产手动触发全用它(比如备份时执行bgsave
) |
关键:fork子进程是什么?
你资料里提到的fork
是RDB的核心机制------Redis会复制一个和自己一模一样的子进程,让子进程去写RDB文件,主线程继续干活。
这里有个"坑":fork时会"克隆"内存数据,虽然用了"写时拷贝"(只有修改过的数据才复制),但如果内存里有10G数据,fork瞬间还是会占一定资源,可能短暂影响性能。
2. 自动触发
就是靠上面的save second changes
配置,比如默认配置里的:
save 900 1 # 900秒内改1次key,触发bgsave
save 300 10 # 300秒内改10次key,触发bgsave
save 60 10000 # 60秒内改10000次key,触发bgsave
只要满足任意一个条件,Redis就会自动执行bgsave
拍快照。
三、RDB的优缺点(结合你资料总结)
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| 优点 | 缺点 |
| 1. 文件小(二进制压缩),备份方便(比如每天凌晨用RDB做全量备份); 2. 恢复快(直接把整个文件读进内存,不用执行命令); 3. 存储效率高 | 1. 丢数据风险高 :如果两次快照之间宕机(比如设了1小时拍一次,59分时宕机),这59分钟的数据全丢; 2. 数据量大时,fork子进程和写文件会耗CPU/内存,影响服务性能; 3. 不同Redis版本的RDB文件可能不兼容(比如6.x的RDB不能直接给5.x用) |
10.2 AOF方式:记"操作日志",实时性更强
AOF的核心是"记录每一次写操作的过程 "------不是存数据结果,而是存"你做了什么修改"(比如set key1 123
、lpush list1 a
),重启时再重新执行这些命令,恢复数据。
它解决了RDB"丢数据多"的问题,现在是Redis持久化的主流。
一、AOF的执行流程(四步走)
- 写缓冲 :客户端的写命令(如
set
)先存到"AOF缓冲区"(内存里,快); - 同步到磁盘 :根据配置的"同步策略",把缓冲区的命令写到硬盘的
.aof
文件; - 文件重写 :AOF文件会越写越大(比如反复
set key1 x
),Redis会"压缩"文件(只留最终有效命令); - 重启恢复 :Redis重启时,读
.aof
文件,重新执行所有命令,恢复数据。
二、AOF的关键配置(你资料里的重点)
1. 基础开关&文件名
|---------------------------------|---------------|----------------------------------------------------------|
| 配置项 | 作用 | 生产建议 |
| appendonly yes
| 是否开启AOF(默认no) | 必须开!除非你能接受丢数据 |
| appendfilename appendonly.aof
| AOF文件名 | 建议改成appendonly-端口号.aof
(比如appendonly-6379.aof
),和RDB区分 |
| dir
| AOF文件存储路径 | 和RDB用同一个路径(比如/data/redis
),方便管理 |
2. 核心:AOF同步策略(appendfsync
)------决定"丢多少数据"
这是AOF最关键的配置,平衡"数据安全"和"性能":
|------------|---------------------------|--------------|------------------------|-----------------------|
| 策略 | 逻辑 | 数据安全性 | 性能 | 生产建议 |
| always
| 每执行一次写命令,就立即把命令同步到硬盘 | 最高(零丢失) | 最差(每次写都要等磁盘IO,QPS会降很多) | 除非是金融级场景(比如交易数据),否则不用 |
| everysec
| 每秒同步一次缓冲区的命令到硬盘 | 较高(最多丢1秒数据) | 较好(每秒只等一次IO,对性能影响小) | 99%的场景选这个,兼顾安全和性能 |
| no
| 让操作系统自己决定什么时候同步(通常是30秒左右) | 最低(可能丢几十秒数据) | 最好(Redis不管IO,全交给系统) | 不建议,数据丢太多,风险高 |
3. 解决AOF文件过大:重写配置
你资料里提到AOF会"越写越大"(比如反复set key1 1
→set key1 2
→set key1 3
,AOF会存3条命令),所以需要"重写"------把无效命令删掉,合并重复命令,压缩文件体积。
|-----------------------------------|---------------|----------------------------------|
| 配置项 | 作用 | 通俗理解 |
| auto-aof-rewrite-min-size 64mb
| 触发自动重写的最小文件大小 | AOF文件至少要到64MB才会考虑重写(太小没必要) |
| auto-aof-rewrite-percentage 100
| 触发自动重写的比例 | AOF文件大小是"上次重写后大小"的2倍(100%)时,触发重写 |
例子:上次重写后AOF是64MB,当文件涨到128MB(64MB×2),Redis会自动执行重写,把128MB的文件压缩成几十MB。
4. 重写的两种方式
|------|----------------------------|-------------------------------------|
| 方式 | 命令 | 逻辑 |
| 手动重写 | bgrewriteaof
| 后台异步执行(和bgsave
类似,不阻塞主线程),想压缩时手动执行 |
| 自动重写 | 靠上面的auto-aof-rewrite-*
配置 | 满足"文件大小≥64MB"且"是上次重写后2倍",自动触发 |
重写规则(很重要,决定压缩效果):
- 过期的数据不写(比如
set key1 123
后key1过期了,重写时就不记这条命令); - 无效命令删掉(比如
set key1 1
→del key1
,这两条命令抵消,重写时全删); - 重复命令合并(比如
lpush list1 a
→lpush list1 b
,合并成lpush list1 a b
); - 集合类命令最多64个元素(比如
lpush list1 1-100
,会拆成lpush list1 1-64
和lpush list1 65-100
,避免命令太长撑爆客户端缓冲区)。
三、AOF的优缺点(结合你资料总结)
|----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| 优点 | 缺点 |
| 1. 数据安全高(everysec
策略最多丢1秒数据); 2. 日志是明文命令(比如.aof
里能看到set key1 123
),能手动修改(比如误删key,可编辑AOF删掉del
命令); 3. 重写机制能控制文件大小 | 1. 文件比RDB大(即使重写后,也比同数据量的RDB大); 2. 恢复慢(需要重新执行所有命令,10G的AOF可能要恢复几分钟); 3. 同步策略如果设always
,会严重影响性能 |
10.3 终极对比:RDB vs AOF(你资料的核心总结)
直接用表格清晰对比,帮你做选择:
|-------|---------------------------|--------------------------------|
| 对比维度 | RDB | AOF |
| 核心逻辑 | 存"数据快照"(全家福) | 存"操作日志"(记账本) |
| 存储空间 | 小(二进制压缩) | 大(明文命令,重写后会缩小) |
| 存储速度 | 慢(每次要写全量数据,fork耗资源) | 快(只追加命令,IO压力小) |
| 恢复速度 | 快(直接读文件进内存) | 慢(要执行所有命令) |
| 数据安全性 | 低(可能丢两次快照间的所有数据) | 高(everysec
最多丢1秒,always
零丢失) |
| 资源消耗 | 高(fork子进程+写全量数据,耗CPU/内存) | 低(只追加命令,重写也是后台执行) |
| 启动优先级 | 低(如果RDB和AOF都开,重启时优先加载AOF) | 高(优先加载,数据更新) |
10.4 生产环境怎么选?(官方建议+实战经验)
- 优先选"RDB+AOF同时开":
-
- 重启时优先加载AOF(数据最新,丢得少);
- RDB用来做全量备份(比如每天凌晨用
bgsave
生成RDB,传到备份服务器,万一AOF文件损坏,还能靠RDB恢复)。
- 只选RDB的场景:
-
- 对数据不敏感(比如缓存热点数据,丢了能从数据库重新加载);
- 追求极致的恢复速度(比如Redis作为缓存,重启要快)。
- 不建议只选AOF:
-
- 官方提到"可能出现Bug"(虽然少见,但AOF日志如果损坏,恢复可能失败;而RDB更稳定);
- 恢复速度慢,大文件恢复耗时久。
- 都不选的场景:
-
- 纯内存缓存(比如存会话数据,丢了不影响业务,只需要Redis快)。
最后补充:你资料里的其他配置(非持久化,但很重要)
这些配置虽然不是持久化核心,但影响Redis性能和稳定性,顺便解释一下:
zset-max-ziplist-value 64
:zset类型的value如果≤64字节,用紧凑的ziplist结构(省内存);超过就用普通zset(查得快)。hll-sparse-max-bytes 3000
:HyperLogLog类型(统计基数用),value≤3000字节用稀疏结构(省内存),超过用稠密结构(计算快)。activerehashing yes
:Redis会每100毫秒用1毫秒CPU,对哈希表重新哈希(省内存);如果你的业务对延迟要求极高(比如不能接受2毫秒延迟),就设no
,否则默认yes
。client-output-buffer-limit
:限制客户端的输出缓冲区(比如slave同步数据时,如果太慢,缓冲区满了就断开连接,避免Redis内存被撑爆)。
Redis的删除策略
结合你提供的详细资料,我会围绕"平衡内存与CPU资源"这个核心目标,详细解析Redis的两种删除策略(基于过期时间和基于内存淘汰),包括它们的工作原理、优缺点缺点和实际应用。
一、数据删除策略的核心目标
Redis的删除策略本质上是在**"内存占用"和"CPU消耗"之间找平衡**:
- 若过度追求"节省内存",可能会频繁删除数据,消耗大量CPU,导致Redis响应变慢;
- 若过度追求"节省CPU",可能会让大量无效数据堆积在内存,导致内存溢出(OOM)。
两种策略(过期时间删除+内存淘汰)相互配合,就是为了避免"顾此失彼",保证Redis整体性能稳定。
二、基于过期时间的删除策略
当我们用EXPIRE
等命令给key设置过期时间后,Redis需要决定何时删除这些过期的key 。首先明确一个重要结论:"过期的数据不会立即被物理删除",而是通过以下三种策略处理:
1. 定时删除:"到期就删,绝不拖延"
- 原理:给每个设置了过期时间的key绑定一个"定时器",当过期时间一到,定时器就立即执行删除操作。
- 优点:内存友好------过期数据会被及时清理,不会占用内存。
- 缺点:CPU压力大------无论当前Redis有多忙,定时器都会强制占用CPU执行删除,可能导致正常请求响应变慢(比如大量key同时过期时,CPU会被删除操作占满)。
- 总结 :这是"用CPU换内存"的策略,Redis不采用这种方式,因为会严重影响性能。
2. 惰性删除:"用到了才检查,没用到就不管"
- 原理 :数据过期后不主动删除,等到下次访问该key时才做检查:
-
- 如果没过期,正常返回数据;
- 如果已过期,立即删除并返回"不存在"。
- 优点:CPU友好------只在必要时(访问数据时)才检查过期,平时不消耗CPU。
- 缺点:内存不友好------如果过期数据长期不被访问,会一直占用内存(比如一个key过期后再也没人查,就会成为"内存垃圾"),可能导致内存泄漏。
- 总结:这是"用内存换CPU"的策略,Redis会用到这种方式,但仅靠它不够(无法解决内存垃圾堆积问题)。
3. 定期删除:"周期性抽查,重点清理"
这是Redis实际使用的核心策略,结合了定时删除和惰性删除的优点,平衡CPU和内存消耗。
工作流程(根据你资料中的细节):
- 执行频率 :Redis启动时读取
server.hz
配置(默认10),每秒钟执行server.hz
次检查(即默认每100ms一次)。 - 每次执行时长 :每次检查最多占用
250ms / server.hz
(默认25ms),避免长时间占用CPU。 - 检查逻辑:
-
- 从所有数据库(0-15号库)中,随机挑选
W
个设置了过期时间的key(W
是固定值,由ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
定义)。 - 删除其中已过期的key。
- 如果本轮删除的过期key占比超过25%(说明过期key较多),就重复检查当前数据库;如果占比≤25%,就检查下一个数据库。
- 记录当前检查到哪个数据库(
current_db
),下次从这里继续,避免每次都从0号库开始。
- 从所有数据库(0-15号库)中,随机挑选
- 优点:
-
- CPU消耗可控------每次检查有时间上限(25ms),不会阻塞服务;
- 内存占用可控------通过"随机抽查+重点清理"(占比超25%就重复检查),能持续清理过期数据,避免大量堆积。
- 总结:这是"周期性抽查+动态调整频率"的策略,是Redis处理过期数据的主力方式。
三种过期删除策略的配合
Redis实际采用"定期删除+惰性删除"的组合:
- 定期删除:主动抽查并清理部分过期key,减少内存垃圾;
- 惰性删除:当访问某个key时,兜底检查是否过期,确保返回有效数据。
这种组合既避免了定时删除的CPU浪费,又缓解了惰性删除的内存堆积问题。
三、基于内存淘汰的删除策略(逐出算法)
当Redis内存达到maxmemory
限制(比如配置了最大使用4GB内存),且有新数据要写入时,Redis会触发内存淘汰策略,删除部分数据腾出空间。
触发条件
- 内存使用量达到
maxmemory
配置的值(默认不限制,生产环境必须设置,比如物理内存的50%-70%); - 执行新的写命令时,内存不足。
影响淘汰的关键配置
|---------------------|----------------------------|---------------------|
| 配置项 | 作用 | 重要性 |
| maxmemory
| 设置Redis最大可用内存(如4gb
) | 必须配置,否则可能耗尽服务器内存 |
| maxmemory-samples
| 每次随机挑选多少个key作为"待淘汰候选"(默认5) | 选太少可能淘汰不准确,选太多消耗CPU |
| maxmemory-policy
| 淘汰策略(核心) | 决定优先删哪些数据,直接影响业务 |
8种内存淘汰策略(按淘汰范围分类)
根据你资料中的分类,策略分为三类:
1. 只淘汰"设置了过期时间的key"(4种)
volatile-lru
:淘汰"最近最少使用"的过期key(关注"访问时间");volatile-lfu
:淘汰"最近使用次数最少"的过期key(关注"访问频率");volatile-ttl
:淘汰"剩余过期时间最短"的过期key(即将过期的先删);volatile-random
:随机淘汰一个过期key。
适用场景:需要保留永久有效数据(如配置信息),只清理临时过期数据(如会话)。
2. 淘汰"所有key"(不管是否过期,3种)
allkeys-lru
:淘汰"最近最少使用"的所有key(最常用);allkeys-lfu
:淘汰"最近使用次数最少"的所有key;allkeys-random
:随机淘汰任意key。
适用场景:Redis作为缓存(如热点数据缓存),所有数据都是临时的,优先保留常用数据。
3. 不淘汰,直接报错(1种)
noeviction
:禁止淘汰任何数据,新写操作直接返回OOM错误((error) OOM command not allowed...
)。
适用场景 :几乎不用,除非数据绝对不能丢(但此时应关闭过期时间,且maxmemory
设为足够大)。
淘汰执行流程
- 每次写命令前,Redis调用
freeMemoryIfNeeded()
检查内存是否充足; - 若内存不足,根据
maxmemory-policy
从候选key中挑选要删除的数据; - 重复删除,直到内存足够或所有候选key都删完;
- 若仍不足,返回OOM错误。
四、总结:两种策略的关系与选择
|-----------|-----------------|--------------------------|------------------------------------------------------------------------------------------------------|
| 策略类型 | 解决的问题 | 核心逻辑 | 生产建议 |
| 基于过期时间的删除 | 清理"已过期但未删除"的key | 定期删除(主动抽查)+ 惰性删除(访问时检查) | 无需手动干预,依赖Redis默认机制即可 |
| 基于内存淘汰的删除 | 内存不足时腾出空间 | 按策略淘汰部分key(LRU/LFU/TTL等) | 必须配置maxmemory
和maxmemory-policy
: - 缓存场景用allkeys-lru
; - 有永久数据场景用volatile-lru
; - 禁用noeviction
|
这两种策略相辅相成:过期删除策略负责日常清理过期数据,内存淘汰策略在内存紧张时"兜底",共同保证Redis在内存和CPU之间的平衡,避免性能问题或数据丢失。
要理解缓存的这4个核心问题(预热、雪崩、击穿、穿透),我们可以结合"生活场景+技术原理+解决方案"的方式,用最通俗的语言拆解,每个概念都先讲"是什么""举例子",再讲"怎么解决"。
一、缓存预热:"提前把货备到货架上"
1. 通俗理解
缓存的核心是"把数据库里的热点数据,提前加载到Redis等缓存中",这样用户请求时直接查缓存,不用查数据库。
而缓存预热就是:在系统启动/流量高峰前,主动把"未来会被频繁访问的数据"加载到缓存里,避免用户第一次访问时"缓存没数据,只能查数据库"(也就是"缓存冷启动"问题)。
2. 举个生活例子
你开了一家便利店:
- 平时用户买可乐,你需要从仓库(数据库)拿出来放到货架(缓存),再给用户(第一次访问慢);
- 如果你知道周末会有很多人买可乐(流量高峰),提前在周五晚上就把100瓶可乐摆到货架上(缓存预热),周末用户来直接拿,不用等你去仓库取------这就是缓存预热的作用。
3. 技术场景举例
比如电商平台的"双11活动":
- 活动开始后,"iPhone 15""华为Mate 60"这些商品的详情页会被千万用户访问;
- 如果不做缓存预热,双11 0点第一个用户访问时,缓存里没有数据,会直接查数据库(数据库压力骤增,可能卡崩);
- 做了缓存预热:活动前1小时,通过脚本主动把这些热门商品的信息(价格、库存、详情)从数据库查出来,写入Redis缓存;
- 0点后用户访问,直接读Redis,速度快,数据库也没压力。
4. 怎么实现缓存预热?
- 脚本批量加载:写个Python/Shell脚本,查询数据库的热点数据(比如按历史访问量排序的Top1000商品),批量写入缓存;
- 接口触发:系统启动后,调用一个"预热接口",主动加载数据;
- 增量预热:如果数据太多,分批次加载(比如每次加载100条,避免一次性给数据库和缓存太大压力)。
二、缓存雪崩:"货架全塌了,所有用户都挤去仓库"
1. 通俗理解
缓存里的大量数据同时过期 ,或者缓存服务器(比如Redis集群)突然宕机,导致所有用户的请求都"绕开缓存,直接冲击数据库"------数据库扛不住这么大的流量,直接崩了,这就是缓存雪崩。
2. 举个生活例子
还是你的便利店:
- 你之前把所有饮料(可乐、雪碧、矿泉水)都摆到货架上,并且规定"所有饮料晚上8点过期,必须下架";
- 晚上8点一到,货架上所有饮料都没了(缓存大量过期);
- 刚好这时来了100个顾客买饮料,你只能让所有人等着,自己跑回仓库一瓶瓶拿(所有请求查数据库);
- 仓库门口挤成一团,你根本忙不过来,最后顾客全走了(数据库崩了,服务不可用)。
3. 技术场景举例
比如某App的首页推荐列表:
- 为了减少缓存占用,给首页所有推荐内容的缓存都设置了"24小时过期",并且过期时间都设成了"每天凌晨2点";
- 凌晨2点一到,所有首页缓存同时失效;
- 早上8点用户起床刷App,10万用户同时访问首页,缓存里没数据,全去查数据库;
- 数据库平时QPS(每秒查询量)只有1000,突然承受10万QPS,直接过载宕机,App首页打不开。
4. 怎么解决缓存雪崩?
(1)避免缓存"同时过期"
- 给缓存过期时间加"随机值":比如原本设24小时过期,改成"24小时±1小时",这样数据会在23-25小时内陆续过期,不会集中失效;
- 分批次设置过期时间:比如首页推荐分10组,第一组23小时过期,第二组23.5小时,...,第十组25小时,分散过期时间。
(2)避免缓存"突然宕机"
- 缓存集群化:用Redis集群(主从+哨兵/Redis Cluster),即使主节点宕机,从节点能立刻顶上,避免缓存整体不可用;
- 缓存降级/熔断:如果缓存真的宕机,暂时用"默认数据"(比如首页推荐显示"热门商品TOP10"的静态数据)或者"拒绝部分非核心请求",不让所有流量冲击数据库(比如用Sentinel/Hystrix做熔断)。
(3)数据库兜底
- 数据库读写分离:主库写,从库读,缓存失效时请求打到从库,分散压力;
- 数据库限流:用Nginx/网关限制每秒访问数据库的请求数(比如最多1万QPS),超出的请求返回"稍等再试"。
三、缓存击穿:"某个热门货架塌了,所有人都挤去拿这一件货"
1. 通俗理解
缓存里的某一个热点数据过期了 (其他数据都正常),但此时刚好有大量用户同时访问这个热点数据------这些请求会同时绕开缓存,冲击数据库的"某一行数据",导致数据库这一行的查询压力骤增(虽然整体数据库没崩,但这个热点数据的查询会卡死),这就是缓存击穿。
注意:和"雪崩"的区别是------雪崩是"大量数据同时过期",击穿是"单个热点数据过期"。
2. 举个生活例子
你的便利店有个"爆款冰淇淋",每天能卖1000支:
- 你平时会把冰淇淋放在门口的冷柜(缓存)里,方便顾客拿,冷柜里的冰淇淋过期时间设为"下午5点";
- 下午5点一到,冷柜里的冰淇淋刚好卖完且过期(热点数据过期);
- 这时刚好来了50个顾客,都要买这款冰淇淋(大量并发请求);
- 你只能让所有人等着,自己跑回仓库拿冰淇淋(所有请求查数据库),仓库里找这一款冰淇淋的过程中,其他顾客全在催,场面混乱(数据库热点行查询压力大)。
3. 技术场景举例
比如某明星官宣结婚,#明星结婚# 这个话题的热度飙升:
- 微博把"#明星结婚# 的话题详情页数据"存到Redis,过期时间设为10分钟;
- 10分钟后缓存过期,刚好这时有10万用户同时刷新这个话题(大量并发请求);
- 缓存里没数据,10万请求同时查数据库的"话题详情表"的这一行数据;
- 数据库的"话题详情表"中,这一行数据的查询QPS瞬间达到10万,远超数据库单表的承受能力(通常单表QPS上限是1万),导致这一行的查询超时,用户刷新不出来。
4. 怎么解决缓存击穿?
核心思路:让"过期的热点数据"在缓存中"续期",或者让请求"排队查数据库",避免并发冲击。
(1)互斥锁(锁机制)
- 当第一个请求发现缓存过期时,先获取一把"互斥锁"(比如Redis的
SETNX
命令,只有一个请求能拿到锁); - 拿到锁的请求去查数据库,查到后更新缓存,然后释放锁;
- 其他请求没拿到锁,就每隔100ms重试一次,直到缓存更新完成,再读缓存;
- 这样就保证了"只有一个请求去查数据库",避免并发冲击。
(2)热点数据永不过期
- 对于绝对的热点数据(比如爆款商品、热门话题),不设置过期时间,让数据永久存在缓存中;
- 同时后台开一个"定时任务",每隔一段时间(比如5分钟)去数据库更新一次缓存中的数据,保证数据最新;
- 缺点:如果数据更新不频繁(比如商品价格很少变),适合用;如果数据更新频繁,可能导致缓存和数据库数据不一致。
(3)缓存预热+提前续期
- 对热点数据做缓存预热时,设置更长的过期时间(比如1小时);
- 同时后台开一个"监控任务",每隔30分钟检查一次热点数据的剩余过期时间,如果小于10分钟,就主动去数据库更新缓存,延长过期时间;
- 避免热点数据在高并发时过期。
四、缓存穿透:"用户要的货根本不存在,所有人都白跑一趟仓库"
1. 通俗理解
用户请求的数据"在缓存里没有,在数据库里也没有"(比如用户查"不存在的商品ID=999999"),导致每次请求都会"绕开缓存,直接查数据库"------如果有大量这样的请求(比如恶意攻击),数据库会被这些"无效请求"打崩,这就是缓存穿透。
注意:和"击穿"的区别是------击穿是"数据在数据库里存在,但缓存过期了",穿透是"数据在数据库里也不存在"。
2. 举个生活例子
有人故意捣乱,每天派100个人来你的便利店,问"有没有外星人饮料"(不存在的商品):
- 你先看门口的货架(缓存),没有"外星人饮料"(缓存没数据);
- 你只能跑回仓库找,翻遍所有货架都没有(数据库没数据),然后告诉顾客"没有";
- 100个人每天都来问,你每天要跑100次仓库,每次都白跑(无效数据库查询),最后累得没力气服务正常顾客(数据库被无效请求打崩)。
3. 技术场景举例
比如某电商平台被恶意攻击:
- 攻击者写脚本,每秒发送1万次请求,查询"商品ID=1000000000""商品ID=1000000001"...(这些ID在数据库里根本不存在);
- 每次请求都先查Redis,Redis里没有这些ID的数据(缓存没数据),然后查数据库,数据库里也没有,返回"不存在";
- 数据库每秒要处理1万次"无效查询",虽然每次查询很快,但累积起来QPS过高,导致数据库CPU占用100%,正常用户查"存在的商品"也查不了。
4. 怎么解决缓存穿透?
核心思路:让"不存在的数据"也在缓存中留个"标记",避免每次都查数据库。
(1)缓存空值(Null值缓存)
- 当请求查"不存在的数据"时(比如商品ID=999999,数据库返回空),也把这个"空结果"写入缓存,设置一个较短的过期时间(比如5分钟);
- 下次再有人查"商品ID=999999",直接读缓存的空值,不用查数据库;
- 注意:过期时间不能太长,避免"后续数据库真的新增了这个数据,缓存里还是空值"(比如后来真的有了商品ID=999999,缓存的空值会导致用户看不到新数据)。
(2)布隆过滤器(Bloom Filter)
- 提前把数据库里"所有存在的key"(比如所有商品ID、所有用户ID)存入布隆过滤器(一种高效的"存在性判断"数据结构);
- 当用户请求过来时,先通过布隆过滤器判断"这个key是否存在于数据库":
-
- 如果布隆过滤器说"不存在",直接返回"没有",不用查缓存和数据库;
- 如果布隆过滤器说"可能存在"(布隆过滤器有极小的误判率),再查缓存和数据库;
- 优点:布隆过滤器占用内存极小(比如1亿个商品ID,只需要100MB左右内存),判断速度极快;
- 缺点:有极小的误判率(比如0.1%),可能把"不存在的key"判断为"可能存在",但不会把"存在的key"判断为"不存在"。
(3)接口限流+恶意请求拦截
- 对接口做限流(比如每个IP每秒最多请求10次),避免恶意脚本的高频请求;
- 拦截明显的恶意请求(比如请求的商品ID是负数、或者远超正常范围的ID,直接返回错误)。
总结:4个概念的核心区别
|------|---------------------|--------------|-----------------|
| 概念 | 核心原因 | 影响范围 | 通俗比喻 |
| 缓存预热 | 缓存冷启动,无热点数据 | 首次访问慢 | 货架没备货,需要去仓库拿 |
| 缓存雪崩 | 大量数据同时过期/缓存宕机 | 所有请求冲击数据库 | 所有货架塌了,全去仓库 |
| 缓存击穿 | 单个热点数据过期 | 单个热点的请求冲击数据库 | 单个爆款货架塌了,全去拿这一款 |
| 缓存穿透 | 请求不存在的数据(缓存+数据库都没有) | 无效请求冲击数据库 | 要的货根本没有,全白跑仓库 |
记住:缓存的核心是"减少数据库压力",这4个问题的解决方案,本质都是"尽可能让请求停在缓存层,或者减少无效请求到数据库"。