前言:Redis作为互联网行业最常用的内存数据库,是后端、运维面试中的必考题,尤其是主从复制、哨兵机制、集群架构、缓存问题及分布式锁,几乎是每面必问,也是面试官判断候选人技术功底的核心考点。
本文不堆砌冗余知识点,全程围绕「面试考点」展开,将Redis核心架构(主从、哨兵、集群)、缓存常见问题及分布式锁的核心原理、执行流程、关键细节、优缺点及解决方案,进行系统化梳理和浓缩,所有内容均贴合真实面试场景,既适合零基础入门搭建知识框架,也适合面试前快速复盘、查漏补缺,帮你高效掌握Redis面试核心要点,轻松应对各类面试提问。

Redis主从复制

只把Redis部署到一台物理服务器上,可能会具有以下问题:

  1. 可用性问题,这台机器一旦出现问题,比如内存爆了、网线断了、硬盘坏了,这个服务就下线了
  2. 性能问题,一台主机能搭载的内存、磁盘、算力就那么多,而且当内存、磁盘等资源要达到上限的时候,性能也会出现下降

💡 所以需要把Redis部署到多台主机上,构成广义上的Redis集群。

主从复制

现在在多个服务器上都有redis-server进程,那么就可以将其中一个作为主节点,其他的都作为从节点,将主节点的数据都同步到从节点,后续有数据变动时也只允许写入主节点,再同步到从节点。总的来说,实现的效果就是让从节点成为主节点的副本

引入主从复制的机制,因为从节点会同步主节点的数据和修改,所以读操作到主节点到从节点执行都是等效的;再加上相对来说,读操作比写操作占比要大,所以就可以通过引入更多从节点的方式提升Redis在读操作上的并发量。在可用性上,首先不太可能所有主从节点同时挂掉,如果从节点挂了也影响不大,到其他从节点读即可,其次即使主节点挂了,虽然确实会影响写操作的执行,但是读操作还能正常进行。

⚠️ 主从复制这种结构,主要针对读操作提升了并发量和可用性,但是毕竟一山不容二虎,主节点只有一个,所以优化相对还是有上限的

怎样开启多个Redis-server
  1. 打开/etc/redis,在其中新建需要数目的新配置文件(可以直接复制自带的redis.conf
  2. 修改其中的port选项,端口号和进程具有映射关系,一个端口号只能对应一个进程,默认的6379就不能再被其他redis-server使用;同时还要查看daemonize是否开启,即是否允许了redis-server在后台运行
怎样配置主从节点

Redis为我们提供了三种方式,如下面展示,三种都可以。

# 方式一:配置文件
replicaof 127.0.0.1 6379  # 表明当前配置文件对应的redis-server进程是127.0.0.1:6379的从节点
# 方式二:启动redis-server时的命令行参数
redis-server slave1.conf --replicaof 127.0.0.1 6379
# 方式三:redis-cli内置命令
replicaof 127.0.0.1 6379

那么怎么连接不同的redis-server呢?redis-cli可以指定服务器、端口

redis-cli -h 127.0.0.1 -p 6379

⚠️ 关于redis服务器的关闭问题

在一个机器上,时常会出现一个进程挂掉的问题。如果这个进程是对外提供服务的话,挂掉之后能不能再次启动是很重要的一个问题,所以Linux系统提供了一些方式,比如 service redis-server start。通过这种方式启动的redis-server进程,操作系统会分配一个进程监视这个redis-server进程的运行情况,即使退出了,也会立马拉起来一个新的进程继续干活。

所以通过这种方式启动的redis-server,想要关闭这个机器的redis-server服务,就不能通过 kill -9 pid,需要通过与之配套的命令 service redis-server stop

再一个就是关于AOF文件的权限问题,比如当主节点挂掉时(主节点采取的是 service redis-server start的方式启动,redis-server是通过redis这个用户的身份启动的;后续我们启动的其他从节点,是通过 redis-server ***.conf这种方式启动的,是以root的身份),我们重新启动,发现无法启动,就是因为appendonlyfile.aof现在是root所有的,主节点需要对AOF文件的读+写权限才可以。

所以一方面,可以通过 chown的方式解决,但是更合理的是通过更改 /etc/redis/redis.conf中的 dir选项来指定不同的 redis-server各自的工作目录

Redis的同步建立方式
  1. 从节点启动之后,会和主节点建立tcp连接,主节点执行bgsave,发送自己生成的RDB文件给从节点,从节点清除自己的数据,用RDB来对自身做初始化
  2. 后续tcp连接会一直保持,当主节点数据发生改变时,主节点会将产生改变的命令发送给从节点,让从节点也做出相应更改,就可以完成数据的同步

🚀 其实实现的效果类似主节点是服务器,从节点是客户端

查看主从结构
  • 使用info replication命令,其中在# Replication中,在主节点中操作,会展示以下内容
role:master  # 代表是主节点
connected_slaves:2  # 连接的从节点的数目
# 从节点的ip、端口、在线状态、同步偏移量和延迟
slave0:ip=127.0.0.1,port=6380,state=online,offset=70,lag=1  
slave1:ip=127.0.0.1,port=6381,state=online,offset=70,lag=0
master_failover_state:no-failover  # 故障转移信息,和下面的master_replied2、second_repl_offset
master_replid:4949c9b5461206f995d40bd493900d623bd5b392  # 主节点的身份标识
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70  #  主节点复制偏移量,如果和前面的offset相同,代表操作已经完全同步
second_repl_offset:-1
repl_backlog_active:1  # 下面这几条,和"部分同步"机制有关
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70
主从结构的删除与修改
slaveof no one

在从节点执行如上命令,会撤销从节点的slave身份,同时数据保留,不过不会再从原来的主节点同步了

slaveof 127.0.0.1 6380

在任意节点执行如上命令,就可以将该节点转为目标节点的从节点

⚠️ 如上操作都是临时生效,不修改配置文件的话,重启后依然会回复原来的主从关系

安全性、只读、传输延迟
安全性

Redis可以设置登录密码,也可以在主从之间设置密码。

只读

在配置文件中,默认 slave-read-only=yes(似乎新版本改为了 replica-read-only=yes),可以设置为no,来允许从节点修改数据

⚠️ 虽然允许,但是不建议,因为从节点对数据的修改不会同步到其他节点,这样会造成数据不一致的问题

传输延迟

在配置文件中,提供了 repl-disable-tcp-nodelay,默认是no,表示默认不开启较小TCP包的合并,只要有TCP包就立刻发送。这个选项主要适合在网络条件较为苛刻时,出于节省网络带宽的目的开启,但是主从之间的延迟也会有所增大。

🐣 其实类似于TCP的捎带应答,把ACK确认应答和其他较小的数据包合并在一起发送,但是发送频率上会有所降低,因为这种方式意味着有些数据包不是立刻发送的

拓扑结构
❓ 何为拓扑结构

其实简单来说,就是各个节点之间应该怎样连接。

写操作优化

当写操作比较多时,主节点会不断更新我们的AOF文件,会有一定的性能消耗,所以我们可以把这些工作交给从节点去做,让他们去产生AOF文件,这样就能增加主节点的并发量。

但是这样还有一个问题,就是主节点挂了之后,重启时,因为没有产生自己的AOF文件,就会导致丢失数据,这样主从之间的同步机制就会删掉从节点中的数据。

怎样优化呢?我们可以让主节点在重启时,去找从节点获取AOF文件来恢复数据

实际情况中,读操作占比是远大于写操作的。考虑如下的拓扑结构

            master
       /      |      \
   slave1   slave2  slave3  ......

当从节点很多时,一旦数据发生更改,主节点就要向海量的从节点发送新的命令同步操作,会对网络带宽有一定的要求与压力,所以也有如下的拓扑结构做出了优化

            master
            /    \
        slave1  slave2
     /    \        /     \
 slave3  slave4  slave5  slave6
                ......

当数据发生更改时,主节点只需要将同步信息发送给作为其子节点的部分从节点,再由他们分层往下转发,就对网卡资源的要求降低了。但是这样也有弊端,就是同步的时间效率要降低了,尤其对于叶子节点来说

所以这种拓扑方式和第一种拓扑方式,就像前面关于传输延迟是否开启的选择,都是要试情况而论的

主从同步建立的流程
  • 从配置文件、启动参数或者内置命令中读取主节点的地址、端口并保存
  • 通过TCP的三次握手机制与主节点建立连接,保证网络层是畅通的
  • 发送ping命令,确保对方的应用层上是工作正常的
  • 同步数据集,全量同步
  • 持续命令复制,增量同步
Replication ID

主节点在每次启动时,会生成一个Replicationid,当主从节点建立了联系,从节点就会从主节点中获取ReplicationID,作为对主节点的表示(目前主从同步的结构中还不涉及多主节点的问题,后面集群中就有区分主节点的需求了)

💡 在前面使用 info replication时,master-replid2一般不需要,但在以下场景中有自己的用武之地

A为主节点,B为从节点,由于网络波动等原因,A、B节点之间可能会失去联系,这时B节点可能就会认为A节点挂了~,自己成为主节点,生成replicationid,并放在 master-replid中,把原来的放在 master-replid2的位置(📝 需要手动干预,或者使用哨兵机制)

当A节点重新获得联系之后,B节点可以根据 master-replid2中记录的replicationid恢复与A的主从关系

Offset

描述的是当前节点在操作指令上的偏移量信息。比如对于主节点来说,每次接收到修改操作的命令,都会以字节为单位将这些命令的大小作为累加,作为偏移量;对于从节点来说,接收到主节点复制来的同步操作命令也会以字节为单位对字节的偏移量进行累加。

💡 这样,如果两个节点之间Replication ID相同,并且偏移量相同,就可以认为他们之间已经完成了同步

全量复制和部分复制

从节点向主节点发起同步请求(PSYNC)时,并不是有从节点决定以何种方式同步,而是主节点根据双方偏移量情况和主节点字节的工作状态,来判断适合通过哪种方式进行同步。

全量复制,适合从节点启动后第一次进行同步的情况,replication_id不匹配,复制积压缓冲区数据已被覆盖的情况

部分复制,适合从节点发生网络抖动或重启时,之前从主节点同步过数据,差别不会太大,尝试只重新同步部分数据的情况

💡 主节点会选择全量复制的情况:

  1. 从节点发起PSYNC时携带的replication_id和主节点的replication_id不同
  2. 从节点发送的复制偏移量和和主节点的复制挤压缓冲区大小不匹配(比如主节点复制解压缓冲数据已被覆盖的情况)

只要以上两点有一点不满足,主节点都会抛弃部分复制,选择全量复制

💡全量复制执行的流程

  1. 首先,主节点会执行bgsave生成rdb文件,期间新接收到的增量操作会被记录到缓冲区中
  2. rdb文件发送成功后,会把缓冲区中的增量操作也进行发送,从节点本地进行回放
  3. 相比之下,部分复制不需要主节点的rdb文件,直接同步增量数据即可(主节点会返回从节点具体要执行的同步方式,全量复制会返回fullresync <new_run_id> 0,部分复制会返回continue <offset>告知从从节点的哪里开始部分复制
  4. 当从节点接受到主节点的RDB文件之后,如果还开起了AOF,那么就会执行bgrewriteaof,生成新的AOF文件

💡 部分复制执行的流程

  1. 当主从节点因为网络抖动失去联系时,增量复制就无法由主节点发送给从节点,这是主节点就会把这段时间内接收到的写操作写到一个叫做“复制积压缓冲区”的部分
  2. 当主节点之后重新和从节点建立连接,从节点发起送psync请求,如果发现从节点已经自立为主节点,那么就要“好好教育一下”,全量复制;如果复制积压缓冲区的数据没被覆盖并且从节点的replication_id仍然是自己,那么就会进行部分复制
全量复制的无硬盘模式
  1. 按照原先的思路,进行全量复制时,需要主节点声称自己的RDB文件,写到磁盘;随后从磁盘中读取发送到从节点;从节点将RDB文件写入磁盘,随后redis-server从磁盘中读取,完成从节点的全量同步。
  2. 现在节省掉一系列的磁盘读写操作,比如:主节点生成RDB直接发送,不写入磁盘;从节点接受RDB文件也不写入磁盘,而是直接加载。
  3. 虽然确实能省掉大量的IO操作,但是在有网络传输的任务场景下,其实最耗时的是网络通信操作,所以全量复制的无硬盘模式确实能够节省时间,但是没有那么明显
关于run_id和replication_id
  1. 其实run_id和replication_id是两个不相同的东西,前者服务于redis的哨兵机制,后者服务于redis的主从复制。
  2. 主节点每次都启动都会生成新的run_id,用来表示自己的身份,后续从节点的replication_id都会保存主节点的replication_id。
实时复制

通过上面提到过的全量复制、部分复制,已经能够完成数据的同步了,但是在同步之后,主节点还是会源源不断的收到修改请求。这是就会通过实时复制的方式继续同步

实时复制复制的也是修改的指令,到从节点自己的数据上重现

采用实时复制的方式要求主从之间连接稳定。Redis有一套心跳包机制:

  1. 主节点每隔10s发送ping给从节点,连接正常的话从节点会发送pong
  2. 从节点每隔1s会向主节点发送自己的同步进度(offset)
  3. 这些数值都是可以修改的了解即可

通过这种方式,尤其是网络拓扑结构是树形的时候,是可能出现主从数据延迟的情况的,但是一般时间很短,影响不会特别的巨大

❌ 主从复制的缺点
  1. 只有一个主节点,一旦主节点挂了,虽然其他从节点还能够提供读服务,但是从节点不会主动升级为主节点,一方面这意味着不能很快恢复读操作;另一方面,将其他从节点升级为主节点需要手动进行一系列复杂操作
  2. 只优化了读操作,虽然写操作占比少于读操作,但是只有一个节点去写还是容易出现性能瓶颈

Redis哨兵机制

什么是哨兵机制

当一个机器出现故障挂掉时,分为两种处理方式,一种是人一直监控,一种是让机器去监控。前者不合适,服务器能7*24工作,人不行,所以采取后者。既然有了监控,那么就离不开报警。这种方式说白了还是让人去处理,但是依然有通知延时和处理时间的问题

所以我们引入了哨兵机制。哨兵(sentinel)是一个单独的进程,若干个这样的进程就可以组成一个哨兵集群。他们可以监控主节点、从节点的工作情况,当主节点挂掉时,他们就可以发现,并“推选”出新的主节点,来完成对Redis集群写操作的维护

哨兵机制的工作流程
  1. 哨兵集群会监视所有的主节点、从节点,并与他们建立TCP连接,定期发送心跳包来检测运行情况。
  2. 当一个哨兵节点发现主节点对心跳包没有回复,并不会直接判断主节点挂掉,而是会“询问”其他哨兵节点主节点是否正常。如果多个哨兵节点都发现主节点没有反应,就会自动判断主节点故障,开始故障转移的流程。
  3. 在哨兵集群中,会派遣一个哨兵节点作为Leader,去从节点中选择一个作为新的主节点,其他的哨兵节点继续监视其他从节点。Leader会让被选作新的主节点的从节点slave of no one,并修改其他从节点的主节点到我们新的从节点。
  4. 完成Redis集群的转移后,哨兵节点会通知客户端新的主节点的信息,这样客户端有新的修改请求时就直接向新的主节点发送了

总结一下,哨兵机制其实完成的就是三件事:监视、故障转移、处理

在这里我们可以总结出来一些思想:我们的哨兵机制,也是采取的哨兵集群而不是只是用单个哨兵节点。一方面有可能哨兵节点也会挂,另一方面哨兵节点也可能会出现自身网络抖动而挂掉的情况,所以会引入一个哨兵集群,每个哨兵节点都在做同样的工作。其实我们发现,在分布式系统中,单点是我们一般要避免的设计,这种冗余的设计思想,其实就是为了提高系统的稳定性,就像主从同步一样,各个从节点都和主节点存着相同的数据,来保证数据或者服务不会因为某个节点的挂掉而丢失或崩溃

故障转移流程实例
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:12.368 # +sdown sentinel 54639092f10cfa8fcef48790b7153bfdc02c7b4c 172.18.0.6 26379 @ redis-master 172.18.0.4 6379
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:12.423 # -sdown sentinel 54639092f10cfa8fcef48790b7153bfdc02c7b4c 172.18.0.6 26379 @ redis-master 172.18.0.4 6379
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:28.105 # +sdown master redis-master 172.18.0.4 6379
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:28.279 # +new-epoch 2
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:28.282 # +vote-for-leader 54639092f10cfa8fcef48790b7153bfdc02c7b4c 2
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:29.192 # +odown master redis-master 172.18.0.4 6379 #quorum 3/2
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:29.192 # Next failover delay: I will not start a failover before Mon Apr  6 12:22:28 2026
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:29.430 # +config-update-from sentinel 54639092f10cfa8fcef48790b7153bfdc02c7b4c 172.18.0.6 26379 @ redis-master 172.18.0.4 6379
redis-sentinel-1  | 1:X 06 Apr 2026 12:16:29.430 # +switch-master redis-master 172.18.0.4 6379 172.18.0.3 6379

我们用Docker来拉取Redis镜像,构建一个主节点,两个从节点的Redis主从结构,并且构建出三个哨兵,来作为哨兵集群。现在,我们用docker-compose stop redis-master的方式挂掉主节点

接下来通过docker-compose logs查看,上面截取了选举出来的Leader的记录。

首先,它最先发现redis-master挂掉,sdown表示自己主观认为主节点已经挂掉了,其他节点会确认结果,如果都认为主节点挂掉了,那么就会odown得到客观结果——redis-master挂了。

现在达成了主节点客观下线的共识。这时,他是第一个发现的,会开启Leader选举,投给自己一票,并通知其他哨兵节点选举的开始。其他两个哨兵在接收到投票的通知后,如果自己的票还没有投出的话,就会把自己的票投给通知他的节点,这样在良好的网络环境下,基本上就是谁先发现,谁就成为 Leader(因为上述机制决定了它大概率能够最新获得半数的票) 。这样redis-sentinel-1就会作为Leader去选择新的从节点。这个操作有以下几种选择方式:

  1. 在各个redis-server的配置文件中,可以给定一个replica-priority,作为一个选择从节点时的优先级,所有从节点中,replica_priority数值最小的,优先级最大,成为新的主节点
  2. 通过上一步选不出来时,会从所有从节点中选择一个offset最大的节点,因为这意味着它同步的数据最多
  3. 如果依然选不出来,那么就开始随机选一个了,每个节点在启动时都会生成一个run_id,去选择run_id最小的一个,作为新的从节点

💡 关于投票平票:

从投票机制中,我们不难发现,存在一个问题:如果所有哨兵节点在同一时间都发现了主节点挂掉,按照规则都投票给自己,那么几个哨兵中谁都选不出来,也就不会去重新选我们的主节点

一般来说这种情况不太会出现,但是在Redis 5.0.x中,对这种情况其实没有做任何处理,当几个哨兵的网络条件相近时,概率会相对比较大。所以之后引入了“随机微小延迟”,让各个哨兵之间能够有发现的时间差,也就能够选出Leader

Redis集群

这里我们谈的是狭义上的集群。广义上,前面的主从复制结构就已经可以叫做Redis集群了。但是我们这里要处理的问题是数据总量的问题,主从复制结构是我们Redis集群所指的体系中的一部分结构

主从结构确实解决了读压力大的问题。但是前面就提过,一方面,写还是全部由主节点自己承受,另一方面,虽然有多个从节点,但是大家都存一样的数据,意味着还是用一个机器所能搭载的最大内存去存储我们的所有数据。

所以为了解决这个问题,我们引入了集群,将数据分成多个部分存储到各个Redis主节点上。

那么问题来了,怎么分数据呢?

哈希求余

我们的Redis数据都是键值对结构的,key都是字符串。所以我们常采用md5哈希算法来根据key生成一个散列值,再用我们的主节点个数取余,得到一个下标,存储到对应主节点即可;后续需要访问某个key时,再通过md5算法进行一边上述过程,到相应节点访问即可。

但是这里我们发现还有一个和哈希表一样的问题:当上线新的分片时,就像哈希表扩容,我们需要对所有key重新定址,同时所有从节点也需要同步,这个代价是非常非常大的

一致性哈希算法

现在,我们把unsigned int的范围映射到一个圆上,将这个源分成若干等分。计算出hash值后,就能够对应到这个圆的某一个位置,这个位置属于那个等分,我们就把他交给哪个主节点写入。

当添加新的分片时,其他分片的位置不需要移动,将新的分片映射到已经分好的某个区间中,将相应的数据考入该节点,后续再映射到新区域的键值对都交给这个节点存储

相比哈希求余,一致性哈希算法解决了拷贝移动数量过多的问题。但是从处理过程中我们不难看出,仍然存在数据倾斜的问题——不患寡而患不均,这样会造成其他节点的压力更大,数据量上也更不一致

哈希槽分区算法

哈希槽分区算法是Redis实际采用的分区算法,将通过crc16(一种哈希算法)得到的结果模上16383,得到的结果就是这个key的哈希槽。就是说:我们为所有主节点提供了[0, 16383]个哈希槽,类似一致性哈希算法,映射到某个范围的哈希槽中的key都会被映射到所属范围的主节点中存储。其实哈希槽分区算法,就是结合哈希求余和一致性哈希算法来进行的。

哈希槽分区算法,对于每个分区还使用了一个位图来表示每个哈希槽是否是当前分片的。这种方式可以不连续的、分散的为每个分片分配哈希槽。

当需要插入新的分片时,只需要每个节点都均等的挪出一部分自己的数据给新分片,使得拷贝完成后各个分片的哈希槽数量相等(或者接近)、达到数据平衡的状态。这样既解决了哈希求余的大量拷贝,又解决了一致性哈希算法数据倾斜的问题

经典面试题一:最多是有16384个分区吗?

理论上确实可以这么去做,但是不建议。Redis作者自己建议分区数不应该超过1000。因为哈希槽分区算法的映射过程是先映射到哈希槽,再映射到分区的。一个分区的哈希槽太少,可能会导致各个分区的数据达不到均衡的状态(比如真的去一个分区一个哈希槽的话,那么没有哈希槽被映射到的分区就一点数据都没有存了)。

经典面试题二:为什么是16384而不是更大呢?

首先,16384对于Redis集群来说,已经够用了。其次,16384的位图需要2kb的空间,在节点之间通信时,会采用心跳包的机制,里面会带有各个节点的哈希槽的位图。更大虽然也是用位图存储,但是对于网卡来说,还是会造成更大的压力(一方面,网络带宽几乎可以说是主机上最贵的资源了,另一方面,对于频繁发送的信息,每个都多携带一部分,总的压力也就上来了)

建立与使用集群
┌──(root㉿DESKTOP-I1KJQG1)-[~/redis-cluster]
└─# redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2
>>> Performing hash slots allocation on 9 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 172.30.0.105:6379 to 172.30.0.101:6379
Adding replica 172.30.0.106:6379 to 172.30.0.101:6379
Adding replica 172.30.0.107:6379 to 172.30.0.102:6379
Adding replica 172.30.0.108:6379 to 172.30.0.102:6379
Adding replica 172.30.0.109:6379 to 172.30.0.103:6379
Adding replica 172.30.0.104:6379 to 172.30.0.103:6379
M: af6df9ab27151e12f1d82747dbe99552a0f3f0fe 172.30.0.101:6379
   slots:[0-5460] (5461 slots) master
M: 2fec47a54dacb05af4c24b069f8a921386412deb 172.30.0.102:6379
   slots:[5461-10922] (5462 slots) master
M: 44093a823441cbd9f838317b18cab4e0fea86bb1 172.30.0.103:6379
   slots:[10923-16383] (5461 slots) master
S: 6f29fccb3e49d86e0e1a7db58949033cee9aaa25 172.30.0.104:6379
   replicates 44093a823441cbd9f838317b18cab4e0fea86bb1
S: e6bf645498d231b214feec64b8be268157065f70 172.30.0.105:6379
   replicates af6df9ab27151e12f1d82747dbe99552a0f3f0fe
S: 874c92e6eaf908147c9db04cd69a21a1355dfda1 172.30.0.106:6379
   replicates af6df9ab27151e12f1d82747dbe99552a0f3f0fe
S: 6550e746e1589c34319ec2cc131ac4ffb81962c9 172.30.0.107:6379
   replicates 2fec47a54dacb05af4c24b069f8a921386412deb
S: 5f81d29b5f3c53dfbacb46666c6d0ca57859031b 172.30.0.108:6379
   replicates 2fec47a54dacb05af4c24b069f8a921386412deb
S: cd17a7d0e398cfbc3f1c1f2016a590dc7cd90b67 172.30.0.109:6379
   replicates 44093a823441cbd9f838317b18cab4e0fea86bb1
Can I set the above configuration? (type 'yes' to accept):

上面是Redis集群建立的过程以及输出内容。可以看到,Redis将我们提供的节点,按照要求的从节点的数目进行了划分,选出了三个主节点,并分别分配了两个从节点。

这样就完成了Redis集群的建立。开启集群模式后,直接 redis-cli 登录,进行各种键值对操作的话,可能会报错,因为集群模式的分片规则可能会使当前的键值对不属于这个节点。所以可以在启动服务器时加上 -c 来自动路由到对应的分片

主节点宕机

当一个主节点被判定为下线后,会从其从节点中选择一个新的主节点来代替。后续原先主节点上线后,会先作为从节点。如果后续需要恢复原来的集群结构的话,可以使用 cluster failover 来恢复原先主节点的主节点身份

故障判定
  • 每个节点每秒都会随机对集群中的部分节点发送心跳包(包含我们前面提到的哈希槽位图、主从身份、分片信息等等)
  • 如果发送了 ping 方法之后,没有在规定时限内接受到 pang 方法,那么就会试图重新建立TCP连接。如果TCP连接也无法建立的话,就会主观的认为该节点下线( pfail )
  • 该节点会向集群中的其他节点确认该节点的信息。每个节点都记录着自己认为已经下线的节点的列表。如果集群中半数的节点都认为某个节点已经下线,并且这个节点是主节点,那么就会认为这个主节点已经下线,开始故障转移流程

关于集群宕机的情况

出现以下三种情况时,可能会让一整个集群都进入宕机状态

  1. 一个主节点及其所有从节点都挂掉
  2. 一个主节点没有从节点,并且主节点挂掉了
  3. 半数的主节点都挂掉了

总结:Redis集群的核心原则是保证每个哈希槽都能正常工作

故障转移

当通过上面故障判定的流程确认集群的一个主从体系的组节点挂掉后,就会进入故障转移的流程。

  • 首先,已经很长时间没有与主节点通信过的节点是不可以参与后续选拔流程的
  • 符合参选条件的节点会进入休眠状态,进行一段时间的休眠,比如C、D两个节点符合。休眠时间 = 500ms的基础时间 + [0, 500]ms的随机时间 + 1000ms * 排名。这里的排名是由偏移量决定的,即:数据越新的节点排名越高。
  • 最新结束休眠的节点会向其他所有节点发起拉票,但是只有主节点有资格参与投票,每个主节点只有一票。最新拿到所有主节点半数的节点成为新的主节点,比如是C节点。
  • C节点首先会对自己执行 slaveof no one,然后会将自己这个主从体系中的其他所有从节点设置为自己的从节点,并对其他主节点进行通知

这种选举方式,就是Raft算法,在分布式系统中广泛使用。

基本上,因为由随机休眠时间的作用,谁先苏醒,谁就能够成为新的主节点

集群扩容

在集群搭建完成后,可能需要加入更多的机器,这里就要涉及到扩容的问题。

添加机器
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379

表示往 172.30.0.101:6379 代表的集群中添加 172.30.0.110:6379 节点,该节点默认会成为主节点,但是还没有分配哈希槽,所以直接使用客户端连接的话,是无法添加 key 的

分配哈希槽
redis-cli --cluster reshard 172.30.0.101:6379

表示为 172.30.0.101:6379 代表的集群扩容。后续会交互式的输入要移动的哈希槽个数(即新的分区占有多少哈希槽)、将哈希槽分配给谁、从哪些节点中分配出这些哈希槽。后续会涉及哈希槽的重新分配以及数据的移动。

在这个搬运的过程中,未被搬运的 key 是完全可以正常访问的。前面我们说过,我们拿一个 key 去访问时,可以自动路由到储存的主节点。现在因为处在搬运的过程中,所有到了该节点之后可能会发现 key 已经被搬运走了,而这时还拿不到新的哈希槽的情况,所有这些 key 的访问会失败。不过搬运结束后,所有 key 就都恢复正常了

添加子节点

现在我们已经完成主节点的添加和哈希槽的分配。为了增加主节点的稳定性和读能力,还要为他添加子节点,使用以下命令

redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --clusterslave
--cluster-master-id [172.30.1.110 节点的 nodeId]
Redis缓存

前面我们说过,Redis 可以作用于数据库前面,负责提供热点数据的访问,防止数据库服务器因压力过大而故障。那么 Redis 怎样获得这些热点数据呢?或者说,怎样知道哪些数据是热点数据呢?

从策略上可以分为两种,第一种是定期更新,第二种是实时更新

定期更新是每隔一段固定时间,从日志(或者其他形式)中做降序排序,统计出最经常访问的数据。在服务器中,可以通过 shell 脚本的方式,自动进行。定期更新的实现简单,但是对于突发情况下的热点数据的支持并不好,比如突发新闻事件热点、春节当天的春晚一样

实时更新让访问的数据都在 Redis 中。客户端访问时,发现Redis中没有,就去数据库中找,然后再把这个Redis中没有的数据加入进入,以此进行。

Redis内存刷新策略

实时更新可以保证热点数据的实时性。但是我们不难发现一个问题——Redis 中存储的数据一直都在增加啊?占满了主机的内存或者配置文件中设定的 maxmemory 怎么办?

通用的淘汰策略,主要有以下几种:

FIFO,First In First Out,最早到来的数据,最早被淘汰

LRU,Least Recently Used,上次使用时间最早的数据,最早被淘汰

LFU,Least Frequently Used,使用次数最少的数据,最早被淘汰

Random,所有的数据,随机淘汰

Redis 在配置文件中,为我们提供了以上几种策略

volatile-lru -> Evict using approximated LRU, only keys with an expire set.
allkeys-lru -> Evict any key using approximated LRU.
volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
allkeys-lfu -> Evict any key using approximated LFU.
volatile-random -> Remove a random key having an expire set.
allkeys-random -> Remove a random key, any key.
volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
noeviction -> Don't evict anything, just return an error on write operations.
  • allkeys-*,就是将对应的策略作用范围在全体 key 上

  • volatile-*,所有设置了过期时间的 key,按照相应的策略进行淘汰

  • volatile-ttl,删除最早过期的key

  • noeviction,默认策略,内存满时不进行内存淘汰,报错

Redis 缓存注意事项

缓存预热

在 Redis 服务器刚刚启动的时候,里面是还没有数据的,这时候请求还是会涌入到我们的数据库中。那么在这个短时间内,Redis 是没有对服务器起到很好的保护作用的。所以,可以在 Redis上线以前,先在 Redis中导入一部分热点数据,可以通过之前定期记录的数据,也可以是通过统计的得到的热点数据。

不用那么精确,核心目标就是起到保护数据库的作用,后续在不断访问的过程中,Redis 中的数据会逐渐更新成最新的热点数据的

缓存穿透(cache penetration)

前面我们提到过,当一个数据在 Redis 中不存在时,会到数据库中进行查找。当这种情况大量发生时,依然可以对数据库造成不小的压力,这种情况就是缓存穿透。

缓存穿透主要发生在以下情形中

  1. 缺少必要的数据检查,导致大量非法数据到达 Redis,进而由数据库处理
  2. 运维或者开发人员误删数据
  3. 黑客攻击

解决方式其实主要围绕数据合法性的确认来实现。一种方式是把不合法数据以键值为空的方式加到 Redis 中,另一种方式是通过布隆过滤器完成。

缓存雪崩

在 Redis 中,可能会出现突然大量 key 过期的情况,导致缓存命中率陡然下降。这时,大量访问数据都会涌向数据库,这种情况就叫做缓存雪崩,主要可能出现在以下情况中

  1. Redis集群大量节点下线或者直接挂了
  2. 给大量 key 设置了相同的过期时间,导致大量 key 同时过期

解决方式大概分为两种

  1. 不设置过期时间或者设置过期时间时通过随机因子使得过期时间不一致
  2. 增加对 Redis 集群的监控机制,保证 Redis 集群的高可用

缓存击穿(cache breakdown)

缓存击穿 和 缓存穿透,两个在中文名称上,可能差别不大,但是英文名称能很好地体现出两者的区别。击穿是一个点击穿,对应到 cache breakdown,就是一个热点数据过期,导致大量请求该热点数据的请求都被发送到数据库处理(例如前面提到的除夕夜的春晚,就会瞬时对数据库造成极大的压力)

解决这种问题,可以从两方面解决

  1. 统计热点的 key,并将这个 key 设置为永不过期。虽然从结果上解决效果是最好的,但是这可能需要服务器结构做出很大调整。
  2. 通过类似于手机“省电模式”的思想,引入分布式锁,降低数据库面临的最大访问量。虽然会增加客户端访问数据的等待时间,但是总比数据库挂了所有人都访问不了要好的多
分布式锁

在同一个机器的进程中,我们可以采用比如 C++ 的 std::mutex 来完成线程安全的操作,但是涉及到分布式的而架构时,连机器都不是同一台,所以需要采取新的方案来解决分布式系统中多个进程之间的数据安全问题。

Redis 采取的解决方案是,引入一个或几个服务器作为分布式锁,当一个修改请求被下达时,会在这个服务器中设置一个特殊的键值对,这样就完成了锁的获取;后续其他客户端发出同样请求时,依然会尝试建立该键值对,如果其已经存在,就说明未能成功持有锁。至于后续是阻塞还是放弃,就要看具体的实现了

引入 setnx

setnx 可以做到原子性的设置一个 key,设置了之后,其他客户端都不能重复设置该key,释放锁时可以通过 del 这个 key 的方式完成锁的释放。

但是这里还存在一个问题,那就是设置锁了之后不能保证锁一定能够及时释放(比如服务器直接断电),这时就会造成死锁。所以可以通过 set key value nx ex …seconds 的方式通过一条指令完成锁的获取和过期时间的设置,这样即使之后因为特殊原因无法及时归还锁,也不会造成死锁的问题。(也不建议使用两条命令,因为不能保证第二条命令一定成功;使用一条命令的话,就一定能够完成过期时间的设置)

关于校验机制

在某个持有锁的服务器设置了上述键值对时,可能有其他服务器意外删除了这个键值对,导致锁机制失效。为了避免这种情况,在上述的键值对中,我们可以将 value 设置为获取锁的服务器的编号。后续其他服务器尝试删除该锁时,先去获取锁的键值对,如果内获取到的话,先不直接进行删除(解锁),而是查看拿到的键值中的编号是否是本主机的编号。如果是的话,那么久直接删除(解锁),否则不进行删除操作。

这个校验机制,其实是通过几个服务器之间完成的,而不是 Redis 本身

引入 Lua 脚本

我们来看下面的情景。

现在有A、B两个服务器。我们分布式锁的解锁过程从前面就可以看出来,是分为 get、set 两步的。现在假设 A 服务器是之前获取的锁,现在有两个线程要释放锁。线程一 get 成功,紧接着线程二 get 成功,线程一 del,在线程二 del 之前,服务器 B 又设置了锁键值对,但是这时线程二又进行了 del,就会删掉服务器 B 刚刚设置的锁键值对,就等于 服务器 B 白设置了。(前面我们说过,Redis 的分布式锁的校验机制是在访问的服务器而不是 Redis 服务器本身实现,这个校验发生在第一步—— get,在上面的情况中,服务器 B 的 setnx ex 是在两个线程 get 之后的,所以校验机制也无可奈何)

Lua 是一种很轻量的语言,Redis 网站上也有说可以通过 Lua 实现更好的事务逻辑,并且 Redis 对Lua脚本的执行是原子的,所以可以解决这里的问题

引入看门狗

前面我们提到过,可以通过 set nx ex 的方式完成锁的定期是释放。但是这个时间设置多长呢?一方面,设置的太短,服务器还没干完活,就要释放锁了;另一方面,设置的太长,会形成一段时间的死锁,这期间其他服务器都无法获取锁

所以可以通过看门狗 Watch Dog,引入自动延时机制。类似于吃自助餐一样,少拿多取,每次时间快到了,都再续上一秒。这样即使服务器挂了没来得及自己释放,也会因为看门狗挂了,而在很短的时间内自动完成锁的释放

RedLock

还有一个问题:提供分布式锁的服务器挂了怎么办?可以通过集群 + 主从复制 + 哨兵的机制(还加上集群的原因是主从复制也会存在延时)解决这个问题,这也是 Redis 作者提供的一种方案。

每次获取锁的时候,如果一半的服务器都成功完成了 key 的设置,就认为锁设置成功;同样,如果释放锁的时候对每个服务器也都进行 del 的操作

Logo

更多推荐