Spring Boot (13): интеграция Redis Sentinel, практика в режиме кластера

Spring Boot

Две предыдущие статьи (Схема сохранения Redis,Освойте три кластерных решения Redis в одной статье) соответственно представили схемы постоянства и кластеризации Redis, включая режим репликации master-slave, дозорный режим и режим кластера.Среди них режим репликации master-slave не может автоматически выполнять отработку отказа, и при сбое узла требуется вмешательство человека, что не соответствует требованиям производственной среды к высокой доступности, поэтому в производственной среде обычно используется дозорный режим или режим кластера. Итак, в проекте Spring Boot, как получить доступ к кластеру Redis в этих двух режимах и с какими проблемами можно столкнуться, — это то, что будет представлено в этой статье.

Spring Boot 2 интегрирует Redis

Очень просто интегрировать Redis в весеннюю загрузку, добавить зависимости в pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

весенний ботинок 2spring-boot-starter-data-redis, в качестве клиента Redis по умолчанию используется lettuce, основные отличия которого от jedis заключаются в следующем:

  1. Jedis является синхронным и не поддерживает асинхронный. Экземпляры клиента Jedis не являются потокобезопасными и требуют экземпляра Jedis для каждого потока, поэтому Jedis обычно используется через пулы соединений.
  2. Lettuce — это клиент Redis, управляемый событиями, основанный на платформе Netty.Вызовы его методов асинхронны, а API Lettuce также является потокобезопасным, поэтому несколько потоков могут управлять одним соединением Lettuce для выполнения различных операций, а Lettuce также поддерживает пулы соединений.

Если вы не используете салат по умолчанию и используете jedis, вы можете исключить зависимость от салата и вручную добавить зависимость от jedis.Конфигурация выглядит следующим образом

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

Добавьте конфигурацию в файл конфигурации application.yml (для одного экземпляра)

spring:
  redis:
    host: 192.168.40.201
    port: 6379
    password: passw0rd
    database: 0 # 数据库索引,默认0
    timeout: 5000  # 连接超时,单位ms
    jedis:  # 或lettuce, 连接池配置,springboot2.0中使用jedis或者lettuce配置连接池,默认为lettuce连接池
      pool:
        max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
        max-wait: -1 # 连接池分配连接最大阻塞等待时间(阻塞时间到,抛出异常。使用负值表示无限期阻塞)
        max-idle: 8 # 连接池中的最大空闲连接数
        min-idle: 0 # 连接池中的最小空闲连接数

Затем добавьте класс конфигурации. Аннотация @EnableCaching предназначена для того, чтобы аннотации @Cacheable, @CacheEvict, @CachePut, @Caching вступили в силу.

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 使用Jackson2JsonRedisSerialize 替换默认的jdkSerializeable序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

Приведенный выше класс конфигурации внедряет пользовательский RedisTemplate для замены автоматически настроенного класса RedisTemplate в RedisAutoConfiguration (RedisAutoConfiguration также автоматически настраивает StringRedisTemplate).

На этом этапе мы можем использовать кэширование, определив класс инструмента на основе RedisTemplate или добавив аннотации @Cacheable, @CacheEvict, @CachePut и @Caching на уровень службы. Например, определите класс RedisService для инкапсуляции общих методов операций Redis.

@Component
@Slf4j
public class RedisService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 指定缓存失效时间
     *
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            log.error("exception when expire key {}. ", key, e);
            return false;
        }
    }

    /**
     * 根据key获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key  键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            log.error("exception when check key {}. ", key, e);
            return false;
        }
    }

   ...
}

Для экономии места полный код можно найти в образце исходного кода этой статьи:GitHub.com/Рон, которого я хочу/Судный день…

Или используйте аннотации на уровне службы, например

@Service
@CacheConfig(cacheNames = "users")
public class UserService {

    private static Map<String, User> userMap = new HashMap<>();

    @CachePut(key = "#user.username")
    public User addUser(User user){
        user.setUid(UUID.randomUUID().toString());
        System.out.println("add user: " + user);
        userMap.put(user.getUsername(), user);
        return user;
    }

    @Caching(put = {
            @CachePut( key = "#user.username"),
            @CachePut( key = "#user.uid")
    })
    public User addUser2(User user) {
        user.setUid(UUID.randomUUID().toString());
        System.out.println("add user2: " + user);
        userMap.put(user.getUsername(), user);
        return user;
    }
    ...
}

Spring Boot 2 интегрирует режим Redis Sentinel

Spring Boot 2 интегрирует режим Redis Sentinel, за исключением немного отличных конфигураций. Другие аналогичны интеграции режима одного экземпляра. Пример конфигурации:

spring:
  redis:
    password: passw0rd
    timeout: 5000
    sentinel:
      master: mymaster
      nodes: 192.168.40.201:26379,192.168.40.201:36379,192.168.40.201:46379 # 哨兵的IP:Port列表
    jedis: # 或lettuce
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0

Полный пример можно найти в исходном коде:GitHub.com/Рон, которого я хочу/Судный день…

В приведенной выше конфигурации указывается только адрес дозорного узла и имя главного узла, но клиент Redis в конечном итоге получает доступ к главному узлу, поэтому как клиент Redis получает адрес главного узла и как автоматически переключать главный адрес когда происходит аварийное переключение, как насчет? Мы берем пул соединений Jedis в качестве примера, чтобы раскрыть тайну его внутренней реализации через исходный код.

В конструкторе класса 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);
 }

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

    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());

        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();
        }
      }
    }
    //省略了非关键代码

    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;
  }

initSentinelsМетод в основном делает две вещи:

  1. Пересеките сторожевые узлы, пройдитеget-master-addr-by-nameКоманда получает информацию об адресе главного узла и выходит из цикла, когда он найден.get-master-addr-by-nameРезультат выполнения команды следующий
[root@dev-server-1 master-slave]# redis-cli -p 26379
127.0.0.1:26379> sentinel get-master-addr-by-name mymaster
1) "192.168.40.201"
2) "7001"
127.0.0.1:26379>
  1. Отслеживайте каждый дозорный узел с помощью MasterListener (функция публикации-подписки Redis) и подписывайтесь на дозорный узел.+switch-masterКанал, когда происходит аварийное переключение, клиент может получить уведомление от дозорного и завершить переключение главного узла, повторно инициализировав пул соединений. Код контрольной части мониторинга в методе MasterListener.run выглядит следующим образом.
 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(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");

Метод initPool выглядит следующим образом: если обнаруживается, что новый главный узел отличается от текущего главного, он повторно инициализируется.

private void initPool(HostAndPort master) {
    if (!master.equals(currentHostMaster)) {
      currentHostMaster = 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);
    }
  }

Выполнив два вышеуказанных шага, клиент Jedis может получить информацию об адресе главного узла, если ему известен только дозорный адрес, и может автоматически переключиться на новый адрес главного узла при аварийном переключении.

Spring Boot 2 интегрирует режим Redis Cluster

Spring Boot 2 интегрирует режим кластера Redis с небольшими отличиями в конфигурации. Другие аналогичны интеграции режима одного экземпляра. Пример конфигурации:

spring:
  redis:
    password: passw0rd
    timeout: 5000
    database: 0
    cluster:
      nodes: 192.168.40.201:7100,192.168.40.201:7200,192.168.40.201:7300,192.168.40.201:7400,192.168.40.201:7500,192.168.40.201:7600
      max-redirects: 3  # 重定向的最大次数
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0

Полный пример можно найти в исходном коде:GitHub.com/Рон, которого я хочу/Судный день…

существуетОсвойте три кластерных решения Redis в одной статьеОсновные принципы доступа в режиме кластера были представлены в .Вы можете перейти к целевому узлу для выполнения команд через любой узел.Максимальные перенаправления в приведенной выше конфигурации контролируют максимальное количество переходов в кластере.

Проверьте метод выполнения JedisClusterConnection,

public Object execute(String command, byte[]... args) {

    Assert.notNull(command, "Command must not be null!");
    Assert.notNull(args, "Args must not be null!");

    return clusterCommandExecutor
            .executeCommandOnArbitraryNode((JedisClusterCommandCallback<Object>) client -> JedisClientUtils.execute(command,
                    EMPTY_2D_BYTE_ARRAY, args, () -> client))
            .getValue();
}

Выполнение команд кластера осуществляется черезClusterCommandExecutor.executeCommandOnArbitraryNodeдостигать,

public <T> NodeResult<T> executeCommandOnArbitraryNode(ClusterCommandCallback<?, T> cmd) {

    Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
    List<RedisClusterNode> nodes = new ArrayList<>(getClusterTopology().getActiveNodes());
    return executeCommandOnSingleNode(cmd, nodes.get(new Random().nextInt(nodes.size())));
}

private <S, T> NodeResult<T> executeCommandOnSingleNode(ClusterCommandCallback<S, T> cmd, RedisClusterNode node,
        int redirectCount) {

    Assert.notNull(cmd, "ClusterCommandCallback must not be null!");
    Assert.notNull(node, "RedisClusterNode must not be null!");

    if (redirectCount > maxRedirects) {
        throw new TooManyClusterRedirectionsException(String.format(
                "Cannot follow Cluster Redirects over more than %s legs. Please consider increasing the number of redirects to follow. Current value is: %s.",
                redirectCount, maxRedirects));
    }

    RedisClusterNode nodeToUse = lookupNode(node);

    S client = this.resourceProvider.getResourceForSpecificNode(nodeToUse);
    Assert.notNull(client, "Could not acquire resource for node. Is your cluster info up to date?");

    try {
        return new NodeResult<>(node, cmd.doInCluster(client));
    } catch (RuntimeException ex) {

        RuntimeException translatedException = convertToDataAccessException(ex);
        if (translatedException instanceof ClusterRedirectException) {
            ClusterRedirectException cre = (ClusterRedirectException) translatedException;
            return executeCommandOnSingleNode(cmd,
                    topologyProvider.getTopology().lookup(cre.getTargetHost(), cre.getTargetPort()), redirectCount + 1);
        } else {
            throw translatedException != null ? translatedException : ex;
        }
    } finally {
        this.resourceProvider.returnResourceForSpecificNode(nodeToUse, client);
    }
}

Приведенная выше логика кода выглядит следующим образом

  1. Произвольно выбрать узел из списка узлов кластера
  2. Получите клиентское соединение с этого узла (если настроен пул соединений, из пула соединений), выполните команду
  3. Если выброшено исключение ClusterRedirectException, перейдите к возвращенному целевому узлу для выполнения.
  4. Если количество переходов больше, чем настроенное значение max-redirects, выдается исключение TooManyClusterRedirectionsException.

возможные проблемы

  1. Время ожидания соединения Redis истекло

Проверьте, нормально ли запускается служба (например,ps -ef|grep redisпросмотреть процесс,netstat -ano|grep 6379Проверьте, работает ли порт, и файл журнала), если он запускается нормально, проверьте, включен ли на сервере Redis брандмауэр, выключен ли брандмауэр или настроен пропускной порт.

  1. В режиме кластера подключение к 127.0.0.1 отклонено из-за ошибки, напримерConnection refused: no further information: /127.0.0.1:7600

Это из-за конфигурации в redis.confbind 0.0.0.0илиbind 127.0.0.1В результате его необходимо изменить на доступный извне IP-адрес, напримерbind 192.168.40.201. Если кластер был запущен ранее и данные сгенерированы, после изменения файла redis.conf вам необходимо изменить файл cluster-config-file, заменить 127.0.0.1 на конкретный IP-адрес привязки, а затем перезапустить.

  1. Ведущий кладет трубку, ведомый обновляется до ведущего, а ведущий перезапускается, и новые данные ведущего не могут быть нормально синхронизированы.

Если установлен пароль, его необходимо настроить в файлах конфигурации главного и подчиненного устройств.masterauth password

Связанное чтение:

  1. Схема сохранения Redis
  2. Освойте три кластерных решения Redis в одной статье

Автор: Конгшан Синьюй, ветеран ИТ, который все еще учится. За последнее время автор написал десятки технических блогов, включая Java, Spring Boot, Spring Cloud, Docker, опыт технического управления и т. д.
Добро пожаловать, чтобы обратить внимание на публичный аккаунт автора WeChat: техническое пространство Kongshan Xinyu, учиться и расти вместе

微信公众号