跳转到内容

redis

标签「redis」下的 2 篇文章

redis核心原理与实战应用

Redis 常被用作缓存、分布式锁、排行榜、计数器和会话存储。它的核心优势不是“能存数据”这么简单,而是用内存访问、丰富数据结构、持久化和高可用机制,支撑高并发场景下的读写性能与系统稳定性。

这篇文章按实际使用路径整理 Redis:先看基础能力,再看高可用与缓存问题,最后整理一致性、性能优化和典型场景。

Redis 支持多种常用数据结构:

  • String:适合缓存简单值、计数器、分布式锁标记。
  • Hash:适合存储对象字段,例如用户信息。
  • List:适合队列、消息列表、时间线等场景。
  • Set:适合去重、集合交并差。
  • Sorted Set(ZSet):适合排行榜、权重排序、延迟队列。

这些结构让 Redis 不只是一个 Key-Value 缓存,而是可以承接一部分高频读写业务逻辑。

Redis 主要有两种持久化方式:RDB 和 AOF。

RDB(快照)

  • 定期生成内存快照,例如通过 bgsave 命令。
  • 优点:文件体积较小,数据恢复速度快。
  • 缺点:如果 Redis 异常退出,可能丢失最后一次快照之后的数据。

AOF(追加日志)

  • 记录每一次写操作命令,例如 SET key value
  • 优点:数据安全性更高,可以配置更高频率的落盘。
  • 缺点:文件体积更大,恢复速度通常慢于 RDB。

实际生产中常见策略是 RDB + AOF 组合使用:RDB 用于快速恢复,AOF 用于降低数据丢失风险。

Redis 主从同步分为全量同步和增量同步。

全量同步

通常发生在从节点初次连接主节点,或断线时间过长无法增量追赶时:

  1. 主节点生成 RDB 文件。
  2. 主节点把 RDB 文件发送给从节点。
  3. 从节点清空旧数据并加载 RDB。
  4. 主节点继续把同步期间缓冲区中的写操作发送给从节点。

增量同步

当从节点短暂断线后重新连接,可以通过 psync 根据复制偏移量(offset)同步缺失的写操作,避免重新做一次全量同步。

Redis 常见高可用方案主要有两类:

  • 哨兵模式(Sentinel):监控主从节点状态,在主节点故障时自动完成故障转移。
  • 集群模式(Cluster):通过分片存储数据,提升容量上限,并支持水平扩展。

哨兵更偏向主从高可用,集群更偏向容量扩展和分片治理。

缓存击穿指的是一个热点 Key 在高并发访问时突然过期,大量请求同时打到数据库。

常见解决方案:

  • 热点 Key 不设置过期时间,通过后台任务主动刷新。
  • 互斥锁或分布式锁,只允许一个请求回源重建缓存。
  • 逻辑过期,缓存中保存过期时间,由后台异步刷新数据。

缓存穿透指的是大量请求访问不存在的数据,缓存无法命中,请求直接落到数据库。

常见解决方案:

  • 布隆过滤器:在请求进入缓存和数据库之前过滤明显不存在的 Key。
  • 缓存空对象:对不存在的数据短时间缓存空值,避免重复打到数据库。
  • 参数校验:拦截非法 ID、异常参数和明显无效请求。

缓存雪崩指的是大量 Key 在同一时间过期,导致请求集中打到数据库。

常见解决方案:

  • 随机过期时间,避免大量 Key 同时失效。
  • 限流降级,在数据库压力过高时保护核心服务。
  • 多级缓存或集群部署,减少单点缓存失效带来的冲击。

四、数据一致性与 Redis-MySQL 同步

Section titled “四、数据一致性与 Redis-MySQL 同步”

业务中常见的数据源组合是 MySQL 存储主数据,Redis 作为缓存。这里的核心问题是:数据库更新后,缓存如何保持一致。

常见方案:

  • 先写 MySQL,再更新 Redis:逻辑直接,但并发下可能出现旧值覆盖新值。
  • 先写 MySQL,再删除 Redis:更常见,后续请求回源数据库并重建缓存。
  • 延迟双删:写库后删除缓存,短暂延迟后再次删除,降低并发读写导致的脏缓存概率。
  • 最终一致性:通过消息队列或 Canal 监听 MySQL Binlog,把数据变更异步同步到 Redis。
  • 对实时一致性要求很高的场景,需要更严格的事务或强一致方案。
  • 对短暂延迟可接受的场景,通常使用缓存删除、消息队列、Binlog 同步等最终一致性方案。

缓存一致性没有绝对通用答案,重点是结合业务对“延迟、正确性、复杂度”的要求做取舍。

Redis 提供 slowlog 用于排查慢查询。常见优化方向:

  • 避免 KEYS *SORT、大范围 SUNION 等高复杂度命令。
  • 对批量操作使用 Pipeline,减少网络往返。
  • 控制单次命令处理的数据规模,避免阻塞主线程。

当内存达到 maxmemory 限制时,Redis 会根据配置的淘汰策略处理 Key。

常见策略:

  • noeviction:内存不足时拒绝写入。
  • volatile-lru:只在设置了过期时间的 Key 中淘汰最近最少使用的 Key。
  • volatile-lfu:只在设置了过期时间的 Key 中淘汰最近最不常用的 Key。
  • allkeys-lru:在所有 Key 中淘汰最近最少使用的 Key。
  • allkeys-lfu:在所有 Key 中淘汰最近最不常用的 Key。
  • allkeys-random:在所有 Key 中随机淘汰。

缓存场景通常更常见 allkeys-lruallkeys-lfu,但具体选择要看访问分布和业务容忍度。

BigKey 指单个 Key 占用过大内存或包含过多元素,会导致网络传输、删除、迁移和持久化变慢。

HotKey 指少数 Key 被高频访问,容易造成单点压力。

优化方向:

  • 拆分过大的 Hash、List、Set、ZSet。
  • 避免一次性读取或删除大 Key。
  • 对热点数据做本地缓存、多副本缓存或请求合并。

Redis 的命令执行主要是单线程模型,这让它避免了大量锁竞争,命令执行顺序也更容易理解。

Redis 6.0 之后引入多线程处理网络 I/O,但命令执行本身仍然保持单线程语义。因此,慢命令、大 Key 操作、复杂聚合仍然可能阻塞 Redis。

Redis 对过期 Key 主要使用两种删除策略:

惰性删除

  • 获取 Key 时检查是否过期,过期则删除。
  • 优点:不主动消耗额外资源。
  • 缺点:如果过期 Key 长时间不被访问,可能继续占用内存。

定期删除

  • Redis 定期随机抽样部分 Key,删除其中已经过期的 Key。
  • 优点:可以主动清理过期数据。
  • 缺点:不保证所有过期 Key 都会立刻被删除。

Redis 默认使用 惰性删除 + 定期删除 的组合策略。

Redis 变慢时,可以从以下方向排查:

  • 检查内存是否耗尽,是否发生 Swap,例如使用 free -m
  • 检查磁盘 I/O,尤其是 AOF 写入和重写期间。
  • 使用 SSD 替代传统磁盘,降低持久化带来的 I/O 压力。
  • 排查是否使用了 KEYSSORTSUNION 等高复杂度命令。
  • 检查是否存在大批量读取、大批量删除或大集合遍历。
  • 对批量请求使用 Pipeline,减少网络延迟。

AOF 重写期间可能与 fsync 竞争磁盘资源。可以根据场景配置:

no-appendfsync-on-rewrite yes

这个配置可以降低 AOF 重写期间的写入阻塞风险,但也会增加极端情况下的数据丢失窗口,需要结合业务容忍度评估。

如果数据库和 Redis 同时出现压力波动,需要排查是否存在:

  • 大量 Key 同时过期。
  • 热点 Key 突然失效。
  • 缓存重建逻辑没有互斥保护。
特性RedisMemcached
数据类型String、Hash、List、Set、ZSet 等主要支持简单 Key-Value
持久化支持 RDB、AOF不支持持久化
内存回收支持多种淘汰策略支持 LRU 等缓存淘汰机制
原子操作支持较丰富的原子操作支持有限原子操作
线程模型命令执行以单线程为主,Redis 6.0 后支持 I/O 多线程多线程网络模型
场景适用复杂缓存、计数、排行榜、分布式锁简单缓存、高吞吐 Key-Value

简单说:如果只是做非常简单的缓存,Memcached 也可以胜任;如果需要更丰富的数据结构、持久化、高可用和分布式能力,Redis 更适合。

Redis 常见业务场景包括:

  • 缓存:存储热点数据,减少数据库压力。
  • 会话保持:存储用户 Session 或登录态。
  • 排行榜:通过 ZSet 实现 Top N 榜单。
  • 限流降级:通过计数器、滑动窗口或令牌桶实现接口限流。
  • 分布式锁:使用 SET key value NX PX 等命令实现基础锁能力。
  • 延迟队列:通过 ZSet 分数存储执行时间,实现定时任务调度。

Redis 的核心价值在于 高性能访问、灵活数据结构、持久化能力和分布式支持

实际使用 Redis 时,需要重点关注四件事:

  1. 数据结构是否选对。
  2. 缓存异常场景是否有兜底方案。
  3. Redis 与数据库之间是否能接受最终一致性。
  4. 是否避免了慢命令、BigKey、HotKey 和集中过期。

理解这些核心机制后,Redis 就不只是一个缓存组件,而是高并发系统中非常重要的性能与稳定性基础设施。

Redis 缓存穿透、击穿、雪崩面试题复盘

这是 Redis 面试里非常高频的一类问题:

什么是缓存穿透、缓存击穿和缓存雪崩?它们有什么区别?应该怎么解决?

这三个概念很容易混在一起,因为它们的结果都可能是“请求打到数据库,数据库压力变大”。但它们的根因完全不同。

可以先用一句话区分:

穿透:查的数据根本不存在。
击穿:一个热点 Key 过期。
雪崩:大量 Key 同时过期,或者 Redis 整体不可用。

缓存穿透指的是:请求的数据在缓存中不存在,在数据库中也不存在。

比如:

  • 请求一个不存在的用户 ID:id = -1
  • 请求一个已经删除的商品
  • 恶意攻击者构造大量非法参数

正常缓存流程一般是:

请求 -> 查 Redis -> Redis 没有 -> 查数据库 -> 写入 Redis -> 返回数据

但如果数据根本不存在,就会变成:

请求 -> 查 Redis -> Redis 没有 -> 查数据库 -> 数据库也没有 -> 返回空

问题在于:下一次同样的非法请求过来,Redis 里还是没有,于是又会打到数据库。

如果有人构造大量不存在的 ID,请求就会绕过缓存,持续打到数据库。

缓存穿透常见有两种解决方案:缓存空对象和布隆过滤器。

当数据库查不到数据时,也往 Redis 里写一个空值或特殊值,并设置较短过期时间。

cache-null.js
async function getUser(id) {
const cacheKey = `user:${id}`;
const cached = await redis.get(cacheKey);
if (cached !== null) {
return cached === 'NULL' ? null : JSON.parse(cached);
}
const user = await db.queryUserById(id);
if (!user) {
await redis.set(cacheKey, 'NULL', 'EX', 60);
return null;
}
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
return user;
}

这样下一次请求同一个不存在的 ID,就会被 Redis 拦住,不会继续打数据库。

它的优点是简单,缺点是如果恶意请求的非法 ID 极多,Redis 里会出现很多空值缓存。所以空值缓存的过期时间一般要短一些。

布隆过滤器适合在请求进入缓存和数据库之前,先判断这个数据是否可能存在。

请求 userId
|
v
布隆过滤器判断
|
| 不存在 -> 直接拒绝
|
| 可能存在
v
查 Redis / DB

布隆过滤器的特点是:

  • 如果它判断不存在,那就一定不存在。
  • 如果它判断存在,只能说明可能存在。

所以它适合挡掉大量明显非法的请求。

面试里可以这样说:缓存空对象适合兜住少量不存在数据,布隆过滤器适合在入口处拦截大量非法 Key。

缓存击穿指的是:某一个热点 Key 在过期瞬间,大量并发请求同时打到数据库。

注意,击穿通常是单个 Key。

比如:

  • 某个大促商品详情页
  • 某个热门直播间信息
  • 某个高访问量用户主页
  • 某条热点新闻

这个 Key 平时在 Redis 里,所以数据库压力不大。

但它刚好过期时,大量请求同时进来,发现 Redis 没有,于是一起查数据库。

热点 Key 过期
|
大量请求同时进入
|
Redis 都没查到
|
全部打到数据库

这就是缓存击穿。

缓存击穿常见有两种方案:互斥锁和逻辑过期。

互斥锁的思路是:同一时间只允许一个线程去查数据库并重建缓存,其他线程等待或重试。

mutex-lock.js
async function getProduct(id) {
const cacheKey = `product:${id}`;
const lockKey = `lock:product:${id}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const locked = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (!locked) {
await sleep(50);
return getProduct(id);
}
try {
const product = await db.queryProductById(id);
await redis.set(cacheKey, JSON.stringify(product), 'EX', 3600);
return product;
} finally {
await redis.del(lockKey);
}
}

这个方案可以保护数据库,但会让部分请求等待,接口耗时可能变长。

逻辑过期的思路是:Redis 里的数据不设置物理过期,或者设置很长的物理过期;真正的过期时间写在 value 里。

logical-expire-value.json
{
"data": {
"id": 1,
"name": "热门商品"
},
"expireAt": "2026-05-20T20:00:00+08:00"
}

请求到来时:

  • 如果逻辑时间没过期,直接返回数据。
  • 如果逻辑时间过期,当前线程尝试拿锁。
  • 拿到锁的线程异步重建缓存。
  • 当前请求先返回旧数据。
查到缓存
|
判断逻辑过期
|
已过期 -> 尝试加锁 -> 异步重建缓存
|
直接返回旧数据

它的优点是响应速度更稳,不会让大量请求等待数据库。

缺点是用户可能短时间看到旧数据,所以它更适合对实时性要求不那么高的热点数据。

缓存雪崩指的是:大量 Key 在同一时间过期,或者 Redis 服务不可用,导致大量请求同时打到数据库。

它和击穿的区别是:

  • 击穿是单个热点 Key。
  • 雪崩是大量 Key,或者缓存层整体出问题。

比如:

  • 批量导入缓存时设置了相同过期时间。
  • 活动开始前预热了一批商品缓存,过期时间完全一样。
  • Redis 宕机或网络故障。
  • Redis 集群大面积不可用。

雪崩的危害比击穿更大,因为它不是一个点,而是一片。

缓存雪崩通常要从多个层面解决。

不要让大量 Key 设置完全一样的过期时间。

random-expire.js
const baseTtl = 3600;
const randomTtl = Math.floor(Math.random() * 300);
await redis.set(cacheKey, value, 'EX', baseTtl + randomTtl);

这样可以把过期时间打散,避免某一秒大量 Key 同时失效。

可以在 Redis 前面加本地缓存,比如 Caffeine、Guava Cache 或进程内缓存。

请求 -> 本地缓存 -> Redis -> 数据库

即使 Redis 短时间抖动,本地缓存也能顶住一部分热点请求。

不过本地缓存也会带来一致性问题,所以一般只适合热点数据或允许短暂不一致的数据。

当数据库压力过大时,不能让所有请求继续堆积。

可以做:

  • 数据库访问限流。
  • 熔断降级。
  • 返回兜底数据。
  • 返回“服务器繁忙,请稍后再试”。

降级不是偷懒,而是在系统压力过大时保护核心链路。

如果雪崩原因是 Redis 宕机,就需要高可用架构。

常见方案:

  • Redis 主从复制。
  • 哨兵模式。
  • Redis Cluster。
  • 多机房或多可用区部署。

高可用解决的是缓存服务不可用的问题,过期时间随机解决的是大量 Key 同时失效的问题。两者关注点不同。

维度缓存穿透缓存击穿缓存雪崩
核心原因数据根本不存在热点 Key 过期大量 Key 同时过期或 Redis 宕机
Key 数量大量不存在 Key单个热点 Key多个或大部分 Key
数据库状态数据库里也没有数据库里有数据数据库里有数据
主要危害绕过缓存打数据库单个热点打爆数据库大量请求压垮数据库
主要方案缓存空对象、布隆过滤器互斥锁、逻辑过期过期随机、多级缓存、限流降级、高可用

记忆时可以抓住关键词:

穿透:不存在
击穿:热点
雪崩:大量

这道题可以这样回答:

缓存穿透是请求的数据在缓存和数据库中都不存在,请求会绕过缓存持续打到数据库,常用缓存空对象和布隆过滤器解决。缓存击穿是某个热点 Key 在过期瞬间被大量并发请求访问,导致请求同时打到数据库,常用互斥锁或逻辑过期解决。缓存雪崩是大量 Key 同时过期,或者 Redis 服务不可用,导致大量请求同时涌向数据库,常用过期时间加随机值、多级缓存、限流降级和 Redis 高可用解决。

如果面试官继续追问,可以补充方案取舍:

  • 缓存空对象简单,但要设置较短 TTL,避免缓存污染。
  • 布隆过滤器适合拦截大量非法请求,但存在误判为可能存在。
  • 互斥锁能保护数据库,但会增加等待时间。
  • 逻辑过期响应快,但可能返回旧数据。
  • 过期随机能打散失效时间,但不能解决 Redis 宕机。
  • Redis 高可用能提升可用性,但不能替代限流和降级。

这类 Redis 面试题的关键,是把三个概念的根因讲清楚。

缓存穿透:缓存没有,数据库也没有。
缓存击穿:缓存没有,但数据库有,而且是热点 Key。
缓存雪崩:大量缓存同时没有,或者 Redis 整体不可用。

只要先分清根因,再说解决方案,就不会混乱。

最后再补一句工程思维:缓存是为了保护数据库,但缓存系统本身也会失效。所以真正可靠的设计,通常不是只靠一个方案,而是缓存策略、限流降级、异步重建和高可用架构一起配合。