目录 

一、Redis主从复制 

full resync(全量复制)

partial resync(增量复制)

二、Redis主从复制配置

三、哨兵机制原理

每个 Sentinel 都需要定期执行的任务

自动发现 Sentinel 和从服务器

Sentinel 选主规则

四、哨兵机制配置

五、哨兵机制整合SpringBoot


一、Redis主从复制 

               主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者为主节点(master),后者称为从节点(slave);数据的复制只能由主节点到从节点。主从复制保证了数据的备份,高可用(配合哨兵机制)、负载均衡(读写分离分担master压力)。

Redis主从数据同步有两种方式,(full resync)全量复制和增量复制(partial resync)

full resync(全量复制)

Redis通过psync命令进行全量复制的过程如下:

  1. 从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制,就进行全量复制。
  2. 主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令
  3. 主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态
  4. 主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态
  5. 如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态

partial resync(增量复制)

由于网络原因导致主从服务器断开连接,当主从重新连接之后,不需要全量复制,只需要进行增量复制。因为主从服务器都会维持一个offset(偏移量),当连接恢复之后,对比两者的偏移量,把不同的数据同步过来。

在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给积压缓冲区,作为写命令的备份;除了存储写命令,积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出(队列),所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。

由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size 1M 默认是1M);

  • 如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
  • 如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。

每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid。

主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

二、Redis主从复制配置

配置方式有两种,这种方式不推荐,如果从服务器过多,数据同步效率很差

springboot redis 主从 获取的地址不对 springboot redis主从复制_服务器

采用树状结构

 配置主从复制

由于我是在一台虚拟机上模拟所以需要修改响应的端口号为6379,6380,6381

 修改响应的 pidfile /var/run/redis_6379.pid,pidfile /var/run/redis_6380.pid,pidfile /var/run/redis_6381.pid

在6380配置文件添加指向主节点

slaveof 192.168.139.154 6379
#密码
masterauth xiaojie

在6381上修改配置文件如下

slaveof 192.168.139.154 6380
masterauth xiaojie

 启动redis实例

#由于是同一台虚拟机,启动时候需要指定端口,不然默认是6379
[root@bogon bin]# ./redis-cli -p 6379
[root@bogon bin]# ./redis-cli -p 6380
[root@bogon bin]# ./redis-cli -p 6381

输入指令

127.0.0.1:6379>  info replication

springboot redis 主从 获取的地址不对 springboot redis主从复制_Redis_02

springboot redis 主从 获取的地址不对 springboot redis主从复制_redis_03

springboot redis 主从 获取的地址不对 springboot redis主从复制_Redis_04

replica-read-only yes(yes只能读不能写)。这个时候如果主节点宕机之后,从节点不能写,然后服务就不能用了,于是引入哨兵机制。 

三、哨兵机制原理

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。

每个 Sentinel 都需要定期执行的任务

  • 每个 Sentinel 以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令。
  • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线(subjectively down,简称 SDOWN )。 一个有效回复可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。
  • 如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。
  • 如果一个主服务器被标记为主观下线, 并且有足够数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线(objectively down, 简称 ODOWN)。
  • 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。 当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  • 当没有足够数量的 Sentinel 同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向 Sentinel 的 PING 命令返回有效回复时, 主服务器的主观下线状态就会被移除。

自动发现 Sentinel 和从服务器

一个 Sentinel 可以与其他多个 Sentinel 进行连接, 各个 Sentinel 之间可以互相检查对方的可用性, 并进行信息交换。

你无须为运行的每个 Sentinel 分别设置其他 Sentinel 的地址, 因为 Sentinel 可以通过发布与订阅功能来自动发现正在监视相同主服务器的其他 Sentinel , 这一功能是通过向频道 sentinel:hello 发送信息来实现的。

与此类似, 你也不必手动列出主服务器属下的所有从服务器, 因为 Sentinel 可以通过(info)询问主服务器来获得所有从服务器的信息。

  • 每个 Sentinel 会以每两秒一次的频率, 通过发布与订阅功能, 向被它监视的所有主服务器和从服务器的 sentinel:hello 频道发送一条信息, 信息中包含了 Sentinel 的 IP 地址、端口号和运行 ID (runid)。
  • 每个 Sentinel 都订阅了被它监视的所有主服务器和从服务器的 sentinel:hello 频道, 查找之前未出现过的 sentinel (looking for unknown sentinels)。 当一个 Sentinel 发现一个新的 Sentinel 时, 它会将新的 Sentinel 添加到一个列表中, 这个列表保存了 Sentinel 已知的, 监视同一个主服务器的所有其他 Sentinel 。
  • Sentinel 发送的信息中还包括完整的主服务器当前配置(configuration)。 如果一个 Sentinel 包含的主服务器配置比另一个 Sentinel 发送的配置要旧, 那么这个 Sentinel 会立即升级到新配置上。
  • 在将一个新 Sentinel 添加到监视主服务器的列表上面之前, Sentinel 会先检查列表中是否已经包含了和要添加的 Sentinel 拥有相同运行 ID 或者相同地址(包括 IP 地址和端口号)的 Sentinel , 如果是的话, Sentinel 会先移除列表中已有的那些拥有相同运行 ID 或者相同地址的 Sentinel(自己发布的消息) , 然后再添加新 Sentinel 。

Sentinel 选主规则

  • 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复PING 命令的时间大于五秒钟的从服务器都会被淘汰。
  • 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。
  • 在经历了以上两轮淘汰之后剩下来的从服务器中, 我们选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器; 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID(runid) 的那个从服务器成为新的主服务器。

四、哨兵机制配置

复制redis解压文件中的sentinel.conf文件到安装目录的bin下面

[root@bogon bin]# cp /usr/local/redis-6.2.5/sentinel.conf /usr/local/redis6379/bin/
[root@bogon bin]# cp /usr/local/redis-6.2.5/sentinel.conf /usr/local/redis6380/bin/
[root@bogon bin]# cp /usr/local/redis-6.2.5/sentinel.conf /usr/local/redis6381/bin/
[root@bogon bin]#

 修改sentinel.conf

#后台启动
daemonize yes
#修改端口
port 26379-26381 
#指定pid
 pidfile /var/run/redis-sentinel-6379.pid
#指定监听的主节点 2是至少有2个哨兵认为宕机的个数
sentinel monitor mymaster 192.168.139.154 6379 2
#指定主节点密码
sentinel auth-pass mymaster xiaojie
#指定哨兵的密码
requirepass xiaojie
#打开这个注释
protected-mode no

启动哨兵

./redis-sentinel sentinel.conf

 然后手动宕机6379服务节点。

启动6379的节点再检查info replication 

springboot redis 主从 获取的地址不对 springboot redis主从复制_Redis_05

springboot redis 主从 获取的地址不对 springboot redis主从复制_服务器_06

springboot redis 主从 获取的地址不对 springboot redis主从复制_redis_07

  此时6380节点被选举为master,6379原来的master成为了slave。

五、哨兵机制整合SpringBoot

来 上才艺,不对 !上代码

springboot redis 主从 获取的地址不对 springboot redis主从复制_redis_08

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xiaojie</groupId>
    <artifactId>springboot-redis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.4.2</version>
        <relativePath/>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.12</version>
        </dependency>
<!--序列化-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.10.4</version>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <!--lombok-->
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

配置文件

spring:
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
  datasource:
    name: iot-home
    url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    password: xiaojie #这个密码一定要加,然后才能保证能连接到redis服务器,如果没有配置密码则不需要
    connect-timeout: 5000
    database: 0
    sentinel:
      master: mymaster
      nodes: 192.168.6.137:26379,192.168.6.137:26380,192.168.6.137:26381
      password: xiaojie #这个密码是哨兵的密码,如果在sentinel.conf中没有配置requirepass则不需要

核心代码 

package com.xiaojie.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.net.UnknownHostException;

/*
 *
 * @param null
 * @redis配置
 * @author xiaojie
 * @date 2021/9/8 
 * @return
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
public User getUserByName(String name) {
        JSONObject obj= (JSONObject) redisUtil.get(USERKEY + ":" + name);
        if (null==obj){
            System.out.println("缓存中没有该值,查询数据库");
            User resultUser = userMapper.selectByName(name);
            if (null!= resultUser) {
                redisUtil.set(USERKEY+":"+resultUser.getName(), JSONObject.toJSON(resultUser),30);
                return resultUser;
            }
        }
        User user = JSONObject.toJavaObject(obj,User.class);
        return user;
    }

测试:宕机主节点,然后还能继续写入缓存,搭建完成。