文档教程知识库【免费下载链接】InterviewGuide「InterviewGuide」是阿秀从校园-职场多年计算机自学过程的记录以及学弟学妹们计算机校招秋招经验总结文章的汇总包括但不限于C/C 、Golang、JavaScript、Vue、操作系统、数据结构、计算机网络、MySQL、Redis等学习总结坚持学习持续成长项目地址https://gitcode.com/forthespada/InterviewGuide点击查看免费下载本文是 InterviewGuide阿秀的学习笔记面试八股文 Redis 系列的进阶篇第 21~40 题。围绕 Redis 在线上高并发场景中的核心问题展开并发竞争 Key、缓存与数据库双写一致性、主从复制的完整原理、哨兵sentinel高可用与故障转移、Redis Cluster 的 Gossip 协议与 hash slot 寻址以及生产环境 Redis 的典型部署形态。读完本文后你可以系统性地回答“Redis 的高并发和高可用如何保证”“缓存和数据库如何保持一致”“主备切换会丢哪些数据”这类高频面试问题并理解每个机制背后的参数、流程与取舍。本系列的第一部分Redis 01~20 题覆盖 Redis 是什么、五种底层数据结构等基础知识位于 04-02-01-Redis.md建议配合阅读。一、如何解决 Redis 的并发竞争 Key 问题所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作但最后执行的顺序和期望的顺序不同从而导致结果不同。推荐方案是分布式锁ZooKeeper 和 Redis 都可以实现分布式锁。需要注意的是如果不存在 Redis 的并发竞争 Key 问题不要使用分布式锁这样会影响性能。基于 ZooKeeper 临时有序节点可以实现的分布式锁大致思想为每个客户端对某个方法加锁时在 ZooKeeper 上与该方法对应的指定节点的目录下生成一个唯一的瞬时有序节点判断是否获取锁的方式很简单只需要判断有序节点中序号最小的那一个释放锁时只需将这个瞬时节点删除即可这种方式同时可以避免服务宕机导致的锁无法释放而产生的死锁问题——完成业务流程后删除对应的子节点即释放锁。在工程实践中当然是以可靠性为主所以首推 ZooKeeper。二、如何保证缓存与数据库双写时的数据一致性只要你用缓存就可能会涉及到缓存与数据库双存储双写只要是双写就一定会有数据一致性的问题那么如何解决首先如果你的系统不是严格要求“缓存 数据库”必须一致的话缓存可以稍微跟数据库偶尔有不一致的情况。如果业务上无法接受这种不一致就不要做这个方案最好将读请求和写请求串行化串到一个内存队列里去这样就能保证一定不会出现不一致的情况。代价是串行化之后系统吞吐量会大幅度降低需要用比正常情况下多几倍的机器去支撑线上请求。最经典的缓存 数据库读写模式就是预留缓存模式Cache Aside Pattern读的时候先读缓存缓存没有的话就读数据库取出数据后放入缓存同时返回响应更新的时候先删除缓存然后再更新数据库这样读的时候就会发现缓存中没有数据从而直接去数据库中拿最新数据。互联网公司非常喜欢问这道题因为缓存在互联网公司中使用非常频繁。在高并发业务场景下数据库的性能瓶颈往往来自用户并发访问过大所以一般使用 Redis 做一个缓冲操作让请求先访问 Redis 而不是直接访问 MySQL 等数据库从而减少网络请求的延迟响应。三、数据为什么会出现不一致这类不一致问题主要发生在并发读写访问时缓存操作和数据库操作相互交叉执行。3.1 单库情况下的不一致假设同一时刻发生了并发读写请求例如 A写、B读2 个请求时序如下A 请求发送一个写操作到服务端第一步会淘汰 cache然后因为各种原因卡住了不再执行后面的业务例如大量的业务操作、调用其他服务处理消耗了 1sB 请求发送一个读操作读 cache因为 cache 已被淘汰所以为空B 请求继续读 DB读出一个旧数据脏数据并写入 cacheA 请求终于执行完全把新数据写入 DB。总结因为写操作最后才把数据入 DB、且没有同步cache 里一直保持着脏数据。脏数据是指源系统中的数据不在给定的范围内、或对实际业务毫无意义或是数据格式非法以及在源系统中存在不规范的编码和含糊的业务逻辑。3.2 主从同步、读写分离情况下的不一致A 请求发送一个写操作到服务端第一步淘汰 cacheA 请求写主数据库写入最新数据B 请求发送一个读操作读 cache因为 cache 已淘汰所以为空B 请求继续读 DB读的是从库此时主从同步还没完成读出旧数据脏数据脏数据入 cache最后数据库主从同步完成。总结这种情况下请求 A 和请求 B 的操作时序没有问题是主从同步的时延假设 1s问题导致读请求读取从库读到脏数据造成数据不一致。根本原因单库下逻辑处理中消耗 1s可能读到旧数据入缓存主从 读写分离下在 1s 的主从同步时延中从库的旧数据入了缓存。四、常见的缓存一致性优化方案4.1 缓存双淘汰法先淘汰缓存再写数据库往消息总线ESB发送一个淘汰消息发送后立即返回。写请求的处理时间几乎没有增加。这个方法淘汰了缓存两次因此被称为“缓存双淘汰法”。在消息总线下游有一个异步淘汰缓存的消费者在拿到淘汰消息后1s 后再次淘汰缓存。这样即使在一秒内有脏数据入缓存也能够被淘汰掉。4.2 异步淘汰缓存binlog 方案上述步骤都是在业务线里执行。另一种思路是新增一个线下的读取 binlog、异步淘汰缓存的模块读取 binlog 的总数据然后进行异步淘汰。这里提供一个具体思路1. 总体思路MySQL binlog 增量发布订阅消费 消息队列 增量数据更新到 Redis。读请求走 Redis热数据基本都在 Redis写请求走 MySQL增删改都操作 MySQL更新 Redis 数据MySQL 的数据操作产生 binlog用来更新到 Redis。2. Redis 更新数据操作主要分为两块——全量将全部数据一次写入到 Redis增量实时更新指的是 MySQL 的 update、insert、delete 变更数据。这样一旦 MySQL 中产生了新的写入、更新、删除等操作就可以把 binlog 相关的消息推送至 RedisRedis 再根据 binlog 中的记录对自身进行更新就无需再在业务线中去操作缓存内容。五、Redis 的高并发是如何保证的主从架构与 replicationRedis 的主从架构模式是实现高并发的主要依赖很多项目只需要一主多从就可以实现其所需的功能。通常使用单主用来写入数据单机几万 QPS多从一般是查询数据多个从实例可以提供每秒 10w 的 QPS。一些项目需要在实现高并发的同时尽可能多地容纳大量数据这时需要使用 Redis 集群。使用 Redis 集群之后可以提供每秒几十万的读写并发。单机的 Redis 能够承载的 QPS 大概在上万到几万不等。对缓存来说一般用来支撑读高并发因此架构做成主从master-slave架构一主多从主负责写并将数据复制到其它 slave 节点从节点负责读所有读请求全部走从节点。这样也能很轻松地实现水平扩容支撑读高并发。可以概括为Redis replication → 主从架构 → 读写分离 → 水平扩容支撑读高并发。5.1 Redis replication 的核心机制Redis 采用异步方式复制数据到 slave 节点不过 Redis 2.8 开始slave node 会周期性地确认自己每次复制的数据量一个 master node 可以配置多个 slave nodeslave node 也可以连接其他的 slave nodeslave node 做复制的时候不会 block master node的正常工作slave node 在做复制的时候也不会 block 对自己的查询操作它会用旧的数据集来提供服务但复制完成时需要删除旧数据集、加载新数据集这个时候会暂停对外服务slave node 主要用来横向扩容、做读写分离扩容的 slave node 可以提高读的吞吐量。注意如果采用了主从架构建议必须开启master node 的持久化。不建议用 slave node 作为 master node 的数据热备——如果关掉 master 的持久化master 宕机重启时数据可能是空的经过复制后 slave node 的数据也可能全丢。另外master 的各种备份方案也需要做万一本地的所有文件丢失从备份中挑选一份 RDB 去恢复 master这样才能确保启动的时候是有数据的。即使采用了高可用机制、slave node 可以自动接管 master node但也可能 sentinel 还没检测到 master failure 时 master node 就自动重启了同样可能导致所有 slave node 数据被清空。5.2 主从复制的核心原理当启动一个 slave node 时它会发送一个PSYNC命令给 master node。如果这是 slave node初次连接到 master node会触发一次full resynchronization全量复制master 启动一个后台线程开始生成一份RDB快照文件同时把从客户端新收到的所有写命令缓存在内存中。RDB文件生成完毕后master 将其发送给 slaveslave 会先写入本地磁盘再从本地磁盘加载到内存中。接着 master 会把内存中缓存的写命令发送到 slaveslave 也会同步这些数据。如果 slave node 跟 master node 之间网络故障断开连接会自动重连连接之后 master node 仅会复制给 slave部分缺少的数据。5.3 主从复制的断点续传从 Redis 2.8 开始支持主从复制的断点续传如果主从复制过程中网络连接断掉了可以接着上次复制的地方继续复制而不是从头开始复制一份。master node 会在内存中维护一个backlogmaster 和 slave 都会保存一个replica offset和一个master run idoffset 就保存在 backlog 中。如果 master 和 slave 网络连接断掉了slave 会让 master 从上次 replica offset 开始继续复制如果没有找到对应的 offset就会执行一次resynchronization。如果根据 hostip 定位 master node 是不靠谱的如果 master node 重启或者数据出现了变化slave node 应该根据不同的run id来区分。5.4 无磁盘化复制master 可以在内存中直接创建RDB然后发送给 slave不再自己本地落地磁盘。只需要在配置文件中开启repl-diskless-sync yes # 等待 5s 后再开始复制因为要等更多 slave 重新连接过来 repl-diskless-sync-delay 55.5 过期 key 处理slave不会过期 key只会等待 master 过期 key。如果 master 过期了一个 key或者通过 LRU 淘汰了一个 key那么会模拟一条 del 命令发送给 slave。5.6 复制的完整流程slave node 启动时会在自己本地保存 master node 的信息包括 master 的host和ip但复制流程还没开始。slave node 内部有个定时任务每秒检查是否有新的 master node 要连接和复制如果发现就跟 master node 建立 socket 网络连接然后 slave node 发送ping命令给 master node。如果 master 设置了requirepass那么 slave node 必须发送masterauth的口令过去进行认证。master node第一次执行全量复制将所有数据发给 slave node后续 master node 持续将写命令异步复制给 slave node。全量复制要点master 执行bgsave在本地生成一份 RDB 快照文件master node 将 RDB 快照文件发送给 slave node。如果 RDB 复制时间超过 60 秒repl-timeoutslave node 会认为复制失败可以适当调大这个参数对千兆网卡的机器一般每秒传输 100MB6G 文件很可能超过 60smaster node 在生成 RDB 时会把所有新的写命令缓存在内存中等 slave node 保存了 RDB 之后再将新的写命令复制给 slave node如果在复制期间内存缓冲区持续消耗超过 64MB或者一次性超过 256MB那么停止复制、复制失败client-output-buffer-limit slave 256MB 64MB 60slave node 接收到 RDB 之后清空自己的旧数据然后重新加载 RDB 到自己的内存中同时基于旧的数据版本对外提供服务如果 slave node 开启了 AOF那么会立即执行BGREWRITEAOF重写 AOF。增量复制要点如果全量复制过程中 master-slave 网络连接断掉slave 重新连接 master 时会触发增量复制master 直接从自己的 backlog 中获取部分丢失的数据发送给 slave node默认backlog 就是 1MBmaster 就是根据 slave 发送的psync中的 offset 来从 backlog 中获取数据的。heartbeat主从节点互相都会发送 heartbeat 信息。master 默认每隔10 秒发送一次 heartbeatslave node 每隔1 秒发送一个 heartbeat。异步复制master 每次接收到写命令之后先在内部写入数据然后异步发送给 slave node。六、Redis 如何才能做到高可用failover 故障转移如果系统在 365 天内有 99.99% 的时间都可以对外提供服务就说系统是高可用的。一个 slave 挂掉不会影响可用性还有其它 slave 在提供相同数据下的对外查询服务。但如果master node 死掉了没法写数据了写缓存全部失效slave node 也没有 master 给它们复制数据了系统相当于不可用。Redis 的高可用架构叫做failover故障转移主备切换master node 在故障时自动检测并将某个 slave node 自动切换为 master node 的过程实现了 Redis 主从架构下的高可用。七、Redis 基于哨兵集群实现高可用7.1 哨兵sentinel的介绍哨兵是 Redis 集群架构中非常重要的一个组件主要有以下功能集群监控负责监控 Redis master 和 slave 进程是否正常工作消息通知如果某个 Redis 实例有故障哨兵负责发送消息作为报警通知给管理员故障转移如果 master node 挂掉了会自动转移到 slave node 上配置中心如果故障转移发生了通知 client 客户端新的 master 地址。哨兵用于实现 Redis 集群的高可用本身也是分布式的作为一个哨兵集群运行、互相协同工作故障转移时判断一个 master node 是否宕机了需要大部分的哨兵都同意才行涉及到分布式选举问题即使部分哨兵节点挂掉了哨兵集群仍能正常工作——如果作为高可用机制重要组成部分的故障转移系统本身是单点的那就很糟糕了。7.2 哨兵的核心知识哨兵至少需要 3 个实例来保证自己的健壮性哨兵 Redis 主从的部署架构是不保证数据零丢失的只能保证 Redis 集群的高可用性对于哨兵 Redis 主从这种复杂的部署架构尽量在测试环境和生产环境都进行充足的测试和演练。哨兵集群必须部署 2 个以上节点。如果哨兵集群仅仅部署了 2 个哨兵实例quorum 1---- ---- | M1 |---------| R1 | | S1 | | S2 | ---- ----配置quorum1如果 master 宕机s1 和 s2 中只要有 1 个哨兵认为 master 宕机了就可以进行切换同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是这个时候需要majority大多数哨兵都在运行2 个哨兵majority2 3 个哨兵majority2 4 个哨兵majority2 5 个哨兵majority3 ...如果此时仅仅是 M1 进程宕机了、哨兵 s1 正常运行那么故障转移是 OK 的但如果整个 M1 和 S1 所在的机器宕机了哨兵只剩 1 个此时没有 majority 允许执行故障转移——虽然另一台机器上还有一个 R1但故障转移不会执行。经典的 3 节点哨兵集群是这样的---- | M1 | | S1 | ---- | ---- | ---- | R2 |--------| R3 | | S2 | | S3 | ---- ----配置quorum2如果 M1 所在机器宕机了三个哨兵还剩下 2 个S2 和 S3 可以一致认为 master 宕机了然后选举出一个来执行故障转移同时 3 个哨兵的 majority 是 2剩下的 2 个哨兵运行着就可以允许执行故障转移。八、Redis 哨兵主备切换的数据丢失问题8.1 导致数据丢失的两种情况1异步复制导致的数据丢失因为 master → slave 的复制是异步的可能有部分数据还没复制到 slavemaster 就宕机了此时这部分数据就丢失了。2脑裂split-brain导致的数据丢失脑裂是指某个 master 所在机器突然脱离了正常的网络跟其他 slave 机器不能连接但实际上 master 还在运行。此时哨兵可能就会认为master 宕机了然后开启选举将其他 slave 切换成了 master。这个时候集群里就有了两个 master即所谓的脑裂。此时虽然某个 slave 被切换成了 master但 client 可能还没来得及切换到新的 master还在继续向旧 master 写数据。因此旧 master 再次恢复时会被作为 slave 挂到新的 master 上自己的数据会被清空、重新从新 master 复制数据而新 master 并没有后来 client 写入的数据——这部分数据也就丢失了。8.2 数据丢失问题的解决方案进行如下配置min-slaves-to-write 1 min-slaves-max-lag 10表示要求至少有 1 个 slave数据复制和同步的延迟不能超过 10 秒。一旦所有 slave 的数据复制和同步延迟都超过了 10 秒钟master 就不会再接收任何请求了。减少异步复制的数据丢失有了min-slaves-max-lag这个配置就可以确保一旦 slave 复制数据和 ack 延时太长就认为 master 宕机后损失的数据可能太多从而拒绝写请求——把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低到可控范围内减少脑裂的数据丢失如果一个 master 出现脑裂、跟其他 slave 丢了连接上面两个配置可以确保如果不能继续给指定数量的 slave 发送数据且 slave 超过 10 秒没有给自己 ack 消息就直接拒绝客户端的写请求。因此在脑裂场景下最多丢失 10 秒的数据。九、sdown 和 odown 转换机制sdown主观宕机一个哨兵如果自己觉得一个 master 宕机了就是主观宕机odown客观宕机如果 quorum 数量的哨兵都觉得一个 master 宕机了就是客观宕机。sdown 的达成条件很简单如果一个哨兵 ping 一个 master超过了is-master-down-after-milliseconds指定的毫秒数之后就主观认为 master 宕机了。如果一个哨兵在指定时间内收到了quorum 数量的其它哨兵也认为那个 master 是 sdown 的那么就认为是 odown 了。十、哨兵集群的自动发现机制哨兵互相之间的发现是通过 Redis 的pub/sub系统实现的每个哨兵都会往__sentinel__:hello这个 channel 里发送一个消息所有其他哨兵都可以消费到这个消息并感知到其他哨兵的存在。每隔2 秒钟每个哨兵都会往自己监控的某个 master slaves 对应的__sentinel__:hellochannel 里发送一个消息内容是自己的 host、ip 和 runid以及对这个 master 的监控配置。每个哨兵也会去监听自己监控的每个 master slaves 对应的__sentinel__:hellochannel感知到同样在监听这个 master slaves 的其他哨兵的存在并互相交换对 master 的监控配置、互相同步。10.1 slave 配置的自动纠正哨兵会负责自动纠正 slave 的一些配置如果 slave 要成为潜在的 master 候选人哨兵会确保 slave 在复制现有 master 的数据如果 slave 连接到了一个错误的 master 上比如故障转移之后哨兵会确保它们连接到正确的 master 上。10.2 slave → master 选举算法如果一个 master 被认为 odown 了而且 majority 数量的哨兵都允许主备切换那么某个哨兵就会执行主备切换操作。此时首先要选举一个 slave会考虑 slave 的以下信息跟 master 断开连接的时长slave 优先级复制 offsetrun id。如果一个 slave 跟 master 断开连接的时间已经超过了down-after-milliseconds的 10 倍外加 master 宕机的时长那么 slave 就被认为不适合选举为 master(down-after-milliseconds * 10) milliseconds_since_master_is_in_SDOWN_state接下来会对 slave 进行排序按slave 优先级排序slave priority 越低优先级越高如果 slave priority 相同看replica offset哪个 slave 复制了越多的数据offset 越靠后优先级越高如果上面两个条件都相同选择一个run id 较小的 slave。10.3 quorum 和 majority每次一个哨兵要做主备切换首先需要quorum数量的哨兵认为 odown然后选举出一个哨兵来做切换这个哨兵还需要得到majority哨兵的授权才能正式执行切换。如果 quorum majority比如 5 个哨兵majority 就是 3quorum 设置为 2那么 3 个哨兵授权就可以执行切换如果 quorum majority必须 quorum 数量的哨兵都授权。比如 5 个哨兵、quorum 是 5那么必须 5 个哨兵都同意授权才能执行切换。10.4 configuration epoch 与 configuration 传播哨兵会对一套 Redis master slaves 进行监控有相应的监控配置。执行切换的那个哨兵会从要切换到的新 masterslave → master那里得到一个configuration epoch这就是一个 version 号每次切换的 version 号都必须是唯一的。如果第一个选举出的哨兵切换失败了其他哨兵会等待failover-timeout时间后接替继续执行切换此时会重新获取一个新的 configuration epoch 作为新的 version 号。哨兵完成切换之后会在自己本地更新生成最新的 master 配置然后通过前面说的pub/sub消息机制同步给其他哨兵。之前的 version 号在这里就很重要了各种消息都是通过一个 channel 去发布和监听的一个哨兵完成一次新的切换之后新的 master 配置是跟着新的 version 号的其他哨兵都是根据版本号的大小来更新自己的 master 配置。十一、Redis 集群模式的工作原理11.1 基本通信原理集中式 vs Gossip集群元数据的维护有两种方式集中式、Gossip 协议。Redis Cluster 节点间采用Gossip 协议进行通信。集中式是将集群元数据节点信息、故障等等集中存储在某个节点上。典型代表是大数据领域的storm它是分布式的大数据实时计算引擎采用集中式的元数据存储结构底层基于 ZooKeeper分布式协调中间件对所有元数据进行存储维护。Gossip则是所有节点都持有一份元数据不同节点如果出现元数据变更就不断将元数据发送给其它节点让其它节点也进行元数据变更。两者的取舍集中式的好处在于元数据的读取和更新时效性非常好一旦元数据出现变更就立即更新到集中存储中其它节点读取时即可感知不好在于所有元数据更新压力全部集中在一个地方元数据存储可能有压力Gossip 的好处在于元数据更新比较分散不是集中在一个地方更新请求会陆陆续续打到所有节点上去更新降低了压力不好在于元数据更新有延时可能导致集群中的一些操作滞后。节点间通信约定10000 端口每个节点都有一个专门用于节点间通信的端口就是自己提供服务的端口号 10000。比如服务端口是 7001那么用于节点间通信的就是17001端口。每个节点每隔一段时间都会往另外几个节点发送ping消息其它节点接收到ping之后返回pong交换的信息包括故障信息、节点的增加和删除、hash slot 信息等等。11.2 Gossip 协议的消息类型Gossip 协议包含多种消息ping、pong、meet、fail等等。meet某个节点发送 meet 给新加入的节点让新节点加入集群然后新节点开始与其它节点通信。比如Redis-trib.rb add-node其实内部就是发送了一个 gossip meet 消息给新加入的节点通知那个节点去加入集群ping每个节点都会频繁给其它节点发送 ping其中包含自己的状态和自己维护的集群元数据互相通过 ping 交换元数据pong返回 ping 和 meet包含自己的状态和其它信息也用于信息广播和更新fail某个节点判断另一个节点 fail 之后就发送 fail 给其它节点通知它们某个节点宕机了。ping 消息深入ping 时要携带一些元数据如果很频繁可能会加重网络负担。每个节点每秒会执行10 次ping每次选择5 个最久没有通信的其它节点当然如果发现某个节点通信延时达到了cluster_node_timeout / 2那么立即发送 ping避免数据交换延时过长。比如两个节点之间都 10 分钟没有交换数据了整个集群就处于严重的元数据不一致状态。所以cluster_node_timeout可以调节如果调得比较大会降低 ping 的频率。每次 ping 会带上自己节点的信息以及1/10 其它节点的信息发送出去进行交换至少包含3个其它节点的信息最多包含总节点数减 2个其它节点的信息。11.3 分布式寻址算法常见的分布式寻址方案有hash 算法大量缓存重建、一致性 hash 算法自动缓存迁移 虚拟节点自动负载均衡、Redis Cluster 的 hash slot 算法。1hash 算法来了一个 key首先计算 hash 值然后对节点数取模打在不同的 master 节点上。一旦某个 master 节点宕机所有请求过来都会基于最新的剩余 master 节点数去取模尝试取数据这会导致大部分请求全部无法拿到有效的缓存大量流量涌入数据库——即缓存雪崩式的重建问题。2一致性 hash 算法一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环整个空间按顺时针方向组织然后将各个 master 节点使用服务器的 ip 或主机名进行 hash确定每个节点在哈希环上的位置。来了一个 key首先计算 hash 值并确定此数据在环上的位置从此位置沿环顺时针“行走”遇到的第一个 master 节点就是 key 所在位置。如果一个节点挂了受影响的数据仅仅是此节点到环空间前一个节点沿逆时针方向遇到的第一个节点之间的数据其它不受影响增加一个节点也同理。但一致性 hash 算法在节点太少时容易因为节点分布不均匀而造成缓存热点问题。为了解决热点问题一致性 hash 引入了虚拟节点机制对每一个节点计算多个 hash每个计算结果位置都放置一个虚拟节点从而实现数据的均匀分布和负载均衡。3Redis Cluster 的 hash slot 算法Redis Cluster 有固定的16384个 hash slot对每个 key 计算CRC16值然后对16384取模即可获取 key 对应的 hash slot。Redis Cluster 中每个 master 都会持有部分 slot比如有 3 个 master每个 master 可能持有 5000 多个 hash slot。hash slot 让 node 的增加和移除变得很简单增加一个 master就将其它 master 的 hash slot 移动部分过去减少一个 master就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本非常低。客户端的 api 可以让指定的数据走同一个 hash slot通过hash tag来实现。任何一台机器宕机其它节点都不受影响因为 key 找的是 hash slot不是机器。11.4 Redis Cluster 的高可用与主备切换原理Redis Cluster 的高可用原理几乎跟哨兵类似。判断节点宕机如果一个节点认为另外一个节点宕机就是pfail主观宕机如果多个节点都认为另外一个节点宕机了就是fail客观宕机跟哨兵的 sdown/odown 原理几乎一样。在cluster-node-timeout内某个节点一直没有返回pong就被认为pfail。如果一个节点认为某个节点pfail了会在 gossip ping 消息中ping给其它节点如果超过半数的节点都认为pfail了就会变成fail。从节点过滤对宕机的 master node从其所有的 slave node 中选择一个切换成 master。检查每个 slave node 与 master node 断开连接的时间如果超过了cluster-node-timeout * cluster-slave-validity-factor那么就没有资格切换成 master。从节点选举每个从节点都根据自己对 master 复制数据的 offset 来设置一个选举时间——offset 越大复制数据越多的从节点选举时间越靠前优先进行选举。所有的 master node 开始 slave 选举投票给要进行选举的 slave 投票如果大部分 master nodeN/2 1都投票给了某个从节点选举通过那个从节点就可以切换成 master。与哨兵比较整个流程跟哨兵非常类似。Redis Cluster 功能强大直接集成了 replication 和 sentinel 的功能。十二、深入理解缓存与数据库双写一致性12.1 Cache Aside Pattern最经典的缓存 数据库读写模式读的时候先读缓存缓存没有的话就读数据库取出数据后放入缓存同时返回响应更新的时候先更新数据库然后再删除缓存。为什么是删除缓存而不是更新缓存原因很简单在很多复杂点的缓存场景中缓存并不单单是数据库中直接取出来的值。比如可能更新了某个表的一个字段而其对应的缓存需要查询另外两个表的数据并进行运算才能计算出缓存的最新值。另外更新缓存的代价有时候是很高的——是不是每次修改数据库的时候都一定要将其对应的缓存更新一份对于比较复杂的缓存数据计算场景就不是这样了如果你频繁修改一个缓存涉及的多个表缓存也频繁更新但问题在于这个缓存到底会不会被频繁访问到举个栗子一个缓存涉及的表的字段在 1 分钟内修改了 20 次甚至 100 次那么缓存就更新 20 次、100 次但这个缓存在 1 分钟内只被读取了 1 次存在大量的冷数据。实际上如果只是删除缓存的话那么 1 分钟内这个缓存不过重新计算一次而已开销大幅度降低用到缓存才去算缓存。其实“删除缓存而不是更新缓存”就是一个lazy 计算的思想不要每次都重新做复杂的计算不管它会不会用到而是让它到需要被使用的时候再重新计算。像 MyBatis、Hibernate 都有懒加载思想查询一个部门时部门带了一个员工 list没有必要每次查询部门都把里面的 1000 个员工数据也同时查出来——因为 80% 的情况下查这个部门就只是要访问部门信息。只有在真的要访问里面的员工时才会去数据库查询这 1000 个员工。12.2 最初级的缓存不一致问题及解决方案问题先更新数据库再删除缓存。如果删除缓存失败了数据库中是新数据、缓存中是旧数据数据就出现了不一致。解决思路 1先删除缓存再更新数据库。如果数据库更新失败了那么数据库中是旧数据、缓存中是空的那么数据不会不一致——因为读的时候缓存没有所以会去读数据库中的旧数据然后更新到缓存中。解决思路 2延时双删。依旧是先更新数据库再删除缓存唯一不同的是把删除的动作在不久之后再执行一次比如 5s 之后public void set(key, value) { putToDb(key, value); deleteFromRedis(key); // ... a few seconds later deleteFromRedis(key); }删除的动作可以有多种选择使用DelayQueue会随着 JVM 进程的死亡丢失更新放在MQ中但编码复杂度会增加。总之需要综合各种因素做设计选择一个最合理的解决方案。12.3 比较复杂的数据不一致问题分析数据发生了变更先删除了缓存然后要去修改数据库此时还没修改完。一个请求过来去读缓存发现缓存空了去查询数据库查到了修改前的旧数据放到了缓存中。随后数据变更的程序完成了数据库的修改——数据库和缓存中的数据不一样了……为什么上亿流量高并发场景下缓存会出现这个问题只有在对一个数据并发地读写时才可能出现这种问题。如果并发量很低比如每天访问量只有 1 万次很少会出现这种不一致场景。但如果每天是上亿的流量、每秒并发读几万每秒只要有数据更新的请求就可能会出现上述数据库 缓存不一致的情况。解决方案更新数据时根据数据的唯一标识将操作路由之后发送到一个 JVM 内部队列中。读取数据时如果发现数据不在缓存中那么将“读取数据 更新缓存”的操作也根据唯一标识路由之后发送到同一个 JVM 内部队列中。一个队列对应一个工作线程每个工作线程串行拿到对应的操作一条一条地执行。这样一来一个数据变更操作先删除缓存、再去更新数据库但还没完成此时如果一个读请求过来没读到缓存可以先把缓存更新请求发送到队列中此时会在队列中积压然后同步等待缓存更新完成。这里有一个优化点一个队列中多个更新缓存请求串在一起是没意义的因此可以做过滤——如果发现队列中已经有一个更新缓存的请求就不用再放更新请求进去了直接等待前面的更新操作请求完成即可。待那个队列对应的工作线程完成了上一个操作的数据库修改之后才会去执行下一个操作缓存更新此时会从数据库中读取最新的值写入缓存。如果请求还在等待时间范围内不断轮询发现可以取到值了就直接返回如果请求等待时间超过一定时长那么这一次直接从数据库中读取当前的旧值。高并发场景下该方案要注意的问题读请求长时阻塞由于读请求进行了非常轻度的异步化一定要注意读超时问题每个读请求必须在超时时间范围内返回。该方案最大的风险点在于数据更新很频繁导致队列中积压了大量更新操作读请求发生大量超时最后导致大量请求直接走数据库。务必通过一些模拟真实的测试看看更新数据的频率是怎样的队列积压的压测算例如果一个内存队列里积压了 100 个商品的库存修改操作每个库存修改操作耗费 10ms那么最后一个商品的读请求可能要等待 10 × 100 1000ms 1s 后才能得到数据导致读请求长时阻塞。一定要根据实际业务系统的运行情况做压力测试、模拟线上环境看最繁忙时内存队列可能挤压多少更新操作。如果读请求在 200ms 内返回计算过后哪怕最繁忙时积压 10 个更新操作、最多等待 200ms那还可以接受积压特别多就加机器让每个机器上部署的服务实例处理更少的数据每个内存队列中积压的更新操作就会越少实际测算一般数据的写频率是很低的读高并发、读缓存架构的项目写请求非常少每秒 QPS 能到几百就不错了。例如一秒有 500 个写操作分成 5 个时间片每 200ms 100 个写操作放到 20 个内存队列中每个队列可能就积压 5 个写操作每个写操作性能测试后一般在 20ms 左右完成那么针对每个内存队列数据的读请求最多 hang 一会儿200ms 以内肯定能返回。单机支撑写 QPS 几百没问题如果写 QPS 扩大 10 倍就扩容 10 倍的机器每台 20 个队列读请求并发量过高还必须做好压力测试确保恰巧碰上上述情况时大量读请求在几十毫秒的延时 hang 在服务上看服务能不能扛住、需要多少机器才能扛住最大极限的峰值。但因为并不是所有数据都在同一时间更新、缓存也不会同一时间失效所以每次可能只有少数数据的缓存失效对应读请求的并发量也不会特别大多服务实例部署的请求路由如果服务部署了多个实例必须保证执行数据更新操作的请求、以及执行缓存更新操作的请求都通过 Nginx 服务器路由到相同的服务实例上。比如对同一个商品的读写请求全部路由到同一台机器上——可以自己按某个请求参数做 hash 路由也可以用 Nginx 的 hash 路由功能热点商品的路由倾斜万一某个商品的读写请求特别高全部打到相同机器的相同队列里可能造成某台机器压力过大。由于只有在商品数据更新时才会清空缓存、才会导致读写并发所以要根据业务系统去看如果更新频率不是太高这个问题的影响并不是特别大但的确可能某些机器负载会高一些。十三、Redis 的并发竞争问题与 CAS 方案这也是线上非常常见的问题多客户端同时并发写一个 key可能本来应该先到的数据后到了导致数据版本错了或者多客户端同时获取一个 key、修改值之后再写回去只要顺序错了数据就错了。而且 Redis 自己就有天然解决这个问题的CAS 类的乐观锁方案。某个时刻多个系统实例都去更新某个 key可以基于 ZooKeeper 实现分布式锁每个系统通过 ZooKeeper 获取分布式锁确保同一时间只能有一个系统实例在操作某个 key别人都不允许读和写见第一节配图。另一个思路是时间戳 CAS你要写入缓存的数据都是从 MySQL 里查出来的都得写入 MySQL 中写入 MySQL 时必须保存一个时间戳从 MySQL 查出来的时候时间戳也一起查出来。每次要写之前先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新如果是可以写否则不能用旧的数据覆盖新的数据。十四、生产环境中的 Redis 是怎么部署的这个问题主要想看看你了解不了解你们公司 Redis 生产集群的部署架构。如果你不了解那确实很失职——你的 Redis 是主从架构还是集群架构用了哪种集群方案有没有做高可用保证有没有开启持久化机制确保可以进行数据恢复线上 Redis 给几个 G 的内存设置了哪些参数压测后你们 Redis 集群承载多少 QPS一个典型的回答示例架构Redis Cluster10 台机器5 台部署 Redis 主实例另外 5 台部署从实例每个主实例挂一个从实例。5 个节点对外提供读写服务每个节点的读写高峰 QPS 可能达到每秒 5 万5 台机器最多 25 万读写请求每秒机器配置32G 内存 8 核 CPU 1T 磁盘但分配给 Redis 进程的是10G 内存。一般线上生产环境 Redis 内存尽量不要超过 10G超过 10G 可能会有问题fork 子进程等场景受影响容量5 台机器对外提供读写一共有 50G 内存。往内存里写的是商品数据每条数据 10KB100 条数据 1MB10 万条数据 1G。常驻内存的是 200 万条商品数据占用内存 20G仅仅不到总内存的 50%。目前高峰期每秒 3500 左右的请求量高可用因为每个主实例都挂了一个从实例所以是高可用的——任何一个主实例宕机都会自动故障迁移Redis 从实例会自动变成主实例继续提供读写服务团队其实大型公司会有基础架构的 team 负责缓存集群的运维。小结与延伸阅读本文覆盖的 Redis 进阶面试题可以归纳为四条主线一致性缓存与数据库双写的 Cache Aside Pattern、并发读写导致脏数据的两种时序单库逻辑延迟、主从同步时延、缓存双淘汰法与 binlog 异步淘汰、串行化内存队列方案及压测要点高并发一主多从读写分离、replication 核心机制、全量/增量复制、断点续传backlog offset run id、无磁盘化复制、过期 key 的模拟删除高可用failover 主备切换、哨兵集群quorum/majority、sdown/odown、__sentinel__:hello自动发现、slave 选举算法、configuration epoch 与配置传播、脑裂与异步复制丢数据及min-slaves-to-write/min-slaves-max-lag缓解手段集群Gossip 协议与集中式元数据的取舍、10000 端口的节点间通信、ping 元数据交换策略、hash / 一致性 hash / hash slot16384CRC16 取模寻址方案对比、Cluster 的 pfail/fail 判定与从节点选举N/2 1 投票。本笔记的参考文献按原文档列出包括《Redis 设计与实现》、《Redis 实战》、极客时间《Redis 核心技术与实战》等公开技术资料可结合仓库中同目录下的前篇 Redis 基础 01~20 与操作系统、MySQL 等相邻八股文文档如 04-01-01-MySQL.md一起系统复习。赞分享文档教程知识库【免费下载链接】InterviewGuide「InterviewGuide」是阿秀从校园-职场多年计算机自学过程的记录以及学弟学妹们计算机校招秋招经验总结文章的汇总包括但不限于C/C 、Golang、JavaScript、Vue、操作系统、数据结构、计算机网络、MySQL、Redis等学习总结坚持学习持续成长项目地址https://gitcode.com/forthespada/InterviewGuide点击查看免费下载相关推荐如何快速部署Qwen2-0.5B-Instruct-openmind5步完成本地AI模型搭建如何快速部署Qwen2 0.5B Instruct openmind5步完成本地AI模型搭建 Qwen2 0.5B Instruct openmind是一款轻Docker-GitLab Redis集群部署终极指南主从复制与哨兵高可用配置Docker GitLab Redis集群部署终极指南主从复制与哨兵高可用配置 想要构建企业级的GitLab代码托管平台吗Redis集群的高可用配置是确保系运维云原生如何快速构建高可用Redis集群Jeecg-Boot主从复制与哨兵模式完整指南如何快速构建高可用Redis集群Jeecg Boot主从复制与哨兵模式完整指南 Jeecg Boot作为一款领先的AI低代码平台不仅支持零代码快速搭建业务系低代码后端前端AI 应用大模型RAG工作流自动化上一篇如何让应用焕然一新Morphe Patches主题配色与自定义品牌完整定制教程下一篇See-through 23层身体标签体系深度解析从hair到wings动漫部件如何映射到Live2D Drawable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考