redis

6 阅读8分钟

核心架构原理

  • 纯内存操作: 内存的读写速度比硬盘快好几个数量级。Redis 绝大部分请求都在操作内存,没有磁盘 I/O 的开销。
  • 单线程模型: 这是一个“反直觉”的设计。正是因为单线程,
    • Redis 避免了多线程频繁切换上下文(Context Switch)带来的性能损耗,
    • 不用为了保护数据安全去加各种复杂的“锁”
  • I/O 多路复用: 想象一个餐厅服务员(单线程),他不会站在一桌客人面前等他们点菜,而是谁招手(有请求进来)就去处理谁。这种非阻塞的模型让它能高效处理成千上万个连接。

I/O 多路复用

在 Linux 中,“多路” 指的是多个网络连接(Socket/文件描述符),“复用” 指的是复用同一个线程。

I/O 多路复用是一种机制,让单个线程可以同时监控多个文件描述符(FD)。一旦某个描述符就绪(一般是读就绪或写就绪),它就能够通知程序进行相应的读写操作

通过使用 I/O 多路复用(主要是 epoll),Redis 能够做到:

  • 不被阻塞: 线程永远在处理已经准备好的请求,不会在某个 Socket 上干等。
  • 高并发: 单台机器可以同时维持数万个连接,而 CPU 消耗极低。
  • 事件驱动: Redis 内部有一个“文件事件处理器”,它就像那个听呼叫铃的服务员,根据不同的事件(连接、读取、写入)分发给不同的函数处理。

持久化机制

  • RDB (Redis Database): 定时给内存里的数据拍个“快照”存到硬盘。
    • 优点: 恢复大数据集时非常快。
    • 缺点: 两次快照之间的数据可能会丢失。
  • AOF (Append Only File): 把每一次写操作都记录在日志里。
    • 优点: 数据安全性极高,几乎不丢数据。
    • 缺点: 文件体积大,恢复速度慢。

高可用与扩展技术

当单机 Redis 扛不住流量或数据量时,有下面几个方式:

主从复制 (Replication)

主从复制就是将一台 Redis 服务器(主节点 Master)的数据,复制到其他的 Redis 服务器(从节点 Slave/Replica)上。主从之间会定期发送 PING(心跳检测),确保对方还活着。

在生产环境中,我们很少只用一台 Redis 机器,原因有三:

  1. 读写分离: 主节点负责写操作从节点负责读操作。因为互联网场景通常是“读多写少”,多台从节点可以极大地分担读压力。
  2. 数据备份: 除了持久化文件,主从复制实现了数据的 “多机热备”,一台机器宕机,数据在另一台机器上还有。
  3. 高可用基础: 它是后面要讲的“哨兵模式”和“集群模式”的基石。没有复制,高可用就无从谈起。

主从复制的工作原理

主从复制的过程主要分为两个场景:全量复制(第一次连接)和 增量复制(运行中断线重连)。

全量复制(Full Resync)

当一个从节点第一次连接主节点,或者主节点发现无法进行增量同步时,会触发全量复制:

  1. 同步请求: 从节点发送 psync 命令给主节点。
  2. 生成快照: 主节点执行 bgsave,在后台生成一个 RDB 文件
  3. 发送 RDB: 主节点将 RDB 文件发给从节点。
  4. 加载数据: 从节点清空旧数据,加载 RDB 文件。
  5. 发送缓冲区指令: 在生成 RDB 期间,主节点收到的新写命令会存入 replication buffer。RDB 加载完后,主节点把这部分新命令再发给从节点。
增量复制(Partial Resync)

如果主从之间网络闪断了一下,连接恢复后,没必要再跑一遍沉重的全量同步。这时会用到增量同步:

  • 偏移量 (Offset): 主从节点都会维护一个复制偏移量。
  • 积压缓冲区 (ReplBacklog): 主节点在内存中维护一个固定长度的循环队列。
  • 逻辑: 连上后,从节点报出自己的偏移量。如果这个偏移量还在主节点的缓冲区范围内,主节点只把缺失的那部分数据补发给从节点。

总结

  • 主节点(Master): 负责写,同步数据给从节点。
  • 从节点(Slave): 负责读,接受主节点的数据。
  • 全量同步: 靠 RDB 文件。
  • 增量同步: 靠偏移量和积压缓冲区。

哨兵模式 (Sentinel)

主从复制没法自动处理“老大(Master)挂了”的情况,那我们就需要一个 “自动值班经理” —— 这就是 Redis 哨兵模式(Sentinel)

哨兵(Sentinel)

哨兵(Sentinel)是一个特殊的 Redis 进程。它不存储数据,它的唯一工作就是监视你的 Redis 主从集群。

为了防止哨兵自己也挂掉,通常我们会部署一个哨兵集群(至少 3 个节点)。

哨兵的三大任务

哨兵每天的工作内容可以概括为:监控、选新的主节点、通知

  • 监控(Monitoring): 哨兵会不断地通过 PING 命令给主节点和从节点发信号,看它们是否还在正常工作。
  • 自动故障转移(Failover): 如果主节点挂了,哨兵会通过“投票”机制,从剩下的从节点里选出一个新的主节点。
  • 通知(Notification): 选出新老大后,哨兵会通知其他的从节点去跟随新老大,同时也会把新老大的地址告诉给客户端(Java/Python 程序)。
第一步:判断“主观下线”与“客观下线”
  • 主观下线(SDOWN): 一个哨兵发现主节点 PING 不通了,它会觉得“这家伙可能挂了”。
  • 客观下线(ODOWN): 这个哨兵会去问其他哨兵,如果**超过半数(Quorum)**的哨兵都觉得主节点挂了,那主节点就真的被判定为“客观下线”了。这能有效防止因为某个哨兵自己网络抖动导致的误判。
第二步:选出“领头哨兵”(Leader)

哨兵们会进行内部投票,选出一个“班长”(Leader)来执行具体的换届仪式。

第三步:选出“新国王”(New Master)

“班长”会从从节点(Slave)中按照以下顺序筛选:

  1. 优先级: 看配置里的 replica-priority,越高越优先。
  2. 复制偏移量: 谁的数据跟老大的最接近(Offset 最大),谁就优先。
  3. 运行 ID: 如果以上都一样,选 RunID 最小的(其实就是随机)。

集群模式 (Cluster)

Redis Cluster(集群模式) 登场了。它是真正的分布式解决方案,实现了数据的横向扩展

Redis Cluster 没有使用一致性哈希,而是引入了哈希槽的概念:

  • Redis 集群固定有 16384 个哈希槽。
  • 当你存入一个 Key 时,Redis 会进行如下计算: slot=CRC16(key)(mod16384)\text{slot} = \text{CRC16}(key) \pmod{16384}
  • 根据计算结果,这个 Key 会落入对应编号的槽位中。

不需要哨兵

在 Cluster 模式下,不再需要额外的哨兵集群,因为集群节点自己就能搞定故障转移。

证数据库和缓存的数据一致性

常见的策略是 Cache Aside Pattern(旁路缓存模式):

  • 写: 先更新数据库,再删除缓存。(更新缓存开销大且容易产生脏数据)
  • 读: 先读缓存,缓存没有则读数据库,然后回写缓存。

过期策略

最常见的集中策略是:

  • allkeys-lru (推荐): 淘汰最近最少使用的 Key(最通用)。
  • volatile-lru: 只在设置了过期时间的 Key 中淘汰最近最少使用的。
  • allkeys-lfu: 淘汰使用频率最低的 Key。

穿透、击穿、雪崩

缓存穿透 (Cache Penetration)

查询一个根本不存在的数据。缓存里没有,数据库里也没有。

解决方案:

  1. 布隆过滤器 (Bloom Filter): 在缓存之前加一道“安检”。将所有可能存在的 Key 哈希到一个足够大的位图中。如果布隆过滤器说“不存在”,那一定不存在,直接返回。
  2. 缓存空对象: 如果数据库返回空,依然把这个“空结果”存入 Redis,并设置一个很短的过期时间(如 5 分钟)。这样下次相同的恶意请求就能被缓存拦住。

缓存击穿 (Cache Breakdown)

某一个超热点 Key(比如微博热搜、秒杀商品)在过期的瞬间,正好有海量并发请求过来。

解决方案:

  1. 设置热点数据永不过期: 对于已知的高频热点数据,物理上不设置过期时间。
  2. 互斥锁 (Mutex Lock): 当缓存失效时,不让所有请求都去查库,而是只允许第一个请求获取分布式锁(如 SETNX),去数据库查完并回写缓存后,其他请求再从缓存读。
  3. 逻辑过期: 在 Value 里存储一个“过期时间戳”,代码判断快过期时,异步起一个线程去更新缓存,而主线程先返回旧数据。

缓存雪崩 (Cache Avalanche)

大量的 Key 在同一时间大面积过期,或者 Redis 直接宕机。