SpringBoot2.0整合Redis高可用之Sentinel哨兵

简介: 本篇博文分享的是一主二从三哨兵模式。至于为什么用三个哨兵,同第一段。本文是模拟环境,都是一个服务器上面。

Sentinel是什么?Redis高可用是什么?


本篇博文分享的是一主二从三哨兵模式。至于为什么用三个哨兵,同第一段。本文是模拟环境,都是一个服务器上面。


【1】POM文件和配置

① pom文件

<parent>
<groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.1.1.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.boot.redis</groupId>
<artifactId>boot-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-redis</name>
<description>Demo project for Spring Boot</description>
<properties>
  <java.version>1.8</java.version>
</properties>
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
      <exclusion>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
      </exclusion>
      <exclusion>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.5.0</version>
    <!--<version>2.4.2</version>-->
  </dependency>
</dependencies>


② yml简单配置

spring:
  redis:
    host: 192.168.2.110 #哨兵模式下不用配置
    port: 6379 # 哨兵模式下不用配置
    password: admin
    jedis:
      pool:
        #最大连接数
        max-active: 1024
        #最大阻塞等待时间(负数表示没限制)
        max-wait: 20000
        #最大空闲
        max-idle: 200
        #最小空闲
        min-idle: 10
    sentinel:
      master: mymaster
      nodes: 192.168.2.110:26379,192.168.2.110:26380,192.168.2.110:26381
server:
  port: 8088

至于客户端使用jedis或者lettuce等相关详细配置,这里回头更新。本文重点不在这里。


③ redis服务配置

6379.conf(默认为master):

port 6379
masterauth admin
requirepass admin
protected-mode yes
# 服务器ip地址
bind 192.168.2.110


6380配置(slave):

port 6380
masterauth admin
requirepass admin
protected-mode yes
# 服务器ip地址
bind 192.168.2.110
slaveof 192.168.2.110 6379

6381同6380,只是端口不一致。若找不到在redis.conf中对应位置,博文末尾GitHub中已经提交相关配置。


④ 哨兵配置

sentinel.conf(默认端口26379):

port 26379
sentinel monitor mymaster 192.168.2.110 6379 2
sentinel auth-pass mymaster admin
sentinel down-after-milliseconds mymaster 15000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 80000
bind 192.168.2.110
protected-mode yes

其他两个哨兵配置文件类似,只是需要修改一下端口分别为26380和26381。


⑤ RedisConfig类

@Configuration
@EnableAutoConfiguration
public class RedisConfig {
    private static Logger logger = LoggerFactory.getLogger(RedisConfig.class);
    @Value("#{'${spring.redis.sentinel.nodes}'.split(',')}")
    private List<String> nodes;
    @Bean
    @ConfigurationProperties(prefix="spring.redis")
    public JedisPoolConfig getRedisConfig(){
        JedisPoolConfig config = new JedisPoolConfig();
        return config;
    }
    @Bean
    public RedisSentinelConfiguration sentinelConfiguration(){
        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
        //配置matser的名称
        redisSentinelConfiguration.master("mymaster");
        //配置redis的哨兵sentinel
        Set<RedisNode> redisNodeSet = new HashSet<>();
        nodes.forEach(x->{
            redisNodeSet.add(new RedisNode(x.split(":")[0],Integer.parseInt(x.split(":")[1])));
        });
        logger.info("redisNodeSet -->"+redisNodeSet);
        redisSentinelConfiguration.setSentinels(redisNodeSet);
        return redisSentinelConfiguration;
    }
    @Bean
    public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig,RedisSentinelConfiguration sentinelConfig) {
        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(sentinelConfig,jedisPoolConfig);
        return jedisConnectionFactory;
    }
}


然后在服务器依次启动redis服务6379、6380、6381和哨兵服务26379/26380以及26381。


此时可以通过命令窗口查看主从以及哨兵状况,可以使用命令进行主从切换,查看哨兵选举。


另外将项目打包放到服务器运行,查看当master挂掉时,SpringBoot所获取到的信息与转变。


【2】几点说明

① 哨兵之间通信

哨兵启动后,相互之间是保持通信的:


如下所示,一个哨兵挂掉重启后将会从其他哨兵获取最新信息:

② 当master挂掉之后,三个哨兵需要选举出来一个leader进行主从转移


③ 如果哨兵挂掉,则MasterListener将会隔5秒重试

④ 如果master挂掉,MasterListener将会通过Sentinel获取最新的master信息并重置JedisSentinelPool

所以不用担心master挂掉,SpringBoot会怎么办。有MasterListener在,一切OK。

⑤ 哨兵配置动态修改

哨兵启动后,配置文件是会被动态改变的,如下所示:

port 26379
sentinel myid 8ac857af0b63edc73e07cce9c11462eef047704d
sentinel monitor mymaster 127.0.0.1 6380 2
sentinel down-after-milliseconds mymaster 15000
sentinel failover-timeout mymaster 80000
sentinel auth-pass mymaster admin
bind 192.168.2.110
protected-mode yes
# Generated by CONFIG REWRITE
dir "/root"
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 1
sentinel known-slave mymaster 192.168.2.110 6379
sentinel known-slave mymaster 127.0.0.1 6381
sentinel known-sentinel mymaster 192.168.2.110 26381 b3d9ee9efae47556349a733ed668385d8083b5cc
sentinel known-sentinel mymaster 192.168.2.110 26380 e7a100d97d70834ea3b117cf23a4b08f7a46b425
sentinel current-epoch 1



【3】几个类

这里面有几个类十分重要 ,建议查看源码跟踪一番。

① JedisConnectionFactory


时序图如下:

重点在步骤12中,MasterListener继承了Thread,它是个单独的线程,通过Jedis监听Redis的哨兵,如果收到master改变的消息,那么会修改JedisFactory。


如下图所示是MasterListener收到master节点变化的消息:

想要debug跟踪源码的同学可以在下图处打上断点:


② JedisSentinelPool

其构造方法如下:

  public JedisSentinelPool(String masterName, Set<String> sentinels,
      final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
      final String password, final int database, final String clientName) {
    this.poolConfig = poolConfig;
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.password = password;
    this.database = database;
    this.clientName = clientName;
    HostAndPort master = initSentinels(sentinels, masterName);
    initPool(master);
  }

其中initSentinels(sentinels, masterName)就是根据哨兵和master名字来获取master的信息–host和port。然后根据获取的maser来初始化pool(所以yml中不用配置spring.redis.host和spring.redis.port)。


从应用启动也可以看到这一点:

获取HostAndPort master方法如下所示:

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
    HostAndPort master = null;
    boolean sentinelAvailable = false;
    log.info("Trying to find master from available Sentinels...");
//遍历sentinels,获取master,然后基于该master创建pool
    for (String sentinel : sentinels) {
      final HostAndPort hap = HostAndPort.parseString(sentinel);
      log.fine("Connecting to Sentinel " + hap);
      Jedis jedis = null;
      try {
        jedis = new Jedis(hap.getHost(), hap.getPort());
//根据哨兵的host port 以及masterName获取masterAddr 
        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
        // connected to sentinel...
        sentinelAvailable = true;
        if (masterAddr == null || masterAddr.size() != 2) {
          log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
              + ".");
          continue;
        }
//转换格式
        master = toHostAndPort(masterAddr);
        log.fine("Found Redis master at " + master);
        break;
      } catch (JedisException e) {
        // resolves #1036, it should handle JedisException there's another chance
        // of raising JedisDataException
        log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
            + ". Trying next one.");
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }
    if (master == null) {
      if (sentinelAvailable) {
        // can connect to sentinel, but master name seems to not
        // monitored
        throw new JedisException("Can connect to sentinel, but " + masterName
            + " seems to be not monitored...");
      } else {
        throw new JedisConnectionException("All sentinels down, cannot determine where is "
            + masterName + " master is running...");
      }
    }
    log.info("Redis master running at " + master + ", starting Sentinel listeners...");
//如下,启动每个哨兵的监听
    for (String sentinel : sentinels) {
      final HostAndPort hap = HostAndPort.parseString(sentinel);
      MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
      // whether MasterListener threads are alive or not, process can be stopped
      masterListener.setDaemon(true);
      masterListeners.add(masterListener);
      masterListener.start();
    }
    return master;
  }

初始化pool的方法如下所示:

private void initPool(HostAndPort master) {
    if (!master.equals(currentHostMaster)) {
      currentHostMaster = master;
      //判断JedisFactory是否为null,为null则使用master创建
      if (factory == null) {
        factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
            soTimeout, password, database, clientName, false, null, null, null);
        initPool(poolConfig, factory);
      } else {
        factory.setHostAndPort(currentHostMaster);
        // although we clear the pool, we still have to check the
        // returned object
        // in getResource, this call only clears idle instances, not
        // borrowed instances
        internalPool.clear();
      }
      log.info("Created JedisPool to master at " + master);
    }
  }

③ MasterListener

MasterListener 是一个继承自Thread 的线程类,用来监听哨兵的master改变信息。 收到master改变的消息后,调用initPool方法修改JedisFactory,进而会影响connection的获取。

protected class MasterListener extends Thread {
    protected String masterName;
    protected String host;
    protected int port;
    protected long subscribeRetryWaitTimeMillis = 5000;
    protected volatile Jedis j;
    protected AtomicBoolean running = new AtomicBoolean(false);
    protected MasterListener() {
    }
    public MasterListener(String masterName, String host, int port) {
      super(String.format("MasterListener-%s-[%s:%d]", masterName, host, port));
      this.masterName = masterName;
      this.host = host;
      this.port = port;
    }
    public MasterListener(String masterName, String host, int port,
        long subscribeRetryWaitTimeMillis) {
      this(masterName, host, port);
      this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis;
    }
//主要功能如下
    @Override
    public void run() {
      running.set(true);
      while (running.get()) {
//拿到一个Jedis
        j = new Jedis(host, port);
        try {
          // double check that it is not being shutdown
          if (!running.get()) {
            break;
          }
//发布订阅模式
          j.subscribe(new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
              log.fine("Sentinel " + host + ":" + port + " published: " + message + ".");
              String[] switchMasterMsg = message.split(" ");
              if (switchMasterMsg.length > 3) {
                if (masterName.equals(switchMasterMsg[0])) {
                //这里,重置initPool
                  initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
                } else {
                  log.fine("Ignoring message on +switch-master for master name "
                      + switchMasterMsg[0] + ", our master name is " + masterName);
                }
              } else {
                log.severe("Invalid message received on Sentinel " + host + ":" + port
                    + " on channel +switch-master: " + message);
              }
            }
          }, "+switch-master");
        } catch (JedisConnectionException e) {
          if (running.get()) {
            log.log(Level.SEVERE, "Lost connection to Sentinel at " + host + ":" + port
                + ". Sleeping 5000ms and retrying.", e);
            try {
              Thread.sleep(subscribeRetryWaitTimeMillis);
            } catch (InterruptedException e1) {
              log.log(Level.SEVERE, "Sleep interrupted: ", e1);
            }
          } else {
            log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
          }
        } finally {
          j.close();
        }
      }
    }
    public void shutdown() {
      try {
        log.fine("Shutting down listener on " + host + ":" + port);
        running.set(false);
        // This isn't good, the Jedis object is not thread safe
        if (j != null) {
          j.disconnect();
        }
      } catch (Exception e) {
        log.log(Level.SEVERE, "Caught exception while shutting down: ", e);
      }
    }
  }


目录
相关文章
|
5月前
|
NoSQL Java 网络安全
SpringBoot启动时连接Redis报错:ERR This instance has cluster support disabled - 如何解决?
通过以上步骤一般可以解决由于配置不匹配造成的连接错误。在调试问题时,一定要确保服务端和客户端的Redis配置保持同步一致。这能够确保SpringBoot应用顺利连接到正确配置的Redis服务,无论是单机模式还是集群模式。
532 5
|
NoSQL Java 关系型数据库
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
本文介绍在 Spring Boot 中集成 Redis 的方法。Redis 是一种支持多种数据结构的非关系型数据库(NoSQL),具备高并发、高性能和灵活扩展的特点,适用于缓存、实时数据分析等场景。其数据以键值对形式存储,支持字符串、哈希、列表、集合等类型。通过将 Redis 与 Mysql 集群结合使用,可实现数据同步,提升系统稳定性。例如,在网站架构中优先从 Redis 获取数据,故障时回退至 Mysql,确保服务不中断。
435 0
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
|
6月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
566 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
缓存 NoSQL Java
基于SpringBoot的Redis开发实战教程
Redis在Spring Boot中的应用非常广泛,其高性能和灵活性使其成为构建高效分布式系统的理想选择。通过深入理解本文的内容,您可以更好地利用Redis的特性,为应用程序提供高效的缓存和消息处理能力。
1225 79
|
NoSQL Java Redis
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
1344 83
|
9月前
|
机器学习/深度学习 数据采集 人机交互
springboot+redis互联网医院智能导诊系统源码,基于医疗大模型、知识图谱、人机交互方式实现
智能导诊系统基于医疗大模型、知识图谱与人机交互技术,解决患者“知症不知病”“挂错号”等问题。通过多模态交互(语音、文字、图片等)收集病情信息,结合医学知识图谱和深度推理,实现精准的科室推荐和分级诊疗引导。系统支持基于规则模板和数据模型两种开发原理:前者依赖人工设定症状-科室规则,后者通过机器学习或深度学习分析问诊数据。其特点包括快速病情收集、智能病症关联推理、最佳就医推荐、分级导流以及与院内平台联动,提升患者就诊效率和服务体验。技术架构采用 SpringBoot+Redis+MyBatis Plus+MySQL+RocketMQ,确保高效稳定运行。
691 0
|
存储 人工智能 NoSQL
SpringBoot整合Redis、ApacheSolr和SpringSession
本文介绍了如何使用SpringBoot整合Redis、ApacheSolr和SpringSession。SpringBoot以其便捷的配置方式受到开发者青睐,通过引入对应的starter依赖,可轻松实现功能整合。对于Redis,可通过配置RedisSentinel实现高可用;SpringSession则提供集群Session管理,支持多种存储方式如Redis;整合ApacheSolr时,借助Zookeeper搭建SolrCloud提高可用性。文中详细说明了各组件的配置步骤与代码示例,方便开发者快速上手。
225 11
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
384 36
|
NoSQL Java API
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Spring Boot 集成 Redis
本文介绍了在Spring Boot中集成Redis的方法,包括依赖导入、Redis配置及常用API的使用。通过导入`spring-boot-starter-data-redis`依赖和配置`application.yml`文件,可轻松实现Redis集成。文中详细讲解了StringRedisTemplate的使用,适用于字符串操作,并结合FastJSON将实体类转换为JSON存储。还展示了Redis的string、hash和list类型的操作示例。最后总结了Redis在缓存和高并发场景中的应用价值,并提供课程源代码下载链接。
2655 0
|
NoSQL Java Redis
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 安装
本教程介绍在 VMware 虚拟机(CentOS 7)或阿里云服务器中安装 Redis 的过程,包括安装 gcc 编译环境、下载 Redis(官网或 wget)、解压安装、修改配置文件(如 bind、daemonize、requirepass 等设置)、启动 Redis 服务及测试客户端连接。通过 set 和 get 命令验证安装是否成功。适用于初学者快速上手 Redis 部署。
376 0

热门文章

最新文章