当前位置: 首页 > 科技观察

跟随刚静视角看懂Redis的主从复制

时间:2023-03-18 18:14:52 科技观察

本文转载自微信公众号《SH的全栈笔记》,作者SH。转载本文请联系SH全栈笔记公众号。虽然Redis在单机上的性能非常好,而且有完备的持久化机制,但是如果你的业务量真的很大,超过了单机承载的上限怎么办?Redis不做任何处理就挂了怎么办?带着这个疑问,开始我们今天的话题——《Redis高可用》。限于篇幅,本章只讲主从复制。为什么先从主从复制说起,因为“主从复制”可以说是整个Redis高可用实现的基石。你可以先有这样一个概念。至于为什么是基石,后面会讲到Sentinel和Redis。聚类的时候会提到。首先我们要知道,对于我们开发者来说,为什么需要“主从架构”?Redis实例不工作吗?其实除了开头提到的负载超过了Redis单机可以承受的上限之外,还有一种情况是Redis无法保证自身的高可用。也就是说,即使Redis能够承载所有的流量,如果Redis进程所在的机器挂掉了怎么办?请求直接转枪,大量流量瞬间挂掉你的DB,然后你就可以扛个P0打包回家了。而且,假设你对Redis的需求真的超过了单机的能力,你该怎么办?你有多个独立的Redis实例吗?那么如果这次用户缓存的数据存在于实例1中,如果下次用户再次访问该实例第二,要不要再走一遍DB?除非你能维护用户和Redis实例的对应关系(但通常这样的逻辑比较复杂),否则部署多个Redis实例就失去了意义,横向扩展也没办法。这个问题可以通过改成主从架构来解决吗?我们可以从一张图上直观的理解。Redis主从复制在主从同步中,我们将节点的角色分为master和slave,形成“一主多从”。slave对外提供读操作,而master负责写操作,形成读写分离的架构,从而承载更多的业务请求。在大多数业务场景下,Redis的“读操作”多于“写操作”,所以当读请求量特别大的时候,我们可以增加从节点的数量,让Redis能够处理更多的流量。不行啊哥们,你要是往master上写数据,那我要是连到slave上,不就拿不到之前的数据了吗?这个字幕不是我写的吗?“主从复制”,slave会按照某种策略从master同步数据。在Redis中,我们可以使用slaveof命令让一个Redis实例复制(replicate)另一个Redis的状态。复制的Redis实例为主节点,执行slaveof命令的机器为从节点。Redis的主从复制分为两个步骤,分别是“同步”和“命令传播”。“同步操作”用于将主节点的内存状态复制到从节点,而“命令传播”是指在同步过程中,客户端执行一些“写入”操作来改变服务器的状态。此时主节点的状态与同步操作在执行时不一致,因此需要传播命令来调和主从状态。同步的一般过程如下:从节点向主节点发送同步命令。master收到sync命令后,会执行bgsave命令,Redis会在后台fork出一个子进程生成RDB文件,并将同步过程中的写命令记录到buffer中,待zone中的文件生成后,master会把RDB文件发给slave,收到server发来的RDB文件后,会加载到内存中,然后master会把buffer中记录的所有写命令发给slave,slave会“重放”这些命令“,更新自己数据库的状态,与master保持一致。为了让大家更清楚地了解这个过程,我们通过图片来看一下Redis主从复制,slave同步完成后挂掉怎么办?slave重启后,很可能又和master不一致了?确实是这样的,这就涉及到一个术语,叫“断点续传”。上面讨论是slave第一次连接master,会进行“全量复制”。对于以上情况,Redis新老版本的处理方式不同。在Redis2.8之前,master-slave完成同步后,如果slave断开重连,则向master发送sync命令,master会再次向slave发送全量数据。但是我们会发现一个问题,就是大部分数据都是有序的,没必要再全额同步。Redis2.8之后,为了解决这个问题,使用psync命令代替了sync。简单来说,psync命令就是在slave断开连接期间,将master收到的所有写命令发送给slave,重放后slave的状态会和master保持一致。呵呵,就这?你知道psync是怎么实现的吗?还是只是使用?psync的实现依赖于master和slave共同维护的offset偏移量。master每次对slave进行“命令传播”,传播了多少字节的数据,它就在传播的字节数上加上自己的偏移量。从机每接收到多少字节的数据,它也会用同样的方式更新自己的偏移量。基于偏移量,只需要简单的比较就可以知道master和slave的当前状态是否一致,然后根据offset,将对应偏移量对应的指令传播给slave进行replay。所以即使slave在同步的时候挂掉了,根据偏移量,也可以达到恢复传输的效果。不不不,那师傅也挂了?当你的slave重启后,master的数据也会更新。按照你的说法,这两个数据永远不会一致。Redis确实考虑过这个问题。其实除了offset之外,slave断开重连后,还会带上之前master实例的runid。每个服务实例都有自己唯一的runid。只要重启Redis服务,它的runid就会改变。master收到这个runid后,会判断是否和自己当前的runid一致。如果一致,则说明在断开连接之前已经建立了与自身的连接。如果不一致,说明master在slave断开的过程中也crash了。这时候,你需要将数据“完全同步”到slave。就算你用redis-runid可以解决这个问题,但是你维护了一个偏移量,那么这个偏移量对应的命令从哪里来呢?是从天上掉下来的吗?我在哪里知道这些命令是什么?确实,我们需要通过这个偏移量来获取真正需要的数据——也就是指令,而Redis是通过“复制积压缓冲区”来实现的。名字是高大上,其实是队列。就像递归、轮询、透传,听上去很高端,其实很简单。言归正传,复制积压缓冲区的默认大小是1M。当Redis进行“命令传播”时,除了向slave发送写命令外,还会将命令写入“replicationbacklogbuffer”,并与当前偏移量相关联。这样就可以通过offset得到对应的command了。redis-backlog但是由于buffer的大小有限,如果slave断开连接的时间过长,replicationbacklogbuffer中较早的指令已经被新的指令覆盖,这里可以理解为一个队列,earlier入队的元素已经出队。由于没有对应的偏移量,所以无法获取到指令数据。这时候Redis就会进行“全同步”。当然,如果offset在replicationbacklogbuffer中还存在,就会根据对应的offset进行“部分同步”。基于上面的全量和增量主从复制,可以在master出现故障时进行主从切换,保证服务的正常运行。此外,还可以解决异常情况下数据丢失的问题。基于读写分离的策略也可以提高整个Redis服务的并发度。别吹牛了,你说的“主从复制”难道就没有缺点吗?其实还有,比如刚才说的主从切换。如果不使用现有的“HA”框架,这个过程需要程序员自己手动完成,同时通知服务调用方Redis的IP发生了变化。这个过程可以说是非常复杂的,甚至可能涉及代码配置的改动。而且,之前的slave复制的都是死掉的master,想要把自己复制的master库改到slave上就更复杂了。另外,虽然实现了读写分离,但是由于“一主多从”的架构,可以扩展集群的“读请求”,但是“写请求”的并发度是有上限的,即,主人可以承载的活的上限,这个是没有办法扩大的。