一、Redis三大件

1.1 缓存穿透

在用户访问数据的时候,这个数据既不在缓存也不再数据库,就会导致这个请求哪里都没打中,直接穿透了这两层数据库,当有大量这样的数据到达的时候就会导致数据库压力增大。

  1. 布隆过滤器:利用一个位数组和多个哈希函数,快速判断一个元素是绝对不存在还是可能存在于数据库中。
  2. 接口层参数校验:最基本的防线,在网关或API入口处,对请求参数进行合法性检查,例如检查ID的格式、范围、类型是否符合预期。
  3. 黑名单:对于已知的恶意用户ID、IP或异常key,可以将其维护在一个Redis Set或独立的黑名单列表中。
  4. 缓存空值:当数据库查询结果为空时,程序不直接返回,而是向Redis中写入一个特殊标记的值(如空字符串""或"NULL"),并为其设置一个较短的过期时间(TTL),例如30秒到5分钟。
  5. 互斥锁:当缓存未命中时,不是所有请求都一窝蜂去查数据库,而是通过分布式锁(基于Redis SETNX实现)来控制,只允许一个请求去数据库查询并回填缓存,其他请求则短暂等待后重试读取缓存。必须设置锁超时:防止拿到锁的请求崩溃,导致锁永不释放,形成死锁。等待策略:未抢到锁的请求重试次数或等待时间需要设上限,避免线程长时间阻塞。
  6. 限流与熔断降级:在系统层面配置限流规则(如基于 QPS),当针对某些key的请求速率超过阈值时,直接快速失败或返回降级内容,避免数据库承受超出其承载能力的流量。对于确认无法处理的异常来源,可以触发熔断,暂时切断对特定接口或IP的访问。

1.2 缓存击穿

在用户访问数据的时候,这个数据的hot key过期了,就会有大量请求大道数据库,击穿了redis这一层缓存,导致数据库压力增大

  1. 互斥锁:同上,只允许一个请求去数据库重建缓存,其他请求等待片刻后重新读取缓存
  2. 逻辑过期:就是让hot key在物理上永不过期,用户可能会短暂读到旧数据,因此该方案要求业务场景能容忍最终一致性,例如商品详情页、首页 Banner、排行榜等。
  3. 大数据预热:对于可预知的热点(如大促商品),通过定时任务在缓存逻辑过期或物理过期前主动刷新,使其“永不失效”。这通常与逻辑过期策略配合使用,形成“异步刷新 + 定时保底”的双重保障。
  4. 熔断降级和互斥锁结合:当系统检测到数据库压力过大(如响应超时)时,触发熔断,直接返回兜底数据(如默认值或旧数据),而不是让请求继续冲击数据库。在互斥锁方案中,也可以优化为:未抢到锁的请求不等待,直接返回降级数据,以此将“保护数据库”作为最高优先级。
  5. 二级缓存:在 Redis 之前再加一层本地缓存(如 Caffeine、Guava Cache)。热点数据在本地缓存中命中,几乎不产生网络开销,能极大地分担 Redis 和数据库的压力,作为整体的性能与稳定性兜底。

1.3 缓存雪崩

大量缓存数据同时过期或者Redis故障宕机的时候,大量的数据直接打到了数据库里,导致数据库压力增加。

  1. 过期时间加上随机值:在基础 TTL 上叠加一个随机偏移量,例如 TTL = 30分钟 + random(0~5分钟),让 Key 的失效时间分散开,避免同一时刻批量过期。这是最常用的基础方案。
  2. 分批过期 / 错峰预热:对于可预知会批量写入的缓存,不要一次性写入相同 TTL,而是按业务维度分批设置不同过期时间;系统启动或大促前预热时也分批加载,避免启动瞬间形成流量尖峰
  3. 热点数据永不过期 + 后台更新:对核心热点数据不设置物理 TTL,而是采用“逻辑过期 + 异步刷新”或定时任务主动刷新,从源头消除集中失效。这通常与击穿方案中的逻辑过期策略配合使用。
  4. Redis高可用架构:使用哨兵模式实现主从自动故障转移,或使用 Redis Cluster 实现分片与高可用。避免单实例故障导致整个缓存层不可用。
  5. 多级缓存:在 Redis 之前增加本地缓存(如 Caffeine、Guava Cache),形成“本地缓存 → Redis → 数据库”的多级结构。即使 Redis 整体故障,本地缓存仍能承接一部分热点流量,为恢复争取时间。本地缓存适合存放极热、变化不频繁的数据。
  6. 持久化与快速恢复:合理配置 RDB/AOF,确保 Redis 重启后能较快恢复数据,缩短缓存层不可用窗口。同时要有容量规划和故障演练,避免恢复过程中再次被流量打垮。
  7. 熔断+限流+互斥锁+数据库层保护

二、Redis快照与恢复机制

2.1 AOF日志

Redis在执行完一条写命令后,就会把该命令追加写入一个文件中,在Redis重启时,会读取这些指令,然后逐一执行来进行数据恢复。

过程:

  1. 执行写命令后,把命令追加到自己的aof_buf缓冲区
  2. 调用write()把aof_buf的数据拷贝到内核的page cache中
  3. 内核在某个时机把page cache刷盘,持久化落库

三种刷盘策略:

  1. always:每次写操作命令后同步将AOF日志落盘
  2. Everysec:每次写操作命令后,现将命令写入AOF文件的内核缓冲区,然后每隔一秒落盘
  3. No:同上,但是落盘时机由操作系统决定

2.2 AOF重写

AOF本质是一个日志文件,随着执行的写操作命令越来越多,文件的大小会越来越大。所以,Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

AOF 重写机制是:在重写时,读取当前 Redis 中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。

Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么做可以达到两个好处

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
  • 子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。

2.3 RDB快照

本质是一个全量快快照,是某一瞬间的内存数据,一般有两种方式来保存,save和bgsave,前者直接在主线程创建RDB文件,所以可能会导致阻塞问题,后者是调用子进程,所以不会有阻塞问题,生产下基本不会用save,除非是迁移等特殊场景

而bgsave在执行过程中,如果有内存被更新了,当主进程需要修改一个共享页中的数据时,会触发一个保护机制。操作系统会为这个即将被修改的物理页创建一个副本,并将主进程的写操作定向到这个副本上。此后,主进程读写这个副本,而子进程依然读取原始的、未被修改的共享页,从而保证了子进程看到的是 fork 瞬间那个稳定、一致的数据快照。当子进程结束后,子进程读取的那一块内存释放,复制的那一份转正。

2.4 AOF与RDB性能对比

  • 写入性能上,RDB 优于 AOF。RDB 在两次快照之间不消耗 IO,AOF 则要持续写盘,尤其是 always 模式下每条命令都 fsync,在高并发写场景下会成为瓶颈;everysec 是性能和安全的折中。
  • **恢复速度上,RDB 明显更快。**它直接把二进制数据加载回内存即可,而 AOF 要逐条重放命令,数据量大时启动会很慢。
  • 数据安全性上,AOF 胜出。RDB 在两次快照之间宕机会丢数据,可能是几分钟;AOF 在 everysec 下最多丢 1 秒,always 下几乎不丢。
  • 文件体积上,RDB 小很多,因为它是压缩过的二进制;AOF 是命令文本累积,即便有重写机制,体积通常还是更大。

2.5 混合持久化

redis4.0后可以使用混合持久化来做快照与恢复,在 AOF 重写时,由子进程把当前全量数据以 RDB 二进制格式写入新 AOF 文件头部,主进程同时将重写期间的增量写命令缓冲下来,待子进程完成后以 AOF 文本格式追加到文件尾部,最后原子替换旧 AOF;恢复时先批量加载 RDB 头部,再回放少量 AOF 尾部命令。其特性是必须开启 AOF,并由 aof-use-rdb-preamble 控制(4.0 默认关闭,5.0 起默认开启),文件呈“RDB 头 + AOF 尾”结构。优点是启动恢复快、数据安全性接近纯 AOF(最多约丢 1 秒)、文件体积更小;缺点是 AOF 头部不再可读、难以手动查看或修改,且混合格式 AOF 不被 4.0 之前版本识别,版本回退需注意兼容性。

三、Redis删除策略

Redis主要采用惰性删除加上定时删除策略

惰性删除就是不主动删除key,在用到的时候看是否过期了,过期了就删

定期删除的流程如下:

  1. 随机抽样:从过期字典中随机抽取 20 个键。
  2. 检查与删除:删除其中所有已过期的键。
  3. 判断是否继续:如果这 20 个键中,过期键的比例超过 25%,说明系统中可能积压了大量过期数据,Redis 会立即再次抽取 20 个键,重复步骤 1-2。
  4. 终止条件:当抽样中过期键的比例低于 25%,或本轮定期删除的**累计执行时间达到上限(默认 25 毫秒)**时,循环终止,等待下一轮定时任务触发。

3.1 Redis持久化中过期的key如何处理

对于RDB来说:

生成RDB文件的时候:Redis会检查每一个键,已经过期的键不会被写入RDB文件。因此,RDB文件本身是“干净”的,不包含过期数据。

载入RDB文件的时候:

  • 主服务器:会过滤掉RDB文件中所有已过期的键,只载入未过期的数据。
  • 从服务器:会载入RDB文件中的所有键,无论是否过期。这是因为从服务器的过期键删除权完全由主服务器控制,载入后等待主服务器的同步指令即可,无需自己判断

对于AOF来说:

  • 日常写入:当一个键逻辑上已过期,但尚未被惰性删除或定期删除机制物理清除时,AOF文件不会对此产生任何影响,它依然可能存在于AOF记录中。只有当该键实际被删除(通过惰性删除或定期删除)时,Redis才会向AOF文件追加一条DEL命令,显式地记录这次删除操作。
  • AOF重写:在执行BGREWRITEAOF进行AOF重写时,Redis会过滤掉所有已过期的键,确保重写后的新AOF文件不包含过期数据,从而精简文件体积。

四、缓存与数据库一致性

4.1 基础方案:旁路缓存模式(Cache Aside Pattern)

最主流最通用的方案,由业务代码直接控制缓存与数据库的操作,核心就是:读时回填,写时删除。也就是读的时候先查缓存,命中则返回;未命中则查数据库,将结果写回缓存后再返回。写的时候先更新数据库,成功后再删除缓存(而不是更新缓存)。后续的读请求未命中缓存时,会自然从数据库加载最新值并重建缓存。

直接删除而不是更新的原因是:因为更新缓存存在并发写乱序的风险

优点是它产生不一致的窗口期极短——仅在“数据库更新提交”到“缓存删除完成”这一瞬间,并且还需要恰好有并发读请求将旧值回填,才会产生实际的脏数据。

4.2 针对残留漏洞的强化方案

  1. 延迟双删:主要是针对并发回填的问题,一个读请求在缓存失效后,从数据库读到了旧值,但在它准备将旧值写回缓存前被阻塞;此时一个写请求完成了“更新数据库+删除缓存”;随后,那个被阻塞的读请求苏醒,将过期的旧值写入了缓存,导致缓存中留下脏数据。