Redis (十) 主从复制

先来简单了解下redis中提供的集群策略, 虽然redis有持久化功能能够保障redis服务器宕机也能恢复并且只有少量的数据损失,但是由于所有数据在一台服务器上,如果这台服务器出现硬盘故障,那就算是有备份也仍然不可避免数据丢失的问题。在实际生产环境中,我们不可能只使用一台redis服务器作为我们的缓存服务器,必须要多台实现集群,避免出现单点故。

  Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。

  如上图所示,我们可以将一台redis服务器作为主库,多台其他的服务器作为从库,主库只负责写数据,从库负责读数据,当主库数据更新时,会同步到它所有的从库。这就实现了主从复制,读写分离。既可以解决服务器负载过大的问题,又能够在一台服务器发生故障时及时使用其他服务器恢复数据。 

  下面演示一下redis中的主从复制,首先我们创建三份配置文件,分别对应端口号6379,6380,6381开启三个redis服务: 

  可以看到目前下面3台服务的角色(role)是master,也就是目前没有主从关系。

  由于我们现在没有配置主从库,所以可以通过命令info replication看到,三台服务器都分别是独立的主库。现在我们将端口号6379配置为主库(master),将端口号6380和6381配置为6379的从库,配置方法为在从库中执行命令:SLAVEOF 主库IP地址 主库端口号,或者直接在从机的配置文件中配置 SLAVEOF 主库IP地址 主库端口号,然后重启

   执行完SLAVEOF 命令以后我们再使用INFO REPLICATION 查看当前3台服务的关系,可以看到79服务为master ,其他两台为slave,说明主从关系建立完成,这里有个注意的点,就是在两台从机的配置文件里需要配置 masterauth password 主机密码,不然在建立主从关系的时候会提示认证失败。

   可以看到一开始3台服务的库里都是空的,在主机上set 值以后,可以直接在从机上get,可以看到下图,当在从机上set值的时候会报错,这是为什么呢?

   默认情况下,从库只能读取数据,执行写操作会报错:可以修改配置文件中的以下参数来配置从机是只读还是可写,这里推荐设置成yes 只读。

1 全量同步
  Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下: 
  1)从服务器连接主服务器,发送SYNC命令; 
  2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令; 
  3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令; 
  4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照; 
  5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令; 
  6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令; 
  完成上面几个步骤后就完成了slave服务器数据初始化的所有操作,savle服务器此时可以接收来自用户的读请求。master/slave 复制策略是采用乐观复制,也就是说可以容忍在一定时间内master/slave数据的内容是不同的,但是两者的数据会最终同步。具体来说,redis的主从同步过程本身是异步的,意味着master执行完客户端请求的命令后会立即返回结果给客户端,然后异步的方式把命令同步给slave。这一特征保证启用master/slave后 master的性能不会受到影响。
  但是另一方面,如果在这个数据不一致的窗口期间,master/slave因为网络问题断开连接,而这个时候,master是无法得知某个命令最终同步给了多少个slave数据库。不过redis提供了一个配置项来限制只有数据至少同步给多少个slave的时候,master才是可写的: min-slaves-to-write 3 表示只有当3个或以上的slave连接到master,master才是可写的 ,min-slaves-max-lag 10 表示允许slave最长失去连接的时间,如果10秒还没收到slave的响应,则master认为该slave以断开.
2 增量同步
  Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
  从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份master node会在内存中创建一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制。但是如果没有找到对应的offset,那么就会执行一次全量同步
  无硬盘复制
  前面我们说过,Redis复制的工作原理基于RDB方式的持久化实现的,也就是master在后台保存RDB快照,slave接收到rdb文件并载入,但是这种方式会存在一些问题
  1. 当master禁用RDB时,如果执行了复制初始化操作,Redis依然会生成RDB快照,当master下次启动时执行该RDB文件的恢复,但是因为复制发生的时间点不确定,所以恢复的数据可能是任何时间点的。就会造成数据出现问题
  2. 当硬盘性能比较慢的情况下(网络硬盘),那初始化复制过程会对性能产生影响
  因此2.8.18以后的版本,Redis引入了无硬盘复制选项,可以不需要通过RDB文件去同步,直接发送数据,通过以下配置来开启该功能repl-diskless-sync yes 。master**在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了。
3 Redis主从同步策略
  主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不功,要求从机进行全量同步。
 
1
slave-read-only yes

  将从库升级为独立的主库:

1
SLAVEOF NO ONE

  如果主库宕机,从库会“原地待命”,待主库重新连接之后,会恢复和主库的联系: 

   当主机重新启动,他依然会回到自己原先的角色,而自己的从机也依然在苦苦等待他回来,一旦发现他回来了,从机依然会在他手下效劳,但是,如果从库宕机,连接会断开,当从库重新连接后,需要重新建立与主库的连接: 如下图:

  一个库可以是一个库的从库,同时也可以是另一个库的主库,这样可以有效减轻master的压力,避免所有的从库都从一个主库中读取数据:

主从复制的原理:

  主库master和从库slave的复制分为全量复制和增量复制:

  全量复制:全量复制一般发生在slave初始化阶段,此时slave需要将master上的所有数据都复制一份,具体步骤如下:

  1. 从库连接到主库,并发送一条SYNC命令;
  2. 主库接收到SYNC命令后,开始执行BGSAVE命令生成RDB快照文件,并使用缓冲区记录此后执行的所有写命令;
  3. 主库执行完BGSAVE之后,将快照文件发送到所有从库,在此期间,仍继续将所有写命令记录到缓冲区;
  4. 从库在接收到快照文件后,丢弃所有旧数据,载入快照文件中的新数据;
  5. 主库继续向从库发送缓冲区中的写命令;
  6. 从库将快照文件中的数据载入完毕后,继续接收主库发送的缓冲区中的写命令,并执行这些写命令以更新数据。

  完成上面的步骤之后,从库可以开始接收来自用户的读数据请求。增量复制:增量复制是指,在slave初始化完成后的工作阶段,主库将新发生的写命令同步到从库的过程。主库每执行一条写命令,都会向从库发送相同的写命令,从库会执行这些写命令。

  总结:主库和从库初次建立连接时,进行全量复制;全量复制结束后,进行增量复制。但是当增量复制不成功时,需要发起全量复制。

主从复制的不足:

  主从模式解决了数据备份和性能(通过读写分离)的问题,但是还是存在一些不足:

  • RDB 文件过大的情况下,同步非常耗时。
  • 在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换成主服务器,这个比较费时费力,还会造成一定时间的服务不可用。

哨兵模式:

  哨兵模式是通过后台监控主库是否故障,当主库发生故障时,将根据投票数自动将某一从库转换为主库。

  下面演示启动哨兵模式的步骤: 首先创建文件sentinel.conf,目录自行选择,我选择在redis配置文件同目录下创建,并写入以下配置:
1
2
3
4
5
6
7
sentinel monitor host6399 192.168.254.137 6399 1 --配置监控的master节点
sentinel down-after-milliseconds host6399 5000 --表示如果5s内mymaster没响应,就认为SDOWN
protected-mode no -- 禁止保护
daemonize yes -- 后台运行
logfile "/var/log/sentinel_log.log"
sentinel failover-timeout host6399 15000 --表示如果15秒后,mysater仍没活过来,则启动failover,从剩下的slave中选一个升级为master
sentinel auth-pass host6399 wuzhenzhao -- 密码

  然后通过 /usr/local/redis/bin/redis-sentinel  /usr/local/redis/etc/sentinel.conf 启动哨兵会出现以下信息:

   此刻说明哨兵已经启动,接下去我让主机 6379 宕机,来演示主机宕机以后从机的反客为主。

  可以看到当主机宕机后,经过哨兵监控,发现主机宕机,会根据事先的配置文件里规则去选举新的master。这里选出来的是6380,如下图: 

  如果 master 被标记为下线,就会开始故障转移流程。既然有这么多的 Sentinel 节点,由谁来做故障转移的事情呢?故障转移流程的第一步就是在 Sentinel 集群选择一个 Leader,由 Leader 完成故障转移流程。Sentinle 通过 Raft 算法,实现 Sentinel 选举。
  在分布式存储系统中,通常通过维护多个副本来提高系统的可用性,那么多个节点之间必须要面对数据一致性的问题。Raft 的目的就是通过复制的方式,使所有节点达成一致,但是这么多节点,以哪个节点的数据为准呢?所以必须选出一个 Leader。大体上有两个步骤:领导选举,数据复制。Raft 是一个共识算法(consensus algorithm)。比如比特币之类的加密货币,就需要共识算法。Spring Cloud 的注册中心解决方案 Consul 也用到了 Raft 协议。
  Raft 的核心思想:先到先得,少数服从多数。
  为了解决master选举问题,又引出了一个单点问题,也就是哨兵的可用性如何解决,在一个一主多从的Redis系统中,可以使用多个哨兵进行监控任务以保证系统足够稳定。此时哨兵不仅会监控master和slave,同时还会互相监控;这种方式称为哨兵集群,哨兵集群需要解决故障发现、和master决策的协商机制问题.
sentinel之间的相互感知:
  sentinel节点之间会因为共同监视同一个master从而产生了关联,一个新加入的sentinel节点需要和其他监视相同master节点的sentinel相互感知,首先
  • 需要相互感知的sentinel都向他们共同监视的master节点订阅channel:sentinel:hello
  • 新加入的sentinel节点向这个channel发布一条消息,包含自己本身的信息,这样订阅了这个channel的sentinel就可以发现这个新的sentinel
  • 新加入得sentinel和其他sentinel节点建立长连接

哨兵机制的不足:

  主从切换的过程中会丢失数据,因为只有一个 master。只能单点写,没有解决水平扩容的问题。如果数据量非常大,这个时候我们需要多个 master-slave 的 group,把数据分布到不同的 group 中。

Jedis的基本操作:
public class JedisSentinelTest {
    private static JedisSentinelPool pool;

    private static JedisSentinelPool createJedisPool() {
        // master的名字是sentinel.conf配置文件里面的名称
        String masterName = "redis-master";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("192.168.1.101:26379");
        sentinels.add("192.168.1.102:26379");
        sentinels.add("192.168.1.103:26379");
        pool = new JedisSentinelPool(masterName, sentinels);
        return pool;
    }

    public static void main(String[] args) {
        JedisSentinelPool pool = createJedisPool();
        pool.getResource().set("name", "qq"+System.currentTimeMillis());
        System.out.println(pool.getResource().get("name"));
    }
}
posted @ 2020-10-09 03:01  跃小云  阅读(222)  评论(0编辑  收藏  举报