跳转至

Redis⚓︎

问题导航
  1. Redis 的主从复制与 Raft 算法有直接关系吗?
  2. Redis Sentinel 是如何进行故障转移的?
  3. 分布式系统中节点数量、法定人数与容错数的关系是什么?为何推荐奇数节点?
  4. 网络分区(脑裂)会导致 Redis 旧主节点写入的数据丢失吗?如何缓解?
  5. 什么是拓扑错位?实际部署中如何放置 Sentinel,Redis Cluster 又如何处理这类问题?
  6. Redis Cluster 和 Sentinel 的关系与区别是什么?
  7. Redis Cluster 如何实现数据路由与分片?
  8. 简述 Gossip 协议的核心机制及应用场景。
  9. Redis RDB 执行 bgsave 期间发生大量写入,底层内存会发生什么?
  10. Redis 的 AOF 机制中,AOF 缓冲区与 AOF 重写缓冲区有何区别?AOF 重写完成后,AOF 缓冲区和 AOF 重写缓冲区会不会导致重复写入?
  11. Redis 的渐进式 Rehash 机制是如何工作的?
  12. Redis 的 SCAN 命令如何在扩缩容(Rehash)期间保证数据不漏读?
  13. Redis LFU 淘汰策略中,是如何用 8 位存储空间记录海量访问频率的?
  14. 在 Cache-Aside 模式中,为什么先更新数据库,再删除缓存可能出现数据不一致?为什么发生概率不高却仍是主流策略?
  15. 什么是“延迟双删”?为什么在实际工程中极少使用?
  16. 如何保证“先更新数据库,再删除缓存”操作的最终一致性?
  17. 使用 Canal 订阅 Binlog 并结合 MQ 时,如何解决高并发下的消息乱序问题?
  18. Redis 管道(Pipeline)的核心原理与局限性是什么?
  19. 如何使用 Redis 实现安全的分布式锁?为什么解锁必须使用 Lua 脚本?

Redis 的主从复制与 Raft 算法有直接关系吗?⚓︎

没有直接关系。Redis 的主从复制(Data Replication)本身并没有使用 Raft 算法。这两者解决的是分布式系统中的不同问题,侧重点也完全不同。

  • Redis 主从复制: 侧重于数据冗余与读写分离。默认采用异步复制,主节点处理写请求后直接返回客户端,随后异步发送给从节点。系统优先保障可用性与高性能(AP 模型),存在数据丢失风险。
  • Raft 算法: 属于分布式共识算法,侧重于强一致性(CP 模型)。要求同步复制,写请求需集群多数节点确认后方可返回成功。
  • 联系: Redis Sentinel(哨兵)和 Redis Cluster 的高可用控制平面(领导者选举与故障转移)借鉴了 Raft 算法的思想(如纪元、多数派投票)。
Redis 主从复制与 Raft 的核心区别

Redis 主从复制的核心逻辑⚓︎

Redis 的主从复制主要为了实现数据冗余读写分离,系统优先保障可用性与高性能(AP 模型),存在数据丢失风险。

  • 异步复制: 默认情况下,主节点(Master)处理完客户端的写请求后,会直接返回给客户端成功,然后再将命令异步发送给从节点(Slave)。
  • 最终一致性: 因为是异步的,主从之间存在数据延迟。如果主节点突然宕机,部分还没来得及同步到从节点的数据可能会丢失。
  • AP 模型: Redis 的设计哲学是快,因此它放弃了强一致性,优先保障可用性和高性能(属于 CAP 理论中的 AP 模型)。

Raft 算法的核心逻辑⚓︎

Raft 是一种分布式共识算法(Consensus Algorithm),它的核心目的是保证集群中多个节点的状态机的强一致性(CP 模型)。

  • 同步复制(大多数机制): 在 Raft 中,Leader 接收到写请求后,必须将日志复制给集群中大多数(Majority)节点,并且收到它们的确认后,才会向客户端返回成功。
  • 强一致性: 只要告诉客户端写入成功了,即使 Leader 宕机,这部分数据也绝对不会丢失(属于 CAP 理论中的 CP 模型)。
  • 应用场景: 常用于 Zookeeper(类 Paxos)、etcd、Consul 等需要强一致性存储的组件中。

联系(选举机制)⚓︎

Redis Sentinel(哨兵)和 Redis Cluster 的高可用控制平面(领导者选举与故障转移)借鉴了 Raft 算法的思想(如纪元、多数派投票)。

  • Redis Sentinel(哨兵): 当主节点宕机时,多个 Sentinel 节点需要共同决定是否真的宕机(客观下线),并选出一个 Sentinel Leader 来执行故障转移。这个 Sentinel Leader 的选举过程,使用的是类似于 Raft 算法的投票机制(基于纪元 Epoch 和多数派投票)。
  • Redis Cluster(集群): 同样,当集群中的某个主节点宕机时,其他的正常主节点需要投票选出一个从节点来晋升为新主节点。这个选举机制也是受到 Raft 算法启发的。

Redis Sentinel 是如何进行故障转移的?⚓︎

Sentinel 在选举领头哨兵(Leader Sentinel)阶段借鉴了 Raft 算法:

  1. 节点状态确认: 哨兵节点确认主节点主观下线(SDOWN),随后与其他哨兵通信。当达到 Quorum 阈值时,确认为客观下线(ODOWN)。只有确认了 ODOWN,才会触发接下来的故障转移流程。
  2. Leader 选举(借鉴 Raft): 发起选举的哨兵递增纪元(Epoch),向其他哨兵发送拉票请求(RequestVote)。采用先到先得原则,获得超过半数且达到 Quorum 选票的哨兵当选 Leader。
  3. 故障转移: Leader 哨兵根据网络健康度、优先级(Slave Priority)、复制偏移量(Replication Offset)和 Run ID,在从节点中选出新的主节点,并执行角色转换。

Redis 哨兵(Sentinel)在进行故障转移时,整个过程非常严谨。很多开发者会误以为是“所有的哨兵一起投票选出一个新的 Redis 主节点”,但实际上并非如此。

SDOWN 和 ODOWN 的区别
  1. 主观下线(SDOWN): 某个哨兵节点发现自己 ping 不通 Redis 主节点了,它就在心里默默给这个主节点打上 SDOWN 标记。
  2. 客观下线(ODOWN): 这个哨兵会去询问其他哨兵能否连上主节点。 如果有足够数量(达到配置的 Quorum 值)的哨兵都说连不上,那么这个主节点就会被标记为 ODOWN(客观下线)。
多数派胜出(Majority Wins)原则

一个拉票的哨兵要想成为 Leader,必须满足两个条件:

  • 获得的选票数必须大于等于所有哨兵总数的一半加一(即大多数,Majority,比如 3 个哨兵需要 2 票,5 个需要 3 票)。
  • 获得的选票数必须大于等于配置的 Quorum 值。

分布式系统中节点数量、法定人数与容错数的关系是什么?为何推荐奇数节点?⚓︎

节点数量(Number of Servers)、法定人数(Quorum)和容错数(Number of Tolerated Failures)是理解所有分布式共识算法(如 Raft、Paxos、Zookeeper 的 ZAB 协议)以及 Redis 哨兵集群的基石。

三者之间有一个非常严谨的数学关系,最核心的公式是:

  1. 必须满足多数派原则 Q = \lfloor N/2 \rfloor + 1,必须是集群节点数的严格多数(超过半数)
  2. 最多容忍故障数为 F = N - Q
  3. 为了容忍 F 个故障节点,集群至少需要 N = 2F + 1 个节点

具体含义是:

  • Number of Servers (N):集群中的总节点数量(比如 3台、5台哨兵)。
  • Quorum (Q):法定人数,或者叫“多数派”。它是集群在做决定时(比如选 Leader、提交数据)必须达到的最少赞成票数。
  • Number of Tolerated Failures (F):系统在不瘫痪(仍能凑齐 Quorum)的前提下,最多允许挂掉的节点数。
N、Q、F 的关系举例
总节点数 (N) 法定人数/多数派 (Q) 最多容忍故障数 (F) 结论与评价
1 1 0 单点,无容错能力。
2 2 0 挂 1 台就凑不齐 2 票,集群瘫痪。极不推荐。
3 2 1 最经典的最小集群配置,允许挂 1 台。
4 3 1 挂 2 台只剩 2 台,凑不齐 3 票。性价比极低。
5 3 2 经典的生产环境配置,允许挂 2 台。
6 4 2 和 5 台机器容错能力一样,浪费资源。
7 4 3 更高可用性需求时的配置。

使用奇数节点原因:增加一个偶数节点并不会提高系统的容错能力(例如,3 节点与 4 节点均只能容忍 1 个节点故障),但会增加网络通信开销与成本。因此,推荐使用 3、5、7 等奇数规模。


网络分区(脑裂)会导致 Redis 旧主节点写入的数据丢失吗?如何缓解?⚓︎

Redis 的设计哲学是优先保证可用性和极高的性能(AP 模型),这就意味着在极端的网络分区(脑裂)场景下,它会不可避免地牺牲数据一致性,导致数据丢失。

  • 机制:网络分区发生时,旧主节点被孤立于少数派,仍继续处理写入。多数派分区选举出新主节点。网络恢复后,旧主节点被降级为从节点,执行全量同步前会清空自身内存数据,导致分区期间写入的数据永久丢失。
  • 缓解方案:配置 min-replicas-to-writemin-replicas-max-lag。若健康从节点数量低于阈值,旧主节点主动拒绝写入请求,以降低可用性为代价减少数据丢失。在 Redis Cluster 中,通过 cluster-node-timeout 机制,节点失联超时后会主动熔断拒绝写入。

什么是拓扑错位?实际部署中如何放置 Sentinel,Redis Cluster 又如何处理这类问题?⚓︎

拓扑错位(Topological Mismatch)是指哨兵节点和应用客户端在物理网络拓扑上的分布不合理,导致在网络分区发生时,客户端无法及时获知主节点状态变化,从而继续向旧主节点写入数据,造成大规模数据丢失。

拓扑错位示例

假设你的应用服务器(Client)和 Redis 主节点在同一个机房 A,而大部分哨兵和从节点在机房 B。突然机房 A 和机房 B 之间的网络断开了,之后:

  • 机房 B(多数派):哨兵选出了新主节点。
  • 机房 A(少数派):所有的应用客户端根本不知道机房 B 选了新主节点。客户端发现身边的旧主节点还在,于是继续向旧主节点写入业务数据。
  • 结果:网络断开时,应用继续往旧主节点写大量无效数据。等网络一恢复,旧主节点被降级,这些业务数据就永远丢失了。

Redis Sentinel 节点的部署策略⚓︎

  1. 哨兵与应用节点同置(Colocating Sentinels with Clients)。其核心思想是:把哨兵节点部署在运行应用客户端(Client)的同一台服务器或同一个容器编排的 Pod 中。

    • 局限性: 这种策略解决了“视图错位”的问题,但它依然无法阻止孤立的客户端向同样被孤立的旧主节点写入数据。要彻底堵住这个漏洞,依然必须配合上文提到的 min-replicas-to-write 配置项。
  2. 跨可用区(Multi-AZ)的奇数散布策略(云原生推荐)。在现代的云环境中部署全栈应用时,通常会利用多个可用区(Availability Zones)来隔离物理故障。

Redis Cluster 的处理机制⚓︎

Redis Cluster 通过节点失联超时(cluster-node-timeout)机制来处理网络分区导致的拓扑错位问题:

在 Redis Cluster 中,节点之间会通过 Gossip 协议不断交换心跳信息。当网络分裂,一个旧的主节点(Old Master)不幸被划分到了少数派群体中时,Cluster 的防御机制会这样运作:

  1. 心跳丢失:孤立的旧主节点发现,自己已经无法和集群中绝大多数的主节点取得联系了。
  2. 触发超时阈值:当这种失联状态持续的时间超过了配置的 cluster-node-timeout(通常设置为 15 秒)。
  3. 自我熔断(拒绝写入):此时,旧主节点会立刻意识到:“我肯定被网络隔离了,另一边的多数派可能已经选出了取代我的新主节点。” 为了保护数据一致性,这个旧主节点会将自己的状态切换为 CLUSTERDOWN,并主动拒绝客户端发来的所有读写请求

Redis Cluster 和 Sentinel 的关系与区别是什么?⚓︎

Redis Sentinel(哨兵)与 Redis Cluster(集群)是 Redis 官方提供的两种应对不同规模和可用性需求的分布式架构方案。它们的核心区别在于解决的系统瓶颈不同:Sentinel 专注于解决 高可用性(High Availability, HA)和单点故障问题;而 Cluster 在提供高可用性的基础上,进一步解决了单机内存容量和写操作吞吐量(Write Throughput)的水平扩展问题。

在实际工程架构中,这两种方案是互斥的,即使用了 Redis Cluster,就不再需要(也不支持)部署 Redis Sentinel。

  • Redis Sentinel:适用于中小规模部署,提供主从复制和自动故障转移功能。它通过监控主节点状态,选举新的主节点来实现高可用性,但不支持数据分片(Sharding),因此单机内存和写吞吐量受限。
  • Redis Cluster:适用于大规模部署,内置数据分片机制(基于哈希槽),同时提供高可用性和水平扩展能力。它通过将数据分布在多个节点上,突破了单机内存限制,并且每个分片都具有主从复制和自动故障转移功能。

Redis Cluster 如何实现数据路由与分片?⚓︎

采用哈希槽(Hash Slot)算法,而非一致性哈希。

  • 核心逻辑:逻辑上划分 16384 个哈希槽。键路由公式为 HASH\_SLOT = CRC16(key) \pmod{16384}
  • 槽位映射:各主节点负责特定区间的哈希槽。底层通过 2KB 的位图(Bitmap)记录槽位归属,并通过 Gossip 协议同步。
  • 路由控制:客户端缓存拓扑结构。若请求发送至错误节点,节点返回 MOVED(永久重定向,槽已迁移)或 ASK(临时重定向,槽正在迁移中)错误,指导客户端修正路由。
数据路由(Data Routing)与数据分片(Sharding)

数据路由是分布式系统中的核心问题之一,指的是如何确定一个特定的 Key 应该存储在哪个节点上,以及客户端应该向哪个节点发送请求来访问这个 Key。

数据分片(Sharding)则是实现数据路由的一种技术手段,通过将数据划分成不同的片段(Shard)并分布在不同的节点上来实现负载均衡和水平扩展。

Redis Cluster 的哈希槽设计

Redis Cluster 采用了哈希槽(Hash Slot)算法来实现数据路由和分片。它将整个 Key 空间划分为 16384 个哈希槽,每个 Key 根据 CRC16 校验算法计算出一个哈希值,然后对 16384 取模得到对应的哈希槽编号。每个主节点负责一个或多个连续的哈希槽区间,客户端通过这个哈希槽编号来确定应该向哪个节点发送请求。

在 Redis 节点的底层源码中,每个节点都会维护一个长度为 16384 位的位图。节点只需要这 2KB 的内存,就能用 01 完美记录下自己负责了哪些槽。

通过 Gossip 协议,节点之间会互相交换这 2KB 的位图。因此,集群里的任何一个节点,都能掌握全局的 16384 个槽分别归属哪个节点

Redis 为什么使用哈希槽而不是一致性哈希?
  1. 数据迁移的确定性: 哈希槽预先定义了固定的 16384 个槽位。扩缩容时,迁移的最小粒度是确定的槽,无需像一致性哈希那样重新遍历并计算大量 Key 的哈希值,迁移过程可控且易于监控。
  2. 拓扑控制与权重分配: 槽机制解耦了数据路由与物理节点,允许根据服务器硬件性能显式、静态地分配不同数量的哈希槽(权重)。
  3. 支持多键操作: 通过哈希标签(Hash Tags,如 {1001})机制,使得相关联的 Key 计算出相同的哈希槽并强制路由至同一物理节点,从而完美支持 MGET 等多键事务操作。

简述 Gossip 协议的核心机制及应用场景。⚓︎

机制: 去中心化通信,节点周期性地随机选择 k 个目标节点交换状态,使新数据以 O(\log N) 的时间复杂度扩散,实现最终一致性。

数据传播策略:

  1. 反熵(Anti-Entropy): 定期交换全量数据的默克尔树摘要,精确修复差异,保证绝对最终一致性,但开销大。
  2. 传言分发(Rumor-Mongering): 仅高速传播增量更新,目标已知晓时概率性停止发送。速度极快但可能产生孤岛节点。

应用: Redis Cluster 节点拓扑感知与心跳监测;Cassandra 节点发现与跨数据中心副本修复。


Redis RDB 执行 bgsave 期间发生大量写入,底层内存会发生什么?⚓︎

  1. 写时复制(Copy-On-Write, COW):fork 产生的子进程与主进程初始共享物理内存。主进程执行写操作时,操作系统拦截请求,将目标内存页(4KB)复制出新副本供主进程修改,子进程依然读取原始物理内存页。
  2. 物理内存变化:频繁写入会导致大量页面复制,服务器物理内存占用显著增加,严重时可能触发 OOM Killer
  3. 系统配置参数:必须设置系统参数 vm.overcommit_memory = 1。防止 Linux 内核在 fork 瞬间因虚拟内存超量评估而拒绝创建子进程。

Redis 的 AOF 机制中,AOF 缓冲区与 AOF 重写缓冲区有何区别?AOF 重写完成后,AOF 缓冲区和 AOF 重写缓冲区会不会导致重复写入?⚓︎

  • AOF 缓冲区: 目标为当前的旧 AOF 文件。主进程在重写期间仍需将命令写入此缓冲区并刷盘,防止重写过程中途崩溃导致数据丢失。
  • AOF 重写缓冲区: 目标为正在生成的新 AOF 文件。用于在 bgrewriteaof 子进程生成快照期间,记录主进程新接收的增量写命令。子进程完成后,主进程将此缓冲区内容追加至新 AOF 文件末尾,确保新文件状态的一致性。

AOF 缓冲区不需要显式清除。在重写期间,新的写命令确实被重复写入了两次(分别进入旧的 AOF 文件和新的临时 AOF 文件),但这种“重复写”是系统为了保证容错性而刻意设计的。在重写流程结束的原子替换操作后,最终保留的 AOF 文件中不存在重复的命令。

Redis 7.0 Multi-Part AOF 机制

Multi-Part AOF 是 Redis 7.0 引入的全新 AOF 重写机制,彻底重构了 AOF 重写流程,极大提升了性能和可靠性。核心设计是将 AOF 文件拆分为三个部分:Base AOF(全量快照)、Incr AOF(增量文件)和 Manifest(清单)。

在重写过程中,主进程直接开启一个新的 Incr AOF 来记录增量命令,完全移除了传统 AOF 重写缓冲区。重写完成后,主进程仅需原子更新 Manifest 文件来指向新的 Base AOF 和 Incr AOF,消除了内存冗余和数据追加带来的同步阻塞问题。


Redis 的渐进式 Rehash 机制是如何工作的?⚓︎

渐进式 Rehash 将 O(N) 的迁移复杂度均摊到请求生命周期中,避免主线程阻塞。

  1. 状态触发: 为新哈希表 ht[1] 分配内存,设置游标 rehashidx = 0
  2. 分摊迁移: 客户端每次执行命令,或底层定时任务触发时,按游标顺序将 ht[0] 中相应桶的数据迁移至 ht[1],并递增游标。
  3. 读写路由: 写入新数据全部路由至 ht[1];读取数据优先查 ht[0],若未命中再查 ht[1]
  4. 缩容逻辑: 字典负载因子低于 10\% 触发缩容(需确保无 RDB/AOF 子进程执行以防 COW 内存暴涨),逻辑与扩容一致。

Redis 的 SCAN 命令如何在扩缩容(Rehash)期间保证数据不漏读?⚓︎

采用二进制高位进位加法(Reverse Binary Iteration)生成游标。

  • 防漏读机制: 哈希表的大小按 2 的次幂变化。扩容或缩容时,数据桶的拆分与合并严格遵循二进制位扩展或截断规律。
  • 算法原理: 高位进位加法保证了系统在扫描过程中,先扫描较低前缀的桶。若发生扩容,原桶的数据裂变到两个新桶,由于新游标基于高位进位生成,算法自然覆盖后续所有衍生桶;若发生缩容,长游标与新短掩码进行按位与(&)操作,直接定位至合并后的新桶,保证数据不遗漏(但可能返回重复数据供客户端去重)。
为什么普通的 i++ 遍历会失败?

如果我们像写 for 循环一样,每次把游标 +1,在 Rehash 过程中就会发生数据漏读或重复读的问题。因为哈希表的扩容和缩容是按照 2 的次幂进行的,数据桶的拆分和合并也严格遵循二进制位的规律。

如果简单地 i++ 遍历,当发生扩容时,原桶的数据会被分散到新桶中,而 i++ 的遍历方式可能会跳过这些新桶,导致数据漏读;同样,在缩容时,原本分散在多个桶的数据被合并到一个新桶中,而 i++ 的遍历方式可能会重复访问这些数据,导致重复读。

重读和漏读的权衡:Redis 的设计哲学

Redis 的设计哲学是:绝不漏掉一个数据,但可能会给你重复的数据。 这意味着在 Rehash 过程中,Redis 的游标生成算法(高位进位加法)保证了所有的数据都能被扫描到,但由于数据的迁移和合并,可能会有一些数据被扫描多次。因此,客户端在使用 SCAN 命令时,需要自行去重,以确保最终结果的正确性。


Redis LFU 淘汰策略中,是如何用 8 位存储空间记录海量访问频率的?⚓︎

LFU 复用了 24 位的 lru 字段(高 16 位存 LDT 时间戳,低 8 位存 logc 计数器)。

  • 惰性衰减(Decay): 访问键或执行淘汰采样时,计算当前时间与 LDT 的差值,根据 lfu-decay-time 参数计算衰减量并扣减 logc,防止历史热点数据永久残留。
  • 对数概率增长: 访问时并不直接加 1,而是基于当前 logc 值计算递增概率 plogc 值越大,递增概率越小。受参数 lfu-log-factor 控制,仅需 8 位空间(最大值 255)即可映射高达数百万次的实际访问频率。
为什么 Redis LFU 需要衰减机制?

由于 Redis LFU 只使用 8 位来存储访问频率计数器(logc),其最大值仅为 255。如果没有衰减机制,某个键在短时间内被频繁访问,其 logc 很快就会达到 255 并饱和。之后,即使该键变冷(不再被访问),它的 logc 仍然停留在 255,这会导致在 LFU 淘汰时,系统误认为该键依然是热点数据,从而错误地淘汰真正的新热点数据。衰减机制的作用是随着时间的推移,动态降低未被访问数据的频率计数值,确保 LFU 算法能够准确反映当前的访问模式。

Redis LFU 衰减的计算逻辑与公式

Redis LFU 的衰减机制是基于时间的,使用了一个全局配置参数 lfu-decay-time 来定义衰减周期。当系统需要计算某个键的最新衰减值时,遵循以下数学逻辑:

$$ \text{current_logc} = \max\left(0, \text{logc} - \left\lfloor \frac{\text{now_in_minutes} - \text{LDT}}{\text{lfu-decay-time}} \right\rfloor\right) $$

其中,now_in_minutes 是当前系统时间(以分钟为单位),LDT 是高 16 位记录的上次访问时间戳(以分钟为单位)。通过这个公式,系统能够根据时间差计算出应该扣除的衰减量,并更新 logc 的值,从而确保 LFU 算法能够动态反映数据的访问频率。

计算步骤解析:

  1. 获取时间差:计算当前系统时间(分钟级)与高 16 位记录的 LDT 之间的时间差(经过了多少分钟)。
  2. 计算衰减量:将时间差除以配置的 lfu-decay-time,向下取整,得到理论上应该扣除的衰减量。例如,若经过了 5 分钟,且 lfu-decay-time 为 1,则衰减量为 5。
  3. 更新计数值:将当前的 logc 减去计算出的衰减量。如果结果小于 0,则截断为 0。

在 Cache-Aside 模式中,为什么先更新数据库,再删除缓存可能出现数据不一致?为什么发生概率不高却仍是主流策略?⚓︎

不一致机制:竞态条件发生在缓存刚好失效的瞬间。读线程 A 未命中缓存,去数据库读取到旧值 V_{old};此时写线程 B 将数据库更新为新值 V_{new} 并删除了缓存;最后,读线程 A 将之前读到的 V_{old} 写入缓存。导致缓存中残留旧数据。

概率不高的原因:触发上述时序要求读线程 A 在读取数据库后发生严重阻塞。实际上,写线程 B 的数据库更新(涉及磁盘 I/O)耗时远大于读线程 A 的缓存写入(纯内存网络请求)。在无极端异常情况下,内存写入不会落后于整个写事务。

成为主流策略的原因:

  1. 相比“先删缓存,再更新数据库”(极易导致旧值被重新写入),此策略引发脏数据的条件更为苛刻。配合设置缓存过期时间(TTL)与异步重试机制,能在系统复杂度和性能之间取得最佳平衡。
  2. 相比“更新缓存”(直接写入新值),此策略避免了并发更新时的竞态条件(如两个写线程同时更新数据库和缓存,导致缓存中保留旧值)。删除缓存属于懒加载策略,只有在数据真正被查询时才重新计算和写入,提高了资源利用率。
  3. 针对可能出现的缓存删除失败或极低概率的并发不一致,业界有成熟且低成本的补偿机制,如设置合理的 TTL 和异步重试机制(通过消息队列或订阅数据库 Binlog 进行重试),确保最终一致性。

什么是“延迟双删”?为什么在实际工程中极少使用?⚓︎

原理: 在更新数据库后,写线程主动休眠一段时间,再次执行缓存删除操作。目的是等待并发的读请求完成“读取旧值并写入缓存”的全过程,通过第二次删除来清除脏数据。

极少使用的原因:

  1. 吞吐量下降:同步休眠会长时间占用处理线程,导致高并发下系统吞吐量断崖式下跌。
  2. 休眠时间不可控:网络抖动、慢查询或 GC 停顿使得读请求耗时不可预测,休眠时间难以精准设置。
  3. 复杂性增加:第二次删除若失败仍需引入重试机制,不如直接采用“先更新数据库,再删除缓存 + 异步重试”的方案更为高效清晰。

如何保证“先更新数据库,再删除缓存”操作的最终一致性?⚓︎

采用异步重试机制处理缓存删除失败的情况,主流方案有两种:

  1. 消息队列(MQ)重试:应用代码捕获缓存删除异常,将删除任务投递至 MQ,由独立的消费者服务持续重试,直至成功并返回 ACK。优点是复用现有基础设施,缺点是对业务代码有侵入性。
  2. 订阅 Binlog 异步重试(如 Canal):业务代码仅负责更新数据库。Canal 伪装为从节点拉取 Binlog,解析数据变更事件并投递至 MQ。专门的缓存同步服务消费事件并删除缓存。优点是业务零侵入且可靠性极高。

使用 Canal 订阅 Binlog 并结合 MQ 时,如何解决高并发下的消息乱序问题?⚓︎

需在生产、消费与数据三个层面进行严格控制:

  1. 生产端(局部有序):将 数据库名+表名+主键ID 作为路由键进行哈希取模,确保同一行记录的所有变更事件被投递到 MQ 的同一个物理分区(Partition)。
  2. 消费端(局部串行化):消费者服务拉取消息后,使用相同的哈希算法将消息分发到内部不同的内存阻塞队列中,由专属的单线程串行处理。
  3. 数据层(乐观版本控制):利用 Binlog 事件自带的时间戳或 GTID 作为版本号。执行缓存更新前,通过 Lua 脚本比对消息时间戳与缓存中的现有时间戳,丢弃时间戳滞后的乱序旧消息。

Redis 管道(Pipeline)的核心原理与局限性是什么?⚓︎

原理:将多个命令打包并一次性发送至服务端,服务端依次执行后将结果批量返回。通过将 N 次网络往返缩减为 1 次,消除了网络 RTT(往返时间)瓶颈,并减少了系统调用引起的上下文切换开销。

局限性:

  1. 不提供事务隔离性,其他客户端的命令可能穿插执行
  2. 若批量命令过多,会导致客户端和服务端内存缓冲占用飙升(需分块发送)
  3. 不适用于有前后数据依赖的命令链。

如何使用 Redis 实现安全的分布式锁?为什么解锁必须使用 Lua 脚本?⚓︎

  1. 加锁: 使用类似 SET resource_name client_id NX PX 10000 命令。利用 NX 保证互斥性,利用 PX(设置 TTL)防止客户端崩溃导致死锁。客户端可通过后台线程(Watchdog)定期为锁续期。
  2. 解锁:使用 Lua 脚本,解锁需要先判断锁是否归属当前客户端(对比 client_id),确认无误后再执行删除。若分两步执行,可能在比对成功后因网络延迟导致锁超时自动释放,进而误删其他客户端建立的新锁。Lua 脚本在 Redis 单线程模型中作为单一指令原子执行,彻底消除了竞态条件。
解锁时 Lua 脚本的具体执行逻辑

客户端必须向 Redis 发送一段 Lua 脚本来执行解锁:

Lua
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
  • KEYS[1]: 对应 resource_name
  • ARGV[1]: 对应客户端在加锁时生成的 unique_client_id

评论