当前位置: 首页 > news >正文

Redis核心机制与实践深度解析:从持久化到分布式锁

目录

1. 持久化

1.1 RDB (Redis DataBase)

1.1.1 手动触发

1.1.1.1 相关概念

1.1.1.2 fork

1.1.2 自动触发

1.1.3 RDB 优缺点

1.1.3.1 优点

1.1.3.2 缺点

1.2 AOF (Append Only File)

1.2.1 自动触发

1.2.2 手动触发

1.2.3 AOF 重写机制

1.2.3.1 重写机制的工作原理

1.3 AOF 与 RDB 的区别

1.4 混合持久化

1.5 混合持久化 和 AOF 重写的区别

2. 事务

2.1 事物的介绍

2.2 事务操作

2.2.1 MULTI

2.2.2 EXEC

2.2.3 DISCARD

2.4.4 WATCH

2.3 事务的作用

3. 不同锁之间的联系与区别

3.1 乐观锁 (Optimistic Lock)

3.1.1 介绍

3.1.2 工作原理

3.1.3 适用场景

3.1.4 优点

3.1.5 缺点

3.2 悲观锁 (Pessimistic Lock)

3.2.1 介绍

3.2.2 工作原理

3.2.3 适用场景

3.2.4 优点

3.2.5 缺点

3.3 读写锁 (Read-Write Lock)

3.3.1 介绍

3.3.2 工作原理

3.3.3 适用场景

3.3.4优点

3.3.5 缺点

3.4 互斥锁 (Mutex Lock)

3.4.1 介绍

3.4.2 工作原理

3.4.3 适用场景

3.4.4 优点

3.4.5 缺点

3.5 公平锁 (Fair Lock)

3.5.1 介绍

3.5.2 工作原理

3.5.3 使用场景

3.5.4 优点

3.5.5 缺点

3.6 可重入锁 (Reentrant Lock)

3.6.1 介绍

3.6.2 工作原理

3.6.3 适用场景

3.6.4 优点

3.6.5 缺点

3.7 对比总结

4. 主从复制

4.1单点问题

4.2 主从模式

4.2.1 基本概念

4.2.2 工作原理

4.2.3 优势

4.2.4 总结

4.3 配置主从结构

4.3.1 配置步骤

4.3.2 TCP 长连接的优点

4.3.3 补充

4.4 拓扑

4.4.1 概念

4.4.2 一主一从结构

4.4.2.1 特点

4.4.3 一主多从结构

4.4.3.1 特点

4.4.4 树形主从结构

4.4.4.1 特点

4.5 数据同步 

4.5.1 数据同步机制

4.5.2 PSYNC

4.5.2.1 replicationid (复制ID)

4.5.2.1.1 定义

4.5.2.1.2 作用

4.5.2.1.3 关键点

4.5.2.2 offset

4.5.2.2.1 定义

4.5.2.2.2 作用

4.5.2.3 replid2 (备用复制ID)

4.5.2.3.1 定义

4.5.2.3.2 作用

4.5.2.3.3 关键点

4.5.2.4 replicationid 和 offset 组合的含义

4.5.2.4.1 定义

4.5.2.4.2  作用

4.5.2.5 PSYNC 运行流程

4.6 全量复制

4.7 部分复制

4.8 复制积压缓冲区

4.8.1 定义与核心概念

4.8.2 工作原理

4.8.3 重要性

4.9 实时复制

4.9.1 心跳机制的核心作用

4.9.2 主节点的心跳行为

4.9.3 从节点的心跳行为

4.9.4 超时处理机制

4.9.5 连接恢复过程

4. 哨兵

4.1 基本概念

4.2 故障转移过程

4.3 Redis Sentinel 的核心功能

4.4 哨兵集群

4.5 安装部署 (基于 Docker)

4.5.1 编排主从节点

4.5.2 编排哨兵节点

4.5.3 选取机制

5. 集群

5.1 集群的概念

5.2 集群的重要性

5.3 数据分片(Sharding)算法

5.3.1 分片方式

5.3.1.1 哈希求余

5.3.1.2 一致性哈希算法

5.3.1.3 哈希槽分区算法

5.4 配置集群

5.5 使用集群

5.6 故障判定

5.7 常见问题合集

6. 缓存

6.1 Redis 作为缓存

6.2 生成策略

6.2.1 定期生成

6.2.1.1 定期生成概念

6.2.1.2 实现方式

6.2.1.3 优点

6.2.1.4 缺点

6.2.2 实时生成

6.2.2.1 实时生成概念

6.2.2.2 实现方式

6.2.2.3 实时生成常见策略

6.2.2.4 优点

6.2.2.5 缺点

6.3 缓存预热

结合定期生成和实时生成的优化策略

6.4 缓存穿透

核心解决方案如下:

6.5 缓存雪崩

6.6 缓存击穿

6.7 缓存雪崩和缓存击穿的联系和区别

7. 分布式锁

7.1 分布式锁的概念

7.2 分布式锁的基础实现

7.3 过期时间

7.4 校验 id

7.5 Lua

7.6 Watch Dog

7.7 Redlock 算法


1. 持久化

持久化对于计算机相关人员来说肯定不陌生,在 MySQL 中事物的特性就曾提到了持久化。我们先来复习一下 MySQL 的事务特性:原子性一致性持久性隔离性

我们所熟知的 Redis 是在内存工作存储数据,而内存中的数据并不具有持久化。这种情况重启系统就会造成内存数据丢失,这对于企业来说肯定不想遇到。于是 Redis 就提出了两种持久化机制RDBAOF 把内存数据保存到硬盘,以便 Redis 从硬盘中恢复数据,这样 Redis 就可以避免因进程退出而造成的数据丢失问题。当然代价就是消耗更多空间,一份数据两种存储。

1.1 RDB (Redis DataBase)

RDB (Redis DataBase) 定期的把 Redis 内存中的所有数据生成快照保存硬盘,后续 Redis 重启就能凭借快照恢复内存中的数据,触发 RDB 持久化过程分为手动触发和自动触发。

1.1.1 手动触发

1.1.1.1 相关概念

save 命令:阻塞当前 Redis 服务器,直到 RDB 过程完成,对于内存比较大的实例会造成长时间阻塞,基本不采用。

bgsave 命令:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,在后台执行。阻塞只发生在 fork 阶段,一般时间很短。此处 Redis 使用的是 “多进程” 的方式 来完成的并发编程

Redis 单线程设计:Redis 使用一个主线程来顺序执行所有客户端命(如 SET、GET 等)。这中涉及避免了锁竞争和上下文切换的开销,提高了性能和简单性。

后台任务:

但某些任务耗时较长(如持久化数据到磁盘、删除大键值对),如果由主线程执行,会导致所有客户端命令被阻塞。为了解决这个问题,Redis 引入了后台任务的概念:

  • 后台指的是独立于主线程的运行环境,这些任务在 Redis 应用程序内部启动,但不占用主线程的执行时间
  • 它们允许 Redis 在单线程模型下,实现“同时”处理多个操作:主线程专注于命令处理,后台任务处理耗时操作。
  • 这不是服务器的多线程(如操作系统级线程),而是 Redis 自身管理的机制。

Redis 不光包含一个主线程:它可以创建子进程和后台线程来处理特定命令,当执行 bgsave 命令时,Redis 会判断是否已经存在其他正在工作的子进程,如果已经存在一个子进程在执行 bgsave,会拒绝新的 bgsave 命令,且新增数据不会被写入。

bgsave 命令运作流程:

RBD 以二进制的形式保存到文件中,文件名字默认 dump.rbd,当备份的时候先创建一个临时文件,当命令执行完毕之后,替换之前的 dump.rbd 

1.1.1.2 fork

fork 执行流程:

        a) 首先,父进程处于运行状态,拥有自己的 PCB、虚拟地址空间和文件描述表等资源。

        b) 当执行 fork () 系统调用时,操作系统会创建一个子进程,并复制父进程的 PCB 等元数据。如果 已经有子进程,则会直接返回 bgsave 命令。

        c) 初始状态下,父子进程共享相同的物理内存空间,这是为了提高效率,避免不必要的内存复制。

        d) 当子进程或父进程中的任何一方修改数据时,会触发写时拷贝 (Copy-on-Write) 机制。

        e) 最终,修改数据的进程会获得一份独立的数据副本,而未修改的部分仍然可以共享。

fork 创建的子进程 只针对这一瞬间的数据进行备份,只有当 子进程/父进程 其中有一个人修改数据时,才会进行物理内存上的数据拷贝,引用新的空间,也叫做 “写时拷贝”

1.1.2 自动触发

自动触发可在 Redis 的配置文件中配置,可以人为修改。RDB 因为成本高,不能执行的太频繁,就导致 实时数据快照数据会出现偏差,这也是不可调和的缺点。自动触发并不是只有到时间才能触发,也可以通过 shutdown 命令关闭服务器触发。但输入 kill 命令终结的 Redis 服务器则不会自动生成 RDB。

1.1.3 RDB 优缺点

1.1.3.1 优点

a) RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。适用于备份,全量复制等场景。

b) Redis 加载 RDB 恢复数据远远于 AOF 的方式

1.1.3.2 缺点

a) RDB 方式数据没办法做到 实时持久化,因为 bgsave 每次运行都要执行 fork 创建子进程。属于重量级操作,频繁执行成本过高。RDB 最大的问题就是不能实时的持久化保存数据,在两次快照之间,数据可能会随着重启而丢失

b) RDB 文件使用特定二进制格式保存,Redis 版本演变中有多个 RDB 版本,兼容性可能有风险。

1.2 AOF (Append Only File)

AOF 持久化:以独立日志的方式记录每次命令,重启时重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用是为了解决数据持久化的实时性。当开启 AOF 时就不会读取 RDB 文件,AOF 默认情况下是关闭的。

1.2.1 自动触发

开启 AOF 功能需要设置配置:appendonly yes,默认不开启。AOF 文件名通过 appendfilename 配置。AOF 工作流程分为:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。工作流程图如下所示:
 

AOF 会先写入一个内存缓存区,当命令累计到一定数目后,统一写入硬盘 避免频繁地 IO 操作。AOF 把每次的新操作写入到原文件的末尾,属于顺序写入操作 速度会稍快。出现缓冲区没来得及写入的数据还是会造成数据丢失,缓冲区的刷新策略可以人为修改 appendfsync 来决定刷新频率。刷新频率越高,性能影响就越明显。

AOF 文件越大,Redis 启动时间就越长。

AOF 缓冲区同步文件策略:

可配置值说明
always命令写入 aof_buf 后调用 fsync 同步,完成后返回。频率最高,数据可靠性最高,性能最低。
everysec命令写入 aof_buf 后只执行 write 操作,不进行 fsync。每秒由同步线程进行 fsync。频率稍低,数据可靠性稍低,性能稍高。
no命令写入 aof_buf 后只执行 write 操作,由 OS 控制 fsync 频率。频率最低,数据可靠性最低,性能最高。

1.2.2 手动触发

我们也可以通过调用 bgrewriteaof 命令来手动触发 aof,这里涉及的不光只有上述简单的几个步骤:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。通过 bgrewriteaof 命令触发的 aof 会引入一个 重写机制

1.2.3 AOF 重写机制

Redis 的 AOF(Append Only File)机制通过记录每个写操作命令来确保数据持久性,但长期运行后,文件中可能积累大量冗余操作(如先执行 SET key value再执行 DEL key,最终该键不存在)。为了优化存储和加载效率,Redis 引入了 AOF 重写机制(AOF Rewriting)。

1.2.3.1 重写机制的工作原理

AOF 重写的核心是创建一个新的 AOF 文件,该文件只包含重建当前数据集所需的最小命令序列,也就是优化后的命令集。重写过程不直接修改旧文档,而是生成一个临时文件,完成后原子性的替换旧文件。

工作流程图:

a) 触发重写:当执行 BGREWRITEAOF 命令时,Redis 会检查当前状态:

  • 如果已有 AOF 重写或 RDB 快照 (bgsave) 在执行,则等待完成后再启动重写(RDB 优先)。
  • 否则,启动重写过程。

b) fork 子进程:

  • 父进程 fork 出一个子进程。
  • 子进程获取 fork 瞬间的内存数据状态(这是 AOF 文件整理后 (重写后) 的最终数据,与 RDB 的二进制格式不同,AOF 使用文本格式)。

c) 生成 AOF 文件:

  • 子进程扫描内存数据,生成优化的命令序列:
    • 例如,多次修改的键(如 INCR)只记录最终值的 SET 命令。
    • 已删除或过期的键会被忽略。
  • 命令序列以 Redis 协议格式(如 * 和 $ 表示数组和字符串)写入临时文件。

d) 父进程处理 fork 后的新命令:

  • 在子进程执行期间,父进程继续处理新请求。
  • 这些新命令不会影响子进程(因为子进程有独立内存快照),而是被写入一个专门的缓冲区aof_rewrite_buf 。

e) 完成重写与原子替换:

  • 子进程完成后,通知父进程。
  • 父进程将 aof_rewrite_buf 中的命令追加到新 AOF 文件。这里的命令不是 重写 之后的命令,只是紧急追加的命令。
  • 最后,原子替换旧 AOF 文件,确保数据一致性。

关于 aof_buf 和 aof_rewrite_buf:

  • aof_buf

    • 这是 Redis 主线程在正常 AOF 持久化中使用的缓冲区,不涉及重写机制。
    • 每当主线程执行一个命令时,该命令的 AOF 格式会被追加到 aof_buf。
    • Redis 根据配置(如 appendfsync)定期或同步地将 aof_buf 内容写入磁盘的 AOF 文件。
    • 简单说:aof_buf 是日常命令记录的缓冲区,用于常规 AOF 写入。
  • aof_rewrite_buf

    • 这是专门用于 AOF 重写期间的缓冲区。
    • 在 fork 子进程后,父进程接收到的新命令不会写入 aof_buf 或旧 AOF 文件,而是写入 aof_rewrite_buf 。
    • 当子进程完成新 AOF 文件的写入后,父进程将 aof_rewrite_buf 中的命令追加到新文件,确保新文件包含 fork 后的所有变化。
    • 简单说: aof_rewrite_buf 是重写时临时存储新命令的缓冲区,仅在重写过程中使用。

1.3 AOF 与 RDB 的区别

方面AOF 重写RDB 快照
持久化目标生成等效命令日志,需包含所有写操作保存时间点数据状态
fork() 后命令必须记录以保持数据完整性不需要记录,快照已固定
缓冲区作用临时存储新命令,防止丢失无必要,写时复制隔离已足够
数据一致性依赖缓冲区确保命令不遗漏快照本身即一致状态

AOF 缓冲区是必要的,它需要捕获重写期间的所有命令来维护日志的连续性;而 RDB 的快照特性使其天然隔离 fork() 后的变化,无需额外缓冲。这体现了 Redis 设计中对不同持久化策略的优化,RDB 本身的设计理念就是用来 “定期备份”。

关键点对比

  • AOF vs RDB
    • AOF 重写生成文本格式文件(命令序列),而 RDB 生成二进制快照。
    • AOF 重写优化命令,RDB 只保存数据状态。
  • 子进程限制:子进程只处理 fork 瞬间的数据,fork 后的变化由父进程通过 aof_rewrite_buf 处理。

1.4 混合持久化

Redis 4.0 引入了 “混合持久化” 的方式,结合了 RDB 和 AOF 的特点,以优化数据持久化和恢复效率。

混合持久化流程:

a) 子进程创建

  • 父进程(主Redis进程)通过 fork() 创建子进程
  • 子进程职责:生成当前数据集的快照,但以RDB二进制格式写入新的AOF文件开头
  • 此时新AOF文件结构:[RDB 格式数据集] + [预留 AOF 追加位置]

当子进程完成RDB数据的写入时,它只完成了AOF重写过程的第一部分。此时,RDB数据集本身已经写入文件,但这并不代表整个AOF重写已经完成。父进程完成所有命令追加后,新AOF文件才被视为"重写完成"。

b) 父进程缓冲切入

  • 子进程工作期间,父进程持续接收写命令
  • 这些命令被存入 aof_rewrite_buf 缓冲区(内存缓冲区)
  • 缓冲区命令保持AOF文本协议格式,例如:
    *3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n

c) 子进程读取缓冲区

  • 子进程完成RDB部分后,会从父进程的缓冲区中读取增量命令(即重写期间发生的写操作),并以AOF格式追加到同一文件中(形成RDB + AOF的混合结构)。

d) 文件最终合成

  • 父进程执行原子替换,使用新文件以 原始 AOF 文本协议 覆盖旧文件。
  • 最终文件结构:[RDB 二进制数据] + [AOF 文本命令]

因此,新生成的 AOF 文件结构是:

  • 开头部分:RDB 格式(二进制数据)。
  • 结尾部分:AOF 格式(文本命令)。

1.5 混合持久化 和 AOF 重写的区别

机制子进程是否读取父进程缓冲区缓冲区处理方式文件格式
AOF重写父进程在重写结束后自行追加缓冲区纯AOF格式
混合持久化子进程在生成RDB后主动读取并追加RDB头部 + AOF尾部
  • AOF重写: 纯子进程主导,生成最小命令集的新AOF文件。父进程只处理增量命令的缓冲。
  • 混合持久化重写: 子进程生成RDB快照 + 追加AOF命令,父进程缓冲增量命令。子进程生成完 RDB 二进制数据,会读取父进程的缓冲区 ,并将命令 主动追加 到新的 AOF 文件。整个过程更高效,因为RDB快照加载快,而AOF部分确保数据完整性。

RDB快照替代了命令最小集的角色,因为它更高效地捕获整个数据集。

  • 原因分析:AOF重写以命令重放为核心,不需要实时同步增量,因此父进程处理追加更简单。混合持久化强调数据完整性,子进程直接读取缓冲区能减少延迟和文件不一致风险。
  • 实际影响:混合持久化在读取缓冲区时可能轻微增加子进程负载,但能提供更好的崩溃恢复能力。AOF重写则更轻量,适合纯日志场景。

2. 事务

Redis 的事务和 MySQL 的事务概念上是类似的。都是把一批操作绑定成一组,让这一组能够批量执行,但是其中的命令并不一定需要执行成功。

2.1 事物的介绍

a) 弱化原子性:Redis 中不具备 “回滚机制”,只能做到这些操作 ”批量执行“。不能做到一个命令失败就恢复到初始状态。

b) 不保证一致性:不涉及 “约束”,也没有 “回滚”。即所不保证所有操作要么全部成功,要么全部失败。

c) 不需要隔离性:没有隔离级别,因为不会并发执行事务。

d) 不需要持久性:保存在内存,是否开启持久化取决于 redis-server 

2.2 事务操作

Redis 在服务器上有 “事务队列”,每次客户端在事务中进行一个操作时,都会把命令先发送给服务器,放到 “事务队列” 中 但不会立即执行。

2.2.1 MULTI

开启一个事务,执行成功返回 OK

2.2.2 EXEC

真正执行事务

实例:

每次添加一个操作的时候,都会提示 “QUEUED”,说明命令已经进入客户端的队列了。真正执行 EXEC 的时候,客户端才会真正把上述操作发送服务器。此时才能获取到上述的 key 值

2.2.3 DISCARD

放弃当前事务

开启事务给服务器发送若干命令之后,此时服务器重启,就会丢失事务的命令,效果等同于 DISCARD。

实例:

2.4.4 WATCH

监控某个 key 是否在事务 EXEC 执行前发生改变 

WATCH 实现机制:

在 Redis 中,WATCH 命令用于实现乐观锁(Optimistic Locking),这是一种非阻塞的并发控制机制。乐观锁的核心思想是:假设事务执行期间数据不会被其他操作修改,因此不需要加锁,而是在提交事务时检查数据是否被更改。如果数据被修改,则事务失败,需要重试。

Redis 通过版本号机制来实现这一点主要分为一下几步

a) 客户端执行 WATCH key 时,Redis 会记录该 key 的当前版本号 

b) 客户端在 MULTI 命令后执行一系列操作(如 SET、GET ),但这些操作不会立即执行,而是排队等待。

c) 当客户端执行 EXEC 提交事务时,Redis 会比较 被监视 key 的当前版本号

d) 如果版本号没有改变,则 key 未被修改,事务成功执行,版本号递增。如果 版本号 已经改变,则表示 key 已经被其他客户端修改,事务失败 (返回 nil)。

2.3 事务的作用

事务的意义就是为了 “打包” 命令,避免其他客户端的命令插队。不是先抢占位置,而是让出位置,等命令齐了统一执行。开启事务时先进去队列,只有遇到执行命令时,就会把队列中的任务按顺序执行。

3. 不同锁之间的联系与区别

上述我们提到了乐观锁 和 悲观锁,那不妨我们一起复习一下 不同所之间的关联与区别。

3.1 乐观锁 (Optimistic Lock)

3.1.1 介绍

乐观锁假设冲突发生概率低,不直接加锁,而是通过版本号或时间戳机制实现。线程在操作前读取资源的版本号,操作后提交时检查版本号是否变化。如果未变化,则更新成功;否则,重试或失败。核心是“先操作,后验证”。

3.1.2 工作原理

使用原子操作(如 CAS,Compare and Swap)实现。例如,在数据库中,通过 WHERE 子句检查版本号:

3.1.3 适用场景

读多写少的场景(如缓存系统),冲突概率低时性能高;但高冲突时重试开销大,可能导致活锁。

3.1.4 优点

无阻塞,高并发性能好。

3.1.5 缺点

冲突处理复杂,需要额外机制(如重试策略)。

3.2 悲观锁 (Pessimistic Lock)

3.2.1 介绍

悲观锁假设冲突发生概率高,因此操作前直接加锁,确保资源独占。线程在访问资源前获取锁,操作后释放。核心是“先加锁,后操作”。

3.2.2 工作原理

通过互斥机制实现,如 synchronized 关键字或数据库的 SELECT FOR UPDATE。

3.2.3 适用场景

如写多读少涉及频繁修改的场景(如事务系统),高冲突时保证数据一致性;但并发性能较低。

3.2.4 优点

简单可靠,避免数据竞争。

3.2.5 缺点

阻塞其他线程,可能导致死锁或性能瓶颈。

3.3 读写锁 (Read-Write Lock)

3.3.1 介绍

读写锁区分读操作和写操作,允许多个读线程同时访问,但写线程独占资源。读锁是共享的,写锁是互斥的。

3.3.2 工作原理

基于计数器实现。当写锁被持有时,所有读锁被阻塞;当读锁被持有时,写锁被阻塞。

3.3.3 适用场景

读多写少的场景(如配置管理),提高读并发性;但写操作频繁时性能下降。

3.3.4优点

提升读性能,减少竞争。

3.3.5 缺点

写操作可能饥饿,实现复杂。

在真实系统中,乐观锁和读写锁常结合使用。例如,数据库系统可能底层用读写锁实现乐观锁的版本管理。测试冲突率是关键:如果冲突率(冲突次数/总操作次数)低于10%,乐观锁通常更优;否则,读写锁更可靠。

3.4 互斥锁 (Mutex Lock)

3.4.1 介绍

互斥锁是最基本的锁,一次只允许一个线程访问资源。其他线程必须等待锁释放。它是悲观锁的一种实现。

3.4.2 工作原理

通过操作系统或语言原语实现,如 Java 的 synchronized 或 C++ 的 std::mutex。

3.4.3 适用场景

简单互斥需求(如计数器更新),保证强一致性。

3.4.4 优点

实现简单,线程安全。

3.4.5 缺点

并发性能差,可能导致死锁。

3.5 公平锁 (Fair Lock)

3.5.1 介绍

公平锁按照线程请求锁的顺序分配锁资源,避免饥饿问题。非公平锁则允许插队,可能提高吞吐量但导致不公平。

3.5.2 工作原理

使用队列管理线程请求顺序,如Java 的 ReentrantLock(true) 实现公平锁

3.5.3 使用场景

需要公平性的场景(如任务调度),防止低优先级线程饥饿。

3.5.4 优点

公平性好,避免饥饿。

3.5.5 缺点

性能较低,因为维护队列增加开销。

3.6 可重入锁 (Reentrant Lock)

3.6.1 介绍

可重入锁允许线程多次获取同一把锁(递归锁)。线程在持有锁时,可以再次进入同步代码块,而不被阻塞。

3.6.2 工作原理

通过计数器记录重入次数。每次 lock() 增加计数,unlock() 减少计数,计数为零时释放锁。Java 的 ReentrantLock 和 synchronized 都是可重入的。

3.6.3 适用场景

递归方法或嵌套同步块(如树遍历),避免自死锁。

3.6.4 优点

灵活,支持复杂同步逻辑。

3.6.5 缺点

不当使用可能导致锁泄露(未完全释放)。

3.7 对比总结

锁类型核心特点适用场景优点缺点
乐观锁无锁机制,基于版本号验证读多写少,低冲突(如缓存)高并发,无阻塞高冲突时重试开销大
悲观锁操作前加锁,资源独占写多读少,高冲突(如事务)简单可靠,数据一致性强性能低,可能死锁
读写锁读共享,写互斥读多写少(如配置管理)提升读并发性能写操作可能饥饿
互斥锁基本互斥,一次一线程简单互斥需求(如计数器)实现简单,强一致性并发性能差
公平锁按请求顺序分配锁需要公平性(如任务队列)避免饥饿,公平性好性能较低,维护开销大
可重入锁允许同一线程多次获取锁递归或嵌套同步(如算法)灵活,避免自死锁可能锁泄露

综合建议

  • 选择锁时,考虑冲突概率:低冲突用乐观锁,高冲突用悲观锁。
  • 考虑操作类型:读多写少用读写锁,写多用互斥锁。
  • 考虑公平性:要求顺序执行用公平锁,否则用非公平锁提高吞吐量。
  • 考虑代码结构:递归逻辑用可重入锁。
  • 在 Java 中,ReentrantLock 可配置为公平/非公平,并支持可重入,是通用选择;但 synchronized 更轻量。

4. 主从复制

主从复制的目标是复制内存中的当前数据状态,而不是硬盘上的所有文件(如旧的 RDB 文件、AOF 文件或系统文件)。复制硬盘所有数据不仅效率低下(可能包含历史或不相关数据),还会破坏 Redis 的实时性。

这个目标的实现就会和后面要提到的集群的目标会有差异。

4.1单点问题

在分布式系统中,单点问题(Single Point of Failure, SPOF)是一个关键挑战,它指的是系统中存在一个关键组件(如服务器或节点),如果该组件发生故障,会导致整个系统或部分功能完全失效。这违背了分布式系统设计的高可用性和容错性目标。

为了解决这个问题,Redis 集群通过主从复制机制来实现多服务器部署,此时这个集群能给整个分布式系统提供 更稳定/高效 的服务,如数据存储功能,从而提升系统的稳定性、性能和并发处理能力。可用性、性能、并发量、数据可靠性都会增强。

4.2 主从模式

4.2.1 基本概念

  • 主节点(Master):负责处理所有写操作(如SET、DEL命令),并将数据修改同步到从节点。主节点是数据的权威来源。
  • 从节点(Slave):作为主节点的副本,只处理读操作(如GET命令)。从节点通过复制机制从主节点获取数据,但不能修改数据。每个从节点都是主节点的完整拷贝。
  • 数据复制:主节点对数据的任何修改(例如,更新一个键值对)都会实时同步到所有从节点。这确保了数据的一致性。

需要注意的是:从节点断开之后不会删除之前同步的数据,

4.2.2 工作原理

  • 复制过程
    • 当从节点启动时,它会连接到主节点,并发送一个SYNC命令。
    • 主节点响应后,会生成一个RDB快照文件(内存数据的快照),并将快照发送给从节点。
    • 从节点加载快照后,主节点会持续发送新的写命令(通过AOF或复制缓冲区),确保从节点数据实时更新。
    • 这个过程是异步的,意味着主节点在写入数据后不会等待从节点确认,这可能导致短暂的数据延迟(通常在毫秒级)。

后续会更细节的解释复制过程

  • 读写分离
    • 客户端可以将读请求分发到多个从节点,而写请求只发送到主节点。
    • 例如,在一个应用中,读操作可以路由到从节点,写操作路由到主节点,从而分散负载。
    • 优势:通过增加从节点,系统能处理更多并发读请求。例如,如果主节点每秒处理1000读请求,添加3个从节点后,理论上系统读并发量可提升至4000(假设负载均衡)。

4.2.3 优势

并发量和可用性提升

  • 分担主节点压力
    • 读操作占数据库负载的大部分(例如,在Web应用中,读请求可能占80%以上)。通过将读请求转移到从节点,主节点专注于写操作,避免了瓶颈。
    • 结果:系统整体并发量大幅提高,支持更多用户同时访问。
  • 提高可用性
    • 故障隔离:如果某个从节点故障,客户端可以切换到其他从节点或主节点读取数据(但主节点只能处理写请求)。
    • 高可用架构:只有所有节点(主节点和所有从节点)同时故障时,服务才会完全停止。建议在不同机房部署节点(例如,主节点在机房A,从节点在机房B和C),以减少单点故障风险。这样,即使一个机房宕机,服务仍能部分运行。
    • 统计上,部署多个节点能显著降低系统宕机概率。例如,单个节点可用性为99.9%,但部署3个节点后,系统可用性可接近99.999%(即每年宕机时间少于5分钟)。

4.2.4 总结

Redis主从模式通过读写分离和数据复制,有效提升了系统的读并发量和可用性。主节点处理写操作,从节点分担读负载,故障时能提供部分服务。但需注意,主节点故障会影响写操作,因此建议结合Sentinel或Cluster实现自动故障转移。

主从复制常作为集群的子组件,用于数据同步,而集群提供更全面的分布式框架。两者结合能有效提升系统的可靠性和性能。

4.3 配置主从结构

可以通过操作 配置文件 在一台云服务器上运行多个 redis 服务器

4.3.1 配置步骤

a) 复制两份 redis.conf 并且修改 port 和 daemonize 。

b) 使用 redis-server 启动服务器,并使用ps aux | grep redis 查看后台运行情况.

c) 以上我们配置完三个 redis 服务器,想要构成 主从结构 需要我们进行进一步的配置,以下有三种方法配置主从结构:

        (1) 在配置文件中加入 slaveof {masterHost} {masterPort} 随 Redis 启动生效

        (2) 在 redis-server 启动命令时加入 --slaveof {masterHost} {masterPort} 生效,缺点是每次重启都需要输入。

        (3) 直接使用 redis 命令:slaveof {masterHost} {masterPort} 生效,缺点是每次重启都需要输入。

这里演示在配置文件中修改相关数据:

        配置完之后我们可以输入 service redis-server start/stop 来重启 6379 端口的 redis 服务器

输入 redis-cli -p 端口号 shutdown ,可以让指定端口号的 Redis 服务器关闭;可以通过 redis-server xxx 来启动不同的 Redis 服务器;可以输入 info replication 来获取更详细的信息。从节点启动之后就会和主节点建立 Tcp 长连接

当我们需要接触 该从节点 所属关系时,可以在客户端输入 salve no one 来解除。不过要注意的是,salve no one 只能接解除当前状态,重启 Redis 服务器还是会和配置文件里设置的关系一样。

4.3.2 TCP 长连接的优点

  • 减少延迟:避免每次通信的握手开销,提高响应速度。
  • 提高吞吐量:连接复用允许更高频率的数据交换。
  • 增强可靠性:通过心跳机制(如定期发送空数据包)检测连接状态,及时处理故障。
  • 资源优化:减少频繁创建和销毁连接的系统资源消耗。
  • 节省网络带宽:TCP 内部支持 nagle 算法,其主要目的是减少网络中小数据包的数量,从而降低网络拥塞和提高传输效率。Nagle算法通过智能地缓存和合并小数据包来解决 小数据包的数量过多 这一问题。

4.3.3 补充

a) 数据节点安全验证配置 

主节点需配置 requirepass 参数设置访问密码,客户端必须使用 auth 命令完成验证。从节点需同步配置 masterauth 参数,且值与主节点密码一直,确保复制连接能通过验证

b) 从节点只读模式规范

从节点默认启用 slave-read-only=yes,强制只读以防止数据不一致。任何对从节点的直接写入均会导致主从数据差异,生产环境必须保持改配置不可更改。

c) 网络传输延迟优化

通过 repl-disable-tcp-nodelay 参数控制 TCP 行为:

  • 设为 no (默认) :禁用Nagle算法,实时发送数据包,降低复制延迟但增加带宽消耗,适合低延迟网络环境(如同机房)。
  • 设为 yes :启用TCP包合并,每40ms(依赖内核配置)发送一次,减少带宽占用但增加延迟,适用于高延迟网络(如跨机房)。

4.4 拓扑

4.4.1 概念

拓扑指的是 Redis 数据库的部署和连接方式,包括单节点、主从复制、哨兵模式、集群模式等。Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构。

4.4.2 一主一从结构

4.4.2.1 特点
  • 主节点(Master)负责写操作,从节点(Slave)复制主节点数据并提供读服务。
  • 主节点宕机时,从节点可手动或自动(需配合哨兵)提升为新主节点,实现故障转移。

4.4.3 一主多从结构

4.4.3.1 特点
  • 单个主节点挂载多个从节点,从节点分担读请求压力。
  • 主节点需向所有从节点发送同步数据,写并发高时可能成为瓶颈。

4.4.4 树形主从结构

4.4.4.1 特点
  • 从节点可级联复制,例如:主节点A同步到B和C,B再同步到D和E。
  • 中间层从节点(如B)既作为主节点的Slave,又作为下层节点的Master(即使该从节点有了一个从节点,但是该从节点的身份还是从节点)。
  • 当层级过多时,主从之间复制需求的时间就会过长 

4.5 数据同步 

主从节点建立复制流程图:
 

1) 从节点只保存主节点地址信息

2) 从节点尝试与主节点建立基于 TCP 的网络连接,从节点内部会每秒运行定时任务去寻找新的主节点,当从节点无法与主节点建立连接时,定时任务会无限重试直到连接成功或用户停止主从复制。

3) 建立连接成功之后,发送 ping 命令,如果 ping 命令的结果 pong 回复超时,从节点会断开 TCP 连接,等待定时任务下次重新建立连接

4) 如果主节点设置了 requirepass 参数,从节点需要密码验证。从节点通过配置 masteruath 参数来设置密码,如果验证失败 则从节点的复制将会停止。

5) 对于首次复制的场景,主节点会把当前所有的数据全部发送给从节点,这步也被称为全量复制。

6) 当从节点复制主节点所有数据之后,针对新的修改命令持续复制,主节点会持续把命令发送给从节点,保证主从数据一致性,这步也被称为部分复制。

4.5.1 数据同步机制

Redis使用 psync 命令实现主从数据同步,优化了效率和可靠性。同步过程分为全量复制和部分复制,通过复制偏移量和运行ID管理。

  • 全量复制
    适用于初次复制场景。主节点将全部数据一次性发送给从节点。当数据量较大时(例如GB级),会对网络和节点资源造成高开销。
    如果主节点没有匹配的 replicationid 或 offset 不在缓冲区(offset)范围内,或者主节点负载过高(自顾不暇),则返回全量数据(RDB文件)。

  • 部分复制
    用于处理网络闪断等中断后的数据恢复。当从节点重新连接主节点时,如果条件允许(如有匹配主节点的 replicationid 并且复制偏移量在缓冲区(offset)范围内),主节点仅补发丢失的数据。这有效降低了开销,提升了容错性。部分复制依赖复制积压缓冲区(replication backlog),其大小可配置。
     

PSYNC 是 Redis 用于数据同步的命令,它允许从节点高效地从主节点获取数据更新。核心目标是实现“部分同步”(partial synchronization),如果条件允许,Redis 只发送增量数据(即自上次同步以来的变化),而不是全量数据。这显著减少了网络开销和同步时间。

如果部分同步不可行(例如,从节点是首次连接或复制历史丢失),则自动回退到“全量同步”(full synchronization),即主节点发送整个数据集的快照(通常是一个 RDB 文件)。从节点负责发起 PSYNC,并从主节点获取数据。

4.5.2 PSYNC

语法:PSYNC replicationid offset

在 Redis 的复制系统中 replicationid、replid2、offset 和 PSYNC 命令是核心概念,用于管理主从节点之间的数据同步。这些机制确保数据一致性、故障恢复和高效复制。

4.5.2.1 replicationid (复制ID)
4.5.2.1.1 定义

replicationid 是主节点在启动时生成的唯一标识符(UUID格式),每次主节点重启都会生成一个新的值。这确保了每个主节点在集群中具有独特的身份。

4.5.2.1.2 作用

用于区分不同主节点。当从节点与主节点建立复制关系时,它会从主节点获取这个 replicationid,从而标识数据来源。

4.5.2.1.3 关键点

如果主节点重启,replicationid 会改变,这有助于集群检测节点状态变化。

4.5.2.2 offset
4.5.2.2.1 定义

offset 是一个累加计数器,主节点每处理一个操作命令(如 SET 或 DEL),都会计算该命令占用的字节大小,并将大小累加到 offset 中。

4.5.2.2.2 作用
  • 在主节点上,offset 表示当前已处理的数据总量。
  • 在从节点上,offset 表示已从主节点同步的数据进度(即同步到哪个位置)。

4.5.2.3 replid2 (备用复制ID)
4.5.2.3.1 定义

replid2 是当从节点晋升为主节点时记录的字段,它保存了之前主节点的 replicationid

4.5.2.3.2 作用
  • 在故障转移场景中(如原主节点宕机,从节点接管为主节点),新主节点会设置自己的 replicationid,同时将原主节点的 replicationid 存储到 replid2。
  • 这允许新主节点在后续恢复中,识别原主节点并尝试重新同步,避免数据冲突。
4.5.2.3.3 关键点

replid2 主要用于故障恢复,确保复制历史的连续性。

4.5.2.4 replicationid 和 offset 组合的含义
4.5.2.4.1 定义

replicationid 和 offset 共同描述了一个数据集合的状态。

  • replicationid 标识数据来源(哪个主节点)。
  • offset 标识数据进度(同步到哪个点)。
4.5.2.4.2  作

如果两个节点的 replicationid 和 offset 完全相同,则表示它们存储的数据完全一致。这用于验证数据一致性,例如在切换主节点或检查复制进度时。

4.5.2.5 PSYNC 运行流程

主节点根据收到的 replid 和 offset 参数,结合自身数据状态(如复制积压缓冲区),决定响应结果。响应分为三种情况:

  • 响应 +FULLRESYNC <replid> <offset>

    • 表示主节点要求从节点进行 全量复制(完整数据同步)。
    • 触发条件:主节点无法提供部分同步,例如:
      • 从节点的 replid 与主节点不匹配(表示从节点数据过旧)。
      • 从节点的 offset 不在主节点的复制积压缓冲区范围内。
    • 从节点收到此响应后,会启动全量复制流程:主节点生成 RDB 快照并发送给从节点,从节点加载数据。
  • 响应 +CONTINUE

    • 表示主节点允许从节点进行 部分复制(增量数据同步)。
    • 触发条件:主节点能提供部分同步,例如:
      • 从节点的  replid  匹配主节点的当前复制 ID。
      • 从节点的 offset 在主节点的复制积压缓冲区中(表示有部分数据可同步)。
    • 从节点收到此响应后,会启动部分复制流程:主节点发送从 offset 开始的增量数据。
  • 响应 -ERR

    • 表示主节点 不支持 PSYNC 命令
    • 触发条件:主节点版本低于 Redis 2.8(PSYNC 在 Redis 2.8 引入)。
    • 从节点收到此响应后,应降级使用 SYNC 命令进行全量复制(例如,发送 SYNC 命令)。
  • PSYNC 的执行方式

    • PSYNC 一般 不需要手动执行。Redis 在主从复制模式下自动调用此命令(例如,从节点启动时或断线重连时)。
    • 优势:PSYNC 是非阻塞的,主节点在处理 PSYNC 时能继续服务其他请求,避免性能下降。

4.6 全量复制

全量复制流程图:

1) 从节点发送 psync 命令给主节点进行数据同步,因为是第一次进行复制,从节点没有主节点的运行 ID 和复制偏移量,所以发送 psync ? -1.

2) 主节点根据命令,解析出要进行全量复制,回复 + FULLRESYNC 响应

3) 从节点接收主节点的运行信息进行保存

4) 主节点执行 bgsave 进行 RDB 文件的持久化

5) 主节点发送 RDB 文件给从节点,从节点保存 RDB 数据到本地硬盘。

6) 主节点将从生成 RDB 到接收完成期间执行的写命令,写入到缓冲区内,等从节点保存完 RDB 文件后,主节点再将缓冲区内的数据补发给从节点。补发的数据仍按照 RDB 的二进制格式追加写入到新的 RDB 文件中

7) 从节点清空自身原有的旧数据

8) 从节点加载 RDB 文件得到与主节点一致的数据

9) 如果从节点加载 RDB 完成之后,并且开启了 AOF 持久化功能,就会进行 bgwrite 操作,得到最近的 AOF  文件。

在Redis复制机制中,全量复制(full synchronization)用于从节点首次连接或需要完全同步数据时,主节点会生成RDB(Redis Database)快照。默认情况下,这个过程涉及磁盘操作,但从Redis 2.8.18版本开始,引入了无磁盘复制选项,以优化性能。主节点在执行 RDB 生成流程时,不会生成 RDB 文件到磁盘中,而是直接把生成的 RDB 数据通过网络发送给从节点。

特性有磁盘复制无磁盘复制(diskless)
磁盘IO高(需写入和读取磁盘文件)低(无磁盘操作)
网络负载中等(文件传输)较高(数据流直接传输)
延迟较高(额外磁盘步骤)较低(直接传输)
内存占用较低(数据写入磁盘后释放)较高(传输期间数据驻留内存)
可靠性高(磁盘文件可重试)中等(依赖网络稳定性)
适用场景磁盘性能好、网络稳定环境磁盘性能差、网络带宽高环境

4.7 部分复制

部分复制流程图:

1) 当主从节点之间出现网络中断时,如果超过 repl-timeout 时间,主节点会认为该从节点故障并终断复制链接

2) 主从连接中断期间,主节点依然在相应命令,并且把命令滞留在复制积压缓冲区中。

3) 主从节点恢复连接

4) 从节点将之前保存的 replicationid 和 offset 作为 psync 的参数发送给主节点,请求部分复制。

5) 主节点接到 psync 命令后进行验证,根据 offset 来判断落下数据超没超出 复制积压缓冲区,没超出响应 + CONTINUE 给从节点

6) 主节点将需要从节点同步的数据发送给从节点,最终完成一致性。

4.8 复制积压缓冲区

4.8.1 定义与核心概念

  • 复制积压缓冲区是一个固定大小的循环缓冲区(circular buffer),它存储主节点最近执行的数据更改操作(如写命令或事务日志)。这些操作以日志形式记录,从节点通过读取缓冲区来同步数据。
  • 缓冲区大小通常可配置,例如在 Redis 中,参数 repl-backlog-size定义了其容量,如默认值 $1\text{MB}$。这表示缓冲区能存储的操作数量取决于日志条目的大小和写入速率。
  • 关键目标:容忍从节点的短暂断开,避免数据丢失或全量同步的开销。

4.8.2 工作原理

  • 写入阶段:主节点接收写操作时,将操作日志追加到缓冲区中。缓冲区采用先进先出(FIFO)机制,新日志覆盖旧日志当缓冲区满时。
  • 同步阶段:从节点连接主节点后,发送自己的复制偏移量(replication offset)。主节点比较偏移量,从缓冲区中提取未同步的日志发送给从节点。
  • 追赶机制:如果从节点的偏移量在缓冲区内,则进行增量同步;否则,需要全量同步(如重新传输整个数据集)。
  • 数学表示:缓冲区大小 $S$ 和写入速率 $R$ 决定了最大容忍断开时间 $T_{\text{max}}$: $$ T_{\text{max}} = \frac{S}{R} $$ 其中 $R$ 是单位时间内的日志写入量(如每秒操作数),$S$ 以字节为单位。这确保在 $T_{\text{max}}$ 内断开的从节点能恢复。

4.8.3 重要性

  • 数据一致性:防止从节点因短暂故障导致数据落后,确保所有副本最终一致。
  • 性能优化:减少全量同步的频率,降低网络带宽和 CPU 开销。例如,在高峰期,缓冲区能处理短暂网络抖动。
  • 高可用性:支持自动故障转移;如果主节点故障,从节点可快速提升为新主节点,因为它已通过缓冲区同步了最新数据。
  • 可配置性:管理员可根据系统负载调整缓冲区大小,权衡内存使用和容错能力。

4.9 实时复制

主从节点建立连接后,通过 TCP 长连接的方式给从节点持续发送 收到的修改操作,保证主从一致性,这样的 TCP 长连接需要通过心跳包来确认连接状态。心跳机制这种机制由应用层实现(而非依赖 TCP 自带的心跳),确保主从节点能及时响应通信状态变化。

4.9.1 心跳机制的核心作用

  • 心跳机制的主要目的是检测连接是否存活,防止因网络问题导致的数据同步失败。
  • 通过定期发送轻量级命令,节点可以:
    • 监控对方节点的存活状态。
    • 上报关键数据(如复制偏移量),确保复制进度一致。
    • 如果通信延迟超过阈值,自动断开并重连,避免资源浪费。

4.9.2 主节点的心跳行为

  • 主节点模拟从节点的客户端,定期发送心跳命令以检测从节点状态。
  • 默认每隔 $10$ 秒,主节点向从节点发送 ping 命令:
    • ping 是一个简单的探测包,用于确认从节点是否在线和响应。
    • 如果从节点在指定时间内回复,主节点认为连接正常。
    • 如果从节点未回复,主节点会记录延迟并等待后续检测。

4.9.3 从节点的心跳行为

  • 从节点模拟主节点的客户端,主动上报自身状态以辅助主节点监控。
  • 默认每隔 $1$ 秒,从节点向主节点发送 replconf ack {offset} 命令:
    • 其中,$offset$ 表示从节点当前的复制偏移量(即已处理的操作日志位置)。
    • 此命令有两个作用:
      • 作为心跳包,证明从节点存活。
      • 上报复制进度,帮助主节点判断数据同步是否滞后。
    • 偏移量上报频率较高(每秒一次),确保主节点能快速检测延迟。

4.9.4 超时处理机制

主节点持续监控从节点的响应时间:

  • 如果从节点的通信延迟超过配置的 repl-timeout 值(默认 $60$ 秒),主节点判定从节点下线。
  • 判定逻辑:主节点计算从上次收到 replconf ack 或 ping 回复的时间差,如果超过 $60$ 秒,则触发超时。
  • 超时后,主节点主动断开与从节点的 TCP 连接,释放资源。

4.9.5 连接恢复过程

  • 一旦连接断开,从节点会尝试重新建立连接:
    • 从节点检测到连接丢失后,自动重新发起 TCP 连接请求。
    • 重新连接成功后,心跳机制立即恢复:主节点继续每 $10$ 秒发送 ping,从节点每 $1$ 秒发送 replconf ack
    • 系统会从断点处继续复制,利用偏移量 $offset$ 确保数据不丢失(例如,主节点重传未确认的操作)。
  • 注意事项:
    • 心跳间隔和超时值可配置,以适应不同网络环境(如在高延迟网络中增加超时阈值)。
    • 频繁心跳(如每秒一次)可能增加网络负载,但确保了高实时性;在稳定环境中,可适当调整以优化性能。

4. 哨兵

在 Redis 的主从复制模式下,主节点故障时需要人工干预进行主从切换,客户端也需手动更新配置以连接到新主节点。这种方案在大规模应用中效率低下,容易导致服务中断。为解决这一问题,Redis 从 2.8 版本引入了 Redis Sentinel(哨兵)机制,实现自动化故障转移和高可用性。

replicationid:主要在主从复制中起作用

runid:主要用在支撑实现 redis 哨兵这个功能,和主从复制没什么关系

4.1 基本概念

Redis Sentinel 相关名词解释:

名词逻辑结构物理结构
主节点Redis 主服务一个独立的 redis-server 进程
从节点Redis 从服务一个独立的 redis-server 进程
Redis 数据节点主从节点主节点和从节点的进程
哨兵节点监控 Redis 数据节点的节点一个独立的 redis-sentinel 进程
哨兵节点集合若干哨兵节点的抽象组合若干 redis-sentinel 进程
Redis 哨兵(Sentinel)Redis 提供的高可用方案哨兵节点集合和 Redis 主从节点
应用方泛指一个多多个客户端一个或多个连接 Redis 的进程

Redis哨兵机制涉及多个角色:Redis 数据节点、哨兵节点集合,以便共同协作确保系统稳定性。

哨兵节点不负责存储数据,所以对比 redis 节点来说配置不需要太好。哨兵+主从复制解决的问题是“提高可用性”,不能解决“数据极端情况下写丢失”的问题,也不能提高数据的存储容量。

这些进程之间会建立 TCP 长连接,通过长连接定期发送心跳包,来监控数据节点集合。

4.2 故障转移过程

        a) 检测故障:哨兵节点集合通过心跳检测发现主节点不可用(例如,超时未响应),并确认故障(需多数哨兵达成共识)。

        b) 选取 leader 节点:哨兵内部使用 Raft 算法选出一个 leader 节点,拉票数超过哨兵节点的一半时则成功,负责接下来的主节点选取任务

        c) 选举新主节点

  • 哨兵从可用的从节点中选择一个作为新主节点(基于优先级、复制偏移量等规则)。
  • 执行命令使该从节点成为主节点:slaveof no one(解除从属关系)。
  • 优先级:在配置文件中, 每一个 redis 节点都会有一个优先级设置,优先级高的从节点就会被选取。

  • offset:如果没有设置优先级,则会根据 offset 的大小来设置,数值越大证明 从节点从主节点数据越相似,越能称为新的主节点。

  • run id:如果每个 redis 节点的优先级和 offset 都一样的情况下,选取 run id 小的作为主节点。

        d) 重新配置从节点:其他从节点连接到新主节点:执行 slaveof <新主节点 IP 端口> 命令,开始复制新主节点的数据。

        e) 通知客户端

  • 哨兵更新配置信息,并通过发布-订阅机制通知客户端连接到新主节点。
  • 客户端自动重定向,确保应用无缝切换。

        f)  旧主节点恢复

  • 如果故障的主节点被修复并重新上线,哨兵会将其作为新从节点加入系统:执行 slaveof <新主节点 IP 端口> 命令,使其开始复制新主节点的数据。
  • 这样,系统恢复到正常状态,旧主节点不再承担主角色,而是作为备份。

4.3 Redis Sentinel 的核心功能

Sentinel 是一个分布式系统,由多个 Sentinel 节点组成,主要职责包括:

        a) 监控:持续检查主节点和从节点的健康状态(如心跳检测)。

        b) 通知:当检测到主节点故障时,向管理员发送警报。

        c) 故障转移:自动选举新的主节点,并更新从节点配置。

        d) 客户端重定向:通知客户端新的主节点地址,无需手动干预。

4.4 哨兵集群

在分布式系统中,哨兵节点(sentinels)用于监控主节点的状态,并在故障时触发故障转移。如果只有一个哨兵节点,它可能成为单点故障:一旦该节点宕机或错误判断主节点状态(例如误判主节点宕机),就会导致系统无法正确执行故障转移,引发服务中断或数据不一致。

因此,引入哨兵集群是必要的,通过多个节点共同决策来提高可靠性和容错性。

哨兵集群中哨兵的个数最好为奇数

4.5 安装部署 (基于 Docker)

Docker 是一种开源容器化平台,主要用于简化应用程序的开发、部署和运行过程。它通过容器技术,将应用程序及其所有依赖项(如库、环境变量、配置文件等)打包到一个轻量级、可移植的单元中。

Docker 的作用带来多个关键好处:

  • 环境一致性:容器在任何支持 Docker 的系统上运行相同,无论是开发、测试还是生产环境,减少了配置错误。
  • 快速部署和扩展:容器启动速度快(通常在秒级),支持自动化部署工具(如 Kubernetes),便于水平扩展。
  • 资源高效:相比虚拟机(VM),容器共享主机操作系统内核,占用更少内存和 CPU,提高资源利用率。
  • 简化开发流程:开发人员可以专注于代码,而不需担心底层基础设施;支持微服务架构,每个服务可独立容器化。
  • 可移植性:容器镜像可以轻松共享(通过 Docker Hub 等仓库),支持跨平台(Linux、Windows、macOS)。

在 Docker 中,“镜像和容器” 类似于 “可执行程序和进程” 的关系。镜像可以自己构建,也可以从 Docker hub 上获取。

由于 Docker 网址在国外,我们可能获取不到,可以使用国内的一些镜像源来获取 。

比如 docker pull docker.1ms.run/redis:5.0.9 在国内就能轻松获取 Redis 镜像。

Docker 容器不是虚拟机(Virtual Machine)。Docker 是一种轻量级的容器化技术,它直接在宿主机(Host)上运行隔离的进程(称为容器),而不是通过虚拟机。端口映射是 Docker 的核心功能之一,它允许将宿主机的端口号映射到容器的端口号,从而实现网络访问。

4.5.1 编排主从节点

Docker 编排主从节点,可以通过编写 docker-compose.yml 文件来配置相关信息,编辑完成后可以使用 docker-compose up -d 命令行来启动所有容器。

如果这里拉取不到 可以通过配置 Docker 的镜像,源配置文件为/etc/docker/daemon.json 来添加国内镜像源

4.5.2 编排哨兵节点

Docker 编排哨兵节点,也可以通过编写 docker-compose.yml 来配置,注意每个目录中只能存在一个 docker-compose.yml,所以我们需要把哨兵节点和主从节点建立在两个不同文件夹下。

默认情况下,哨兵节点和 redis-server 节点 不是同一个局域网,我们可以通过配置文件的方式,来在启动时配置。哨兵节点的 name 需要和 redis 的三个节点的 name 相同,name 也就是 Docker 网络(逻辑上的局域网)  要相同。

networks:default:external:name: xxxxx

4.5.3 选取机制

我们可以通过 docker ps -a 来查看节点状态

部署完 docker 节点之后可以通过 docker stop redis-master 命令,手动停止 redis 主节点 来 观察 哨兵节点的运行机制。当我们停止 master 节点后,通过 docker-compose logs 来查看哨兵节点工作的流程细节。

Sdown (Subjectively Down) 主观下线:当前哨兵节点,认为主节点下线

Odown (Objectively Down) 客观下线:多个哨兵同时认为该 主节点 下线

vote-for-leader:选取哨兵的 leader 节点

switch-master :主节点切换

5. 集群

5.1 集群的概念

广义的集群:多个机器构成分布式系统,可以被称为是一个“集群”

Redis集群的设计目的是:

  • 解决单机内存不足问题:当数据量接近物理内存上限时,通过将数据分布到多个节点(主节点)来扩展存储能力。
  • 提高系统可用性:每个主节点配备从节点(replica),实现故障转移和数据冗余,确保服务不间断。

5.2 集群的重要性

单个Redis节点的内存有限(受硬件限制)。如果数据量超过节点内存,性能会下降(如频繁交换到磁盘),集群通过分片将数据分散到多个节点来提升总内存容量:例如,10个节点各16GB内存,总容量可达160GB。

5.3 数据分片(Sharding)算法

Redis集群使用分片(sharding)技术将数据分布到多个主节点。每个红框部分都可以成为是一个 分片(Sharding),当我们数据越来越多的时候,创建了多个分片时,就需要通过数据分片算法来进行分配。

5.3.1 分片方式

5.3.1.1 哈希求余

借助 hash 函数,把 key 映射到整数,再针对分片的个数求余,根据下标分配各给不同的分片。 后续查询 key 时,也是相同算法来获得 key。 

例如 md5 算法就是计算 hash 值,然后转换成不同进制的数字。md5 计算结果具有定长,分散、理论上结果不可逆 的特点

当分片进行扩容时,某个数据进行计算之后不在原来的分片中,就需要重新分配(搬运数据)。

当数据量庞大时,上述提到的重新分配开销极大,往往只能通过“替换”的方式来实现扩容。

替换步骤概述:

a) 用额外的机器先确定好要扩容的分片个数

b) 把原机器的分片重新分配到 新机器的分片中

c) 使用新机器分配好的分片 替换 原机器中的分片

5.3.1.2 一致性哈希算法

为了降低上述的搬运开销,就需要要引入 一致性哈希算法。本质上是把数据的交替存储,优化为了连续存储。当我们对分片进行扩容时,只需要搬运部分数据即可。

计算步骤概述:

a) 把数据空间映射到 圆环 上,数据按照顺时针方向增长

b) 把分片放在圆环的不同位置上

c) 存储 key 时,按照顺指针往下找,找到的第一个分片就是该 key 所属的分片

d) 相当于 N 个分片,把圆环分成了 N 个区域,key 的 hash 值落在某个区间内,就归对应的区间管理

优点:大大降低了扩容时数据搬运的规模,提高了扩容操作的效率。

缺点:数据分配不均

5.3.1.3 哈希槽分区算法

Redis Cluster 使用哈希槽(hash slot)将数据划分为 16384 个固定槽位。每个键通过 CRC16 算法计算哈希值,再取模 16384 分配到特定的槽位。槽位是逻辑分区,本身不存储数据,而是指向具体的 Redis 节点。

Rdis 通过 hash_slot = crc16(key) % 16384 计算 key 的值,把槽位近似且均匀的分配给不同的分片。本质就是把一致性哈希和哈希求余结合。可以通过 “位图” 来表示当前有多少槽位,每一位用 0/1 来区分是否存储数据,哈希槽正是 redis 采用的分片算法。

当进行分片扩容时,可以把之前每个分片都剪切一部分出来分配给新的切片。当涉及到分片搬运时,也只需要部分搬运。

关键点:

a)槽位总数不变,意味着系统没有新增逻辑分区,而是通过迁移现有槽位来利用新节点的物理资源。总槽位数量固定(例如$N = 16384$),这确保了数据分布的均匀性和一致性哈希的稳定性。

b)在通信中,心跳包包含了 哈希槽 的数据情况,因为网络通信需要频繁发送心跳包,$N 过大则会给 网络带宽 造成巨大的压力。

c)哈希槽不会“满”----每个槽位是一个逻辑分区(固定16384个),可以存储任意数量的键值对(仅受 Redis 节点内存限制)

以下举例了两种拓展情况:

  • 情况1:单个节点内存不足
    如果某个节点管理的槽位数据过多,导致节点内存接近上限(如16GB内存用了15GB),这时:添加新节点到集群,并使用redis-cli --cluster reshard命令重新分配槽位。例如,将原节点的一部分槽位迁移到新节点。迁移过程中,数据会自动同步,服务基本无中断。

  • 情况2:集群整体数据量过大,但槽位固定。即使槽位固定为16384,集群也能处理海量数据,如果所有节点内存总和不足,只需添加更多节点并重新分配槽位。

5.4 配置集群

可以通过 Shell 脚本生成多级目录 的同时创建多个 docker 容器来配置多个 redis 节点,再通过 docker-compose.yml 文件创建 redis 节点 

Shell 相关命令说明:

cluster-enabled yes 开启集群

cluster-congfig-file nodes.conf 集群节点生成的配置

cluster-node-timeout xxxx 节点失联的超时时间

cluster-announce-ip xxxx 节点自身ip(该 redis 节点所在主机的 ip,如果使用 docker 容器模拟主机,则写 docker 容器的ip)

cluster-announce-port xxxx 节点自身的业务端口(redis 节点自身绑定的端口(Docker 容器内的端口),不同的容器内部可以有相同的端口,进行端口映射时,可以把 不同容器内 相同的 端口号,映射到 容器外不同的端口号。)

cluster-announce-bus-port xxxx 节点自身的总线端口

业务端口:响应 redis 客户端请求(通过 redis-cli 的请求)。

管理端口:通信一些 管理任务(如某个分片中 redis 的主节点挂了,需要让从节点成为主节点)。

 当我们完成上述步骤后,使用 docker-compose up -d 启动容器,还需要使用 --cluster create 建立集群,使用 --cluster-replicas x 表示每个主节点的从节点备份。

5.5 使用集群

可以通过 redis-cli h ip -p port 来连接集群,只要连接上任意一个 主节点,都是在操作整个集群。可以通过 cluster nodes 来查看每一个节点的信息.

在 redis 中的很多命令都可以在集群使用的,比如操作多个 key 的操作就可能出现问题。当我们使用 redis-cli h ip -p port 启动客户端时,当数据需要其他 redis 分片存储时,不能自动切换 就可能会出现以下错误。结果如下所示:

此时我们只需要再启动时加上 -c 命令即 redis-cli h ip -p port -c ,便可自动切换 不同的 redis 分片存储。结果如下所示:

5.6 故障判定

a)故障转移(Failover)

  • 集群检测到主节点不可用(例如,通过心跳超时)。
  • 触发选举协议(如Raft或Paxos),从从节点(follower)中选出新主节点。
  • 选举基于半数以上节点投票,确保新主节点的合法性。
  • 原主节点被标记为"故障状态",并从集群中隔离,以防止脑裂问题。 故障转移的核心目标是快速恢复服务,最小化停机时间。

b)故障迁移

故障迁移是故障转移过程中的关键子步骤,涉及服务和数据的无缝迁移:

  • 在新主节点选举完成后,集群将原主节点负责的服务、连接和数据迁移到新主节点。
  • 迁移过程确保数据完整性,例如通过日志复制或状态同步机制。
  • 如果迁移中检测到数据冲突或不一致,系统会使用预定义策略(如基于$term$的优先级)解决。 故障迁移旨在实现平滑过渡,避免服务中断。

c)重新加入集群

当原主节点恢复后尝试重新连接集群时,会经历以下验证和恢复过程:

  • 从节点先判断自己是否有参选资格,如果从节点和主节点很久不通信了,时间超过阈值,就会失去经选资格
  • 新主节点会验证其数据一致性,主要依据日志索引和任期号。
  • 若数据已过期(如存在$log_n < log_{current}$),则强制降级为从节点
  • 若数据最新且获得多数节点认可,可能恢复为主节点(需特定策略支持)

从节点 offset 越大,排名越靠前。给从节点投票时,只有主节点才有投票资格,当 该从节点 收到的票数超过主节点的一半数目时就会成为新的主节点。

offset优先保证了新主节点的数据新鲜度,而多数投票规则防止了脑裂(split-brain)问题

集群宕机:

        a)某个分片主从节点一起挂,

        b)某个分片主节点挂了,没有从节点

        c)超过半数的主节点挂了

集群扩容:

        a)添加节点: redis-cli --cluster add-node <新增节点IP:端口> <集群现有节点IP:端口>
(要把新增节点加入到哪个节点)

  • <新增节点IP:端口>:新Redis节点的地址
  • <集群现有节点IP:端口>:集群中任意现有节点的地址(新节点将加入该节点所在集群)

        b)槽位重新分配 slots:redis-cli --cluster reshard <集群节点IP:端口>

        执行后按提示操作:

                <1> 输入需要迁移的槽位总数

                <2> 输入接收槽位的目标节点ID(可通过 cluster nodes 查看)

                <3> 输入源节点ID(输入 all 表示从所有节点平均迁移)

                <4> 确认迁移方案

        c)给新的主节点添加从节点

                redis-cli --cluster add-node <从节点IP:端口> <集群节点IP:端口> \

                --cluster-slave --cluster-master-id <主节点ID>

使用 cluster nodes 命令获取目标主节点的ID,在搬运 slots / key 中,客户端 可能会对 正在搬运中的 key 出现访问错误,未搬运的大部分 key 可以正常访问。如果 预算充足 可以新搭建集群,把数据导入之后 新的替换旧的集群。

5.7 常见问题合集

a)Redis 集群的最大节点个数不要超过 1000 哥

b)Redis 集群也会有操作丢失,Redis 并不能保证数据的强一致性,这意味着在实际中集群在特定的条件下可能会丢失写操作。比如在成功写入一个 key 之后,主节点宕机,这哥数据灭有来得及同步到从节点,也没有写入 AOF 文件中就会丢失

c)Redis 集群不支持选择数据库,只能使用默认的数据库 0

6. 缓存

缓存(Cache)是计算机系统中一种临时存储机制,用于存储频繁访问的数据或计算结果,以便在后续请求时能够快速获取,从而提高系统性能和响应速度。它类似于日常生活中的“备忘录”,将常用信息放在手边,避免每次都需要从原始来源(如内存、磁盘或网络)中重新获取。

对于计算机硬件来说,往往访问速度越快的设备,成本越高,储存空间越小。缓存速度快但是空间小,大部分情况都是存放热点数据 (访问频繁的数据)。

缓存之所以非常有用,不得不提到计算机里的 “二八定律”。“二八定律” 指的是 20% 的热点数据,能够应对 80% 的访问场景,因此少量的热点数据就可以应对大多场景。

6.1 Redis 作为缓存

在网站中,我们经常会使用关系型数据库 (比如 MySQL ) 来存储数据,但是随之而来的缺点就是性能不高 (一次查询操作消耗的系统资源较多)。

以下有两个核心思路来让数据库能承担更大的并发量:

        a)开源:引入更多的机器,部署更多的数据库实例,构成数据库集群 (主从复制、分库分表)

        b)节流:引入缓存,使用其他的方式保存经常访问的热点数据,从而降低直接访问数据库的请求数量。

其中 Redis 就是一个用来作为数据库缓存的常见方案。访问数据库前先访问 Redis ,如果 Redis 内存中没有再访问 MySQL。缓存是用来加快 “读操作” 的,而不加快 “写操作”。

6.2 生成策略

6.2.1 定期生成

定期生成策略通过周期性任务来处理数据更新。

6.2.1.1 定期生成概念

系统将访问数据记录为日志文件(如访问日志),然后定期(例如一天、一周或一个月)运行统计程序来生成更新结果。例如,在热词分析中,系统记录用户搜索日志,定期分析生成热词列表。

6.2.1.2 实现方式
  • 日志记录:数据访问事件被写入日志文件。
  • 统计程序:使用脚本或工具(如Python、Hadoop)处理日志,生成汇总数据(如热词排名)。
  • 分布式处理:如果数据量庞大(例如TB级),采用分布式系统(如Spark)来并行处理,提高效率。

6.2.1.3 优点
  • 实现简单:不需要复杂实时机制,易于开发和维护。
  • 过程可控:更新过程可安排在低峰时段执行,减少系统干扰。
  • 方便排查问题:日志文件提供详细历史记录,便于调试和分析。

6.2.1.4 缺点
  • 实时性不足:更新间隔导致数据延迟。例如,突发事件(如新闻事件使冷门词变成热词)发生时,系统无法及时响应,可能造成数据库压力激增。
  • 资源开销:大数据量时,分布式系统虽可扩展,但增加了硬件和运维成本。

6.2.2 实时生成

实时生成策略通过即时处理数据访问请求来提供最新结果,通常结合缓存系统(如Redis)实现。

6.2.2.1 实时生成概念

当用户请求数据时,系统首先查询缓存(如Redis)。如果缓存命中(数据存在),直接返回结果;如果未命中(数据不存在),则从数据库查询,并将结果写入缓存以备后续使用。这避免了定期更新的延迟。

6.2.2.2 实现方式
  • 缓存查询:使用Redis作为缓存层,处理高频读取。
  • 回源查询:缓存未命中时,从数据库加载数据并写入Redis。
  • 内存管理:由于持续写入可能导致Redis内存占用增加,Redis提供内存淘汰策略来防止内存溢出。

6.2.2.3 实时生成常见策略
  • FIFO(先进先出):淘汰最早进入缓存的数据项。
  • LRU(最近最少使用):淘汰最近未使用的数据项(基于访问时间)。
  • LFU(最不经常使用):淘汰访问次数最少的数据项。
  • Random(随机淘汰):随机选择数据项淘汰。

可在Redis中,可通过配置文件(如 redis.conf )设置淘汰策略,例如maxmemory-policy allkeys-lru

6.2.2.4 优点
  • 高实时性:立即响应数据变化,适合突发场景(如热词突然流行)。
  • 性能提升:缓存减少数据库访问,降低延迟。
6.2.2.5 缺点
  • 内存压力:持续写入可能使Redis内存占用增长,达到上限时触发淘汰策略,导致部分数据丢失。
  • 复杂性增加:需管理缓存一致性(如数据库更新时缓存失效)和淘汰策略配置,可能引入调试难度。

6.3 缓存预热

在数据库和缓存系统(如Redis)中,缓存预热是一种优化技术,旨在系统启动或新数据接入时预先加载热点数据到缓存中,从而减少首次访问的延迟和数据库(如MySQL)的压力。而定期生成不涉及预热。

结合定期生成和实时生成的优化策略

策略描述:为了平衡冷启动问题和实时性,可以采用“预热 + 实时淘汰”的混合方式。具体步骤如下:

        a)离线预热阶段:先通过离线分析(如日志统计或机器学习模型)识别热点数据(例如,访问频率高的键值)。然后,在系统启动前或低峰期,将这些数据批量导入Redis(例如,使用redis-cli或脚本导入)。这相当于主动预热缓存,确保初始请求不会全落到MySQL。

        b)实时淘汰阶段:预热后,切换到实时生成模式处理新请求:未命中的数据从MySQL加载并缓存,同时引入淘汰机制(如LRU或TTL)移除旧数据,保持缓存新鲜度。这样,系统既能减少初始压力,又能适应数据变化。

6.4 缓存穿透

缓存穿透是指当查询一个不存在的数据(即该键在Redis缓存和MySQL数据库中均不存在)时,每次请求都直接访问数据库,导致数据库负载剧增。

核心解决方案如下:

        方案1: 缓存空对象(Cache Null)

  • 原理:当查询到数据不存在时,在Redis中存储一个特殊值(如空字符串或"NULL"),并设置较短过期时间。后续查询直接返回空值,避免访问数据库。
  • 实现步骤
    1. 先查询Redis,若命中且值为空,直接返回。
    2. 若未命中,查询MySQL。
    3. MySQL无数据时,将空值写入Redis(例如:SET key "NULL" EX 60,过期时间60秒)。
    4. 设置过期时间防止长期存储无效数据。
  • 优点:简单易实现,适用于大多数场景。
  • 缺点:可能缓存大量无效键,需合理设置过期时间。

        方案2: 布隆过滤器(Bloom Filter)

  • 原理:使用一个概率型数据结构,预先存储所有可能存在的键。查询时先检查布隆过滤器:
    • 若过滤器返回“不存在”,则直接返回,不查数据库。
    • 若返回“可能存在”,再查询缓存和数据库。
  • 关键点
    • 布隆过滤器有误判率(假阳性),但不会漏判(假阴性)。
    • 空间效率高,适合海量键场景。
  • 优点:从源头拦截无效查询,减少数据库压力。
  • 缺点:实现复杂,需维护过滤器数据;不适用于键动态变化的场景。

        方案3: 互斥锁(Mutex Lock)

  • 原理:当多个并发请求查询同一个不存在键时,使用锁确保只有一个线程访问数据库,其他线程等待结果后复用。
  • 适用场景:高并发环境,避免重复查询。
  • 缺点:增加系统复杂度,可能引入锁竞争。

        方案4: 数据预热与校验

  • 预热:在系统启动或低峰期,加载常用数据到缓存。
  • 校验:在应用层校验查询参数(如ID格式),过滤无效请求。

方案之间常见混合搭配使用,从而提升系统稳定性和性能。

6.5 缓存雪崩

缓存雪崩是指在短时间内,Redis 中大量 key 同时失效或 Redis 服务不可用,导致缓存命中率急剧下降(例如从 90% 降至 10%),进而使所有请求直接转向数据库(如 MySQL)。这会造成数据库压力剧增,甚至宕机,严重影响系统稳定性。

缓存雪崩大概分为以下两种情况:

  • a) Redis 直接挂了:Redis 服务故障(如集群崩溃),所有请求无法命中缓存,直接访问数据库。
  • b) Redis 没挂,但短时间内 key 同时过期:大量 key 设置了相同的过期时间(如整点失效),导致失效时间点集中,缓存失效风暴发生。

解决方法:加强监控报警,加强 redis 集群可用性的保证。不给 key 设置过期时间,或设置过期时间时添加随机的因子避免同一时间大量过期。

6.6 缓存击穿

指针对某个热点 key(访问频率极高的数据)突然过期或失效,导致大量并发请求直接穿透缓存访问数据库。它是缓存雪崩的一种特殊情况——只涉及单个或少量热点 key,而非全局 key。

原因:

  • 热点 key 设置了固定过期时间,到期后失效。
  • 高并发场景下,多个线程同时检测到 key 失效,同时访问数据库

缓存击穿的解决方案:针对热点 key 突然过期的问题,核心思路是预防并发穿透和减轻数据库压力。

        a)基于统计发现热点 key

        b)设置热点 key 永不过期

        c)当缓存失效时,启用降级策略(如返回默认值或旧数据),避免所有请求压到数据库。或使用熔断器(如 Hystrix):当数据库访问失败率超过阈值时,暂时拒绝请求,保护后端。

        d)访问数据库时使用分布式锁:当 key 失效时,通过分布式锁(如 Redis 的 SETNX 或 ZooKeeper)确保只有一个线程访问数据库重建缓存。其他线程等待锁释放后直接读取缓存,避免重复查询。

6.7 缓存雪崩和缓存击穿的联系和区别

缓存雪崩:全局性风险,需通过分散过期时间、多级缓存或集群高可用预防。

缓存击穿:局部热点问题,重点在于热点识别、永不过期策略和并发控制。

方面缓存雪崩缓存击穿
本质全局性风险(大规模失效)局部热点问题(单个key失效)
触发原因大量缓存数据同时过期(如设置相同TTL)热点key缓存失效(如被手动清除或过期)
影响范围影响整个系统或多个服务,可能导致雪崩效应只影响特定数据点,但可能因并发放大问题
典型场景促销活动结束,所有缓存集中过期热门商品详情页缓存突然失效
风险级别更高,易引发级联故障较低,但热点key可能成为瓶颈

7. 分布式锁

7.1 分布式锁的概念

分布式锁是分布式系统中协调多节点对共享资源互斥访问的同步机制。核心目标是确保在分布式环境下,同一时刻只有一个节点能执行关键操作(如修改共享数据)。其本质是将单机锁的互斥能力扩展到分布式场景,解决资源竞争问题。

分布式锁本质上就是 一个/一组 单独的服务器,给其他服务器提供加锁的服务(Redis 是一种 典型的可以用来实现分布式锁的方案,但不是唯一一种)

7.2 分布式锁的基础实现

思路非常简单,本质上就是通过一个 键值对 来表示锁的状态。我们可以在架构中引入一个 Redis 服务器,作为分布式锁的管理器。每当有服务器申请锁的时候,先访问 Redis,在 Redis 上设置一个键值对,如果设置成功 就表示加锁成功,反之则加锁失败。

7.3 过期时间

当服务器加锁成功之后,如果该服务器意外宕机,就会导致解锁操作 (删除该 key ) 不能执行,就会引起其他服务器始终处于无法获取锁的情况。

为了解决上述情况,可以在设置 key 的同时引入过期时间。即这个锁最多持有多久就会被自动释放。可以使用 set ex nx 的方式,在设置锁的同时把过期时间设置进去。

如果分开多个操作,比如 setnx 后追加 expire 操作。由于 Redis 多个指令之间不存在强一致性,就会导致两个操作不一定都执行成功,也就会导致无法正常释放锁的问题。

7.4 校验 id

对于 Redis 中写入的加锁键值对,其他节点也是可以删除的。为了解决上述问题,我们可以引入一个校验 id。可以设置键值对的值,把该值与主机相关联,设置成该主机的服务器编号。这样就可以在删除 key 时,先校验当前 id 是否为设置该 key 的服务器,如果是正常删除,反之则删除失败。 

7.5 Lua

很明显,上述先检验再解锁是两步操作,但我们知道 Redis 中指令不具有强一致性。所以我们可以引入 Lua 脚本来让 Redis 执行 lua 脚本的命令具有 “原子性”。

Lua 概念:

        Lua 是一个编程语言,类似于 JS,是一个动态弱类型的语言。Lua 的解释器一般使用 C 语言实现。Lua 语法简单精炼,执行速度快。解释器比较轻量 (Lua 解释器的可执行程序体积只有 200KB 左右),因为 Lua 经常作为其他程序内部嵌入的脚本语言,Redis 本身就支持 Lua 作为内嵌脚本。

7.6 Watch Dog

上述方案中仍有一个重要问题,当我们设置了 key 过期时间内,任务还没执行完,就会导致锁提前失效。

Watch Dog 就是专门为上述问题提出的概念,本质上 Watch Dog 就加锁服务器上的一个单独线程,通过这个线程来对锁过期时间进行 “续约”。Watch Dog 最显著的特点就是 “动态续约”。

比如初始情况下设置过期时间为 10s,同时设定看门狗县城每隔 3s 检测一次。 3s 时间到的时候,看门狗就会判定当前任务是否完成。

        如果任务完成,则通过 Lua 脚本的方式释放锁

        如果任务未完成,则把过期时间重写设置为 10s

7.7 Redlock 算法

一个 Redis 单独作为加锁的服务器时,可能遇到宕机的情况。当前情况下,加锁就会失败。

所以我们可以引入一组 Redis 节点,其中每一组节点 Redis 都包含一个主节点和若干从节点,并且组和组之间的数据都是一致的,互相之间属于 "备份" 关系。加锁时,按照顺序给多个 主节点 加锁,加锁成功数超过一半才视为加锁成功。

同理,释放锁的时候也需要把所有节点都进行解锁,解锁成功数目超过一半才视为解锁成功。

如果觉得对你有帮助的话,请给博主一键三连吧,这对我真的很重要

(>人<;) 求你了~
(๑・́ω・̀๑) 拜托啦~
(≧∇≦)ノ 求求你啦~
(ಥ_ಥ) 真的求你了…
(;へ:) 行行好吧~

http://www.lryc.cn/news/608444.html

相关文章:

  • 路面障碍物识别漏检率↓76%:陌讯多模态融合算法实战解析
  • 基于 LFU 策略的存储缓存系统设计与实现
  • 人工智能之数学基础:离散型随机事件概率(古典概型)
  • 兰空图床部署教程
  • LQR个人笔记
  • Unity_数据持久化_C#处理XML文件
  • ollama 多实例部署
  • 睡岗识别误报率↓76%:陌讯动态时序融合算法实战解析
  • JP3-3-MyClub后台后端(三)
  • 小迪23-28~31-js简单回顾
  • 解决mac在安装nvm过程中可能遇到的一些问题
  • 小迪23年-22~27——php简单回顾(2)
  • (nice!!!)(LeetCode 每日一题) 2561. 重排水果 (哈希表 + 贪心)
  • 【自动化运维神器Ansible】YAML支持的数据类型详解:构建高效Playbook的基石
  • 译| Netflix内容推荐模型的一些改进方向
  • Tlias案例-登录 退出 打包部署
  • Leetcode 11 java
  • 论文笔记:Bundle Recommendation and Generation with Graph Neural Networks
  • (1-8-1) Java -XML
  • [ LeetCode-----盛最多的水]
  • 如何快速解决PDF解密新方法?
  • SpringBoot启动项目详解
  • 丝杆升降机在物流运输领域有哪些应用场景
  • 大模型Agent记忆的主流技术与优缺点解析
  • 23th Day| 39.组合总和,40.组合总和II,131.分割回文串
  • 数据结构---概念、数据与数据之间的关系(逻辑结构、物理结构)、基本功能、数据结构内容、单向链表(该奶奶、对象、应用)
  • 模型 古德哈特定律(Goodhart’s law)
  • 跨语言AI服务指标收集实战
  • 【深度学习】【三维重建】windows11环境配置PyTorch3d详细教程
  • 智能图书馆管理系统开发实战系列(五):前后端集成 - koffi调用与接口设计