предисловие
Redis Cluster — это официальное кластерное решение, предоставляемое Redis. Из-за быстрого роста бизнеса автономный режим всегда будет сталкиваться с различными узкими местами, такими как память и производительность.В это время мы всегда будем кричать, переходим к кластеру. Как будто в моем доме так жарко, что ты продолжаешь включать кондиционер. Это правда, что кластер может решить большинство проблем, но в процессе использования кластера неизбежно сталкиваться с такими и другими проблемами.Что мне делать в это время?Вы спрашиваете в различных группах Baidu? НЕТ, как разработчик, прежде чем пользоваться удобством, предоставляемым третьей стороной, необходимо понять его основной рабочий механизм, чтобы вы могли быстро определить проблему и облегчить ее запуск. Эта статья предназначена в основном для того, чтобы разобраться в принципе кластера Redis, а также в рабочем процессе и анализе исходного кода Java-клиента JedisCluster.Несмотря на то, что это длинный текст, принцип прост для понимания, а исходный код ясен.
1. RedisCluster
Базовое введение и руководство по созданию кластера Redis можно найти по адресу:Учебное пособие по кластеру Redis
1.1 Как читать и записывать данные
Мы все знаем, что в одном узле redis данные хранятся в структуре k-v в памяти, что позволяет redis очень быстро читать и записывать данные. Redis Cluster децентрализован, он хранит все данные в разделах. То есть, когда в кластер встроено несколько узлов Redis, каждый узел отвечает только за ту часть данных, которой он должен управлять, и данные, хранящиеся между ними, различаются.
Redis Cluster делит все пространство ключей на 16384 блока, каждый из которых называется слотом, и делит эти слоты и соответствующие слотам k-v на каждый мастер-узел в кластере. Как показано ниже:
В алгоритме выбора ключа -> слот алгоритм, выбранный Redis Cluster, представляет собой хеш (ключ) по модулю 16383, то есть ключ хешируется с использованием алгоритма CRC16, а затем используется по модулю 16383, и результатом является соответствующий слот .Общие методы разделения данных:
- Раздел остатка узла: возьмите хеш-значение определенных данных, а затем возьмите остаток от количества узлов, чтобы определить, какой узел сопоставить. Преимущество в том, что это просто, но недостаток в том, что результат сопоставления нужно пересчитывать при расширении или сжатии, а в крайних случаях это вызовет полную миграцию данных.
- Непротиворечивый раздел хэша: назначьте маркер от 0 до 2^32 каждому узлу, чтобы сформировать кольцо.Правило совпадения данных заключается в том, чтобы найти первый узел, чей токен больше или равен хеш-значению по часовой стрелке в соответствии с хэш-значением ключ. Преимущество в том, что добавление и вычитание узлов влияет только на соседние узлы. Недостаток в том, что при малом количестве узлов преимущества становятся недостатками. Наоборот, это повлияет на большую часть данных в кольце. вычитание узлов приведет к тому, что некоторые данные не попадут.
- Раздел виртуального слота: используйте хорошо распределенную хеш-функцию для сопоставления данных с фиксированным диапазоном целых чисел, эти целые числа являются слотами, а затем назначаются конкретному управлению узлом. Redis Cluster использует разделы виртуальных слотов.
Вышеизложенное в основном показывает, как данные в кластере распределяются по каждому узлу, но на самом делеКак клиент читает и записывает данныекак насчет?Redis Cluster использует подход с прямым узлом. В режиме кластера клиент для управления кластером напрямую подключается к определенному узлу для работы. Когда узел получает какую-либо команду операции с ключом, он сначала вычисляет слот, соответствующий ключу, а затем находит соответствующий узел по слоту (как его найти, будет упомянуто позже).Если соответствующий узел сам, выполните команду команду ключевой операции и вернуть результат; если это не он сам, он вернет клиенту ошибку перенаправления MOVED, сообщая клиенту, какой узел следует запросить, и клиент инициирует второй запрос к правильному узлу для завершения этой ключевой операции. . Сообщение об ошибке MOVED показано ниже:
При использовании redis-cli для прямого подключения к узлам в кластере используйте параметр -c, и redis-cli автоматически перенаправит соединение на целевой узел для ключевых операций. Следует отметить, что эта функция автоматического перенаправления реализована redis-cli и не имеет ничего общего с самим узлом redis, который все равно возвращает клиенту ошибку MOVED.
В команде операции с ключом, кроме операции с одним значением ключа, есть ещеМножественные ключ-значение и массовые операции. Redis Cluster реализует все команды, доступные в нераспределенной версии, для обработки значений одного ключа, но с использованиемОперации с несколькими ключами-значениями, потому что метод связи между кластером и клиентом напрямую связан с узлом, но для операций с несколькими ключами необходимо обойти все узлы, поэтомуне поддерживаетсяДа, в основном клиент реализует в коде необходимые функции.Для массовых операций, с одной стороны, клиентский код может вычислять слот, выполнять биннинг для одного узла и, наконец, работать в пакетном режиме, с другой стороны, Redis Cluster предоставляетhashtagФункция, добавляя хэштег к ключу, тип ключа может храниться в том же слоте, когда он сохраняется, чтобы добиться эффекта хранения в том же узле.
хэштег: это функция, реализованная Cluster для удовлетворения потребностей пользователей в привязке определенных ключей к определенным слотам. При вычислении слота ключа, если ключ содержит фигурные скобки {} и содержимое в фигурных скобках не пусто, будет вычисляться слот, соответствующий метке в фигурных скобках. Если {} не включено или содержимое пусто, вычисляется слот, соответствующий всему ключу. Вы можете использовать эту функцию для привязки типа ключа к слоту в конкретных требованиях, но злоупотреблять ею нельзя, ведь сами данные хранятся в партициях, это приведет к несбалансированному использованию памяти каждой нодой и повлияет на производительность кластера .
Примечание: для выполнения сценария lua и ключевых операций в транзакциях предполагается, что задействованные ключи находятся на узле.Если этих операций нельзя избежать при использовании кластера, вы можете рассмотреть возможность использования хэштега, и тогда клиент работает через соединение этот узел.
1.2 Обмен информацией между узлами
В кластере будет несколько узлов, каждый узел отвечает за часть слота и соответствующие данные k-v и взаимодействует с клиентом, напрямую подключаясь к конкретному узлу. Итак, вопрос в том, что вы спрашиваете меня о значении ключа, я не отвечаю за слот, соответствующий этому ключу, но я должен сказать вам, что он ПЕРЕМЕЩЕН на целевой узел, как мне узнать, кто этот целевой узел?
Redis Cluster использует протокол Gossip для хранения информации о метаданных узлов, этот протокол является режимом P2P, и основная вина заключается в обмене информацией. Узлы продолжают обмениваться метаданными друг с другом, поэтому через какое-то время все знают, кто они, за какие данные они отвечают, правильно ли они работают и так далее. Обмен информацией между узлами зависит от сообщений Gossip, отправляемых друг другом. Обычно используются следующие четыре типа сообщений:
- встретить сообщениеУзел, получивший сообщение, будет уведомлен о том, что отправляющий узел хочет присоединиться к текущему кластеру, и получатель ответит.
- пинг-сообщениеЭто запрос на обнаружение соединения и обмен информацией, который узел в кластере периодически отправляет другим узлам (частично или всем) в кластере.Сообщение содержит информацию об узле-отправителе и другую информацию об узле, известную узлу-отправителю.
- новости понгаЭто ответное сообщение, которое узел отвечает узлу-отправителю после получения сообщений о встрече и проверке связи, сообщая отправителю, что связь нормальная и что сообщение содержит текущий статус узла.
- сообщение об ошибкеЭто широковещательное сообщение, рассылаемое всем узлам в кластере после того, как узел считает, что другой узел в кластере находится в автономном режиме.
В процессе запуска кластера важным шагом являетсяРукопожатие узла, его суть заключается в том, чтобы отправить сообщение о встрече на одном узле всем остальным узлам.Сообщение содержит информацию о текущем узле (идентификатор узла, ответственный слот, идентификатор узла и т. д.), а получатель будет хранить узел-отправитель информация в середине списка локальных узлов. Тело сообщения также содержит другую информацию об узле (идентификатор узла, идентификатор узла, IP-адрес узла, порт и т. д.), который связывается с отправляющим узлом. Получатель также будет анализировать эту часть содержимого. Если она не существует в список локальных узлов, он возьмет на себя инициативу по отправке сообщения на новый узел. После того, как получатель обработает сообщение, он также ответит на сообщение pong узлу-отправителю, и отправитель также проанализирует сообщение pong, чтобы обновить информацию о локальном узле хранения. Следовательно, хотя только один узел отправляет сообщение о встрече всем другим узлам, в конечном итоге все узлы будут иметь информацию обо всех остальных узлах.
После запуска кластера каждый узел в кластере также перейдет вдругие узлыОтправьте сообщение проверки связи, чтобы определить, является ли целевой узел нормальным, и отправьте информацию об отрицательном слоте его последнего узла. Получатель также отвечает на сообщение pong, а отправитель обновляет информацию о локальном узле. При сбое связи с узлом (политика обнаружения сбоев будет описана позже) он будет активно рассылать сообщение о сбое узлам в кластере. Учитывая, что частый обмен информацией увеличит пропускную способность (чем больше узлов в кластере, тем очевиднее) и вычислительную нагрузку, запланированные задачи внутри Redis Cluster выполняются 10 раз в секунду, и каждый раз список локальных узлов пройдено, время для самого последнего полученного сообщения pong больше, чем Узлы cluster_node_timeout/2 немедленно отправляют сообщения ping.Кроме того, 5 узлов обнаруживаются случайным образом каждую секунду, и узел, который не общался в течение самого длительного времени, выбирается для отправлять пинг-сообщения. В то же время приведение сообщения ping несет информацию о своем собственном узле, а тело сообщения будет содержать только 1/10 информации о другом узле, чтобы избежать чрезмерных затрат на связь, вызванных слишком большими сообщениями.
Параметр cluster_node_timeout влияет на количество узлов, отправляющих сообщения.Корректировка должна всесторонне учитывать такие аспекты, как отработка отказа, обновление информации о слотах и скорость обнаружения новых узлов. Как правило, когда ресурсы полосы пропускания особенно ограничены, этот параметр можно соответствующим образом увеличить, чтобы снизить затраты на связь.
1.3 Миграция слотов и масштабирование кластера
Redis Cluster поддерживает автономные или новые узлы кластера во время обычного процесса обслуживания кластера. Но независимо от того, расширяется или сжимается кластер, по сути это миграция слотов и соответствующих им данных на разные узлы. В нормальных условиях после завершения миграции слотов количество слотов, отвечающих за каждый узел, в основном одинаково, что обеспечивает соответствие распределения данных теоретической однородности.
Обычно используемые команды, связанные со слотами, следующие:
- CLUSTER ADDSLOTS slot1 [slot2]...[slotN] —— Назначение слота ответственным за текущий узел, обычно используемый в процессе создания кластера.
- CLUSTER DELSLOTS slot1 [slot2]...[slotN] —— Удалить определенный слот из зоны ответственности текущего узла.Как и команда ADDSLOTS, после успешного выполнения информация о последнем слоте будет отправлена другим узлам в кластер через распространение межузловой связи.
- CLUSTER SETSLOT slotNum NODE nodeId —— Назначить слот узлу с указанным идентификатором.Как правило, он выполняется на каждом главном узле после завершения миграции, чтобы сообщить каждому главному узлу о завершении миграции.
- CLUSTER SETSLOT slotNum IMPORTING sourceNodeId —— Выполните эту команду на целевом узле миграции слота, что означает, что слот будет перенесен с исходного узла на текущий узел В процессе миграции текущий узел (т. е. целевой узел) будет получать команду запроса только после соединения Команда для установки слота в состояние ИМПОРТ.
- CLUSTER SETSLOT slotNum MIGRATING targetNodeId —— Выполните эту команду на исходном узле миграции слота, что означает, что слот будет перенесен с текущего узла на целевой узел.В процессе миграции текущий узел (т. е. исходный узел) по-прежнему будет принимать слот, связанный с MIGRATING. Если конкретный ключ все еще существует в текущем узле, обработка возвращает результат, если нет, возвращает ошибку перенаправления ASK с информацией о целевом узле.Когда другие узлы получат соответствующий запрос слота, они все равно вернутся к исключению перенаправления MOVED исходного узла.
На самом деле ядром слота миграции является миграция данных k-v, соответствующих слоту, на целевой узел. Следовательно, после завершения установки состояния слота на исходном узле и целевом узле (то есть двух последних команд выше) необходимо запустить миграцию конкретного ключа.
- Всего слотов CLUSTER GETKEYSINSLOT — эта команда возвращает набор ключей с указанным количеством слотов.
- MIGRATE targetNodeIp targetNodePort key dbId timeout [auth password] —— Эта команда выполняется на исходном узле, она подключается к целевому узлу, сериализует ключ и его значение и отправляет их в прошлое. целевой узел, удалите хранилище на текущем узле. Вся операция атомарна. Поскольку номер 0 базы данных каждого узла используется в режиме кластера, параметр dbId может быть только 0 во время миграции.
- MIGRATE targetNodeIp targetNodePort "" 0 тайм-аут [пароль аутентификации] keys key1 key2... -- Эта команда представляет собой пакетную версию приведенной выше команды переноса на основе конвейера.
После завершения миграции ключей всего слота необходимо выполнить CLUSTER SETSLOT slotNum NODE nodeId на каждом главном узле, чтобы уведомить о завершении миграции всего слота. Функция reshard, предоставляемая redis-trib.rb, основана на приведенной выше официальной команде.
Процесс расширения кластера фактически заключается в запуске нового узла, присоединении к кластеру (рукопожатие узла и связь по протоколу сплетен) и, наконец, переносе некоторых слотов с предыдущих узлов на новый узел.
Помимо равномерного переноса слотов узлов в автономный режим на другие главные узлы, процесс сжатия кластера также включает автономную работу узлов. Официал предоставляет команду CLUSTER FORGET downNodeId, которая используется для ее выполнения на других узлах, чтобы забыть об автономном узле и не обмениваться с ним информацией.Следует отметить, что эта команда действует в течение 60 с, и связь будет возобновлена по истечении времени истекает. Обычно рекомендуется использовать функцию del-node, предоставляемую redis-trib.rb.
1.4 Высокая доступность
Кластер Redis жертвует принципом строгой согласованности данных и стремится к максимальной производительности. Подчиненный узел не упоминался выше, в основном от главного узла, чтобы разобраться в некоторых принципах хранения данных и масштабирования кластера. Предпосылка обеспечения высокой доступности неотделима от подчиненного узла.Если главный узел по какой-то причине недоступен, ему нужен подчиненный узел, который молча служил запасным колесом. Как правило, при построении кластера требуется не менее 6 экземпляров, из которых 3 экземпляра являются главными узлами, каждый из которых отвечает за часть слота, а остальные 3 экземпляра соответствуют главному узлу в качестве его подчиненного узла, который реплицирует операции мастер-узла (эта статья о мастер-узле. Детали, скопированные из, не детализированы). Redis Cluster не поддерживает команду slaveof при добавлении подчиненного узла к главному узлу, но выполняет команду cluster replicate masterNodeId на подчиненном узле. Полная схема архитектуры кластера Redis выглядит следующим образом:
Обнаружение сбоев кластера также основано на обмене данными между узлами. Каждый узел хранит список узлов (другую информацию об узле) локально, и каждый узел в спискеВ дополнение к хранению своего идентификатора, ip, порта и идентификатора состояния (роль master-slave, переход в автономный режим и т. д.), элемент node также имеет время последней отправки ping-сообщения на узел и последний раз. сообщение pong было получено от узла. и связанный список отчетов, которые содержат автономное распространение узла другими узлами.. Узлы будут периодически отправлять сообщения ping, отвечать друг другу сообщениями pong и обновлять на этот раз после успеха. В то же время у каждого узла есть запланированная задача для сканирования двух сообщений в локальном списке узлов.Если обнаружится, что время ответа pong за вычетом времени отправки ping превышает время ожидания конфигурации узла кластера (по умолчанию 15 секунд , этот параметр используется для установки связи между узлами) После периода ожидания) статус соответствующего узла в локальном списке будет помечен как PFAIL, и считается, что он может находиться в автономном режиме.
При обмене данными между узлами (ping) он будет нести часть информации об узле в списке локальных узлов.Если он включает узел, помеченный как PFAIL, когда получатель сообщения анализирует узел, он найдет элемент узла в своем собственном локальном узле. список. Связанный список линейных отчетов, чтобы увидеть, есть ли уже отчет от узла-отправителя для неисправного узла. Если да, обновите время получения отчета от узла, отправившего сообщение ping на неисправный узел. Если нет, добавить этот отчет в связанный список.Структура каждого элемента связанного списка автономных отчетов состоит только из двух частей: одна предназначена для сообщения информации об отправляющем узле локального неисправного узла, а другая — это время, когда отчет получен локально.(Время сохраняется, потому что отчет о неисправности действителен, чтобы избежать ложных срабатываний). Поскольку список автономных отчетов каждого узла существует в своей собственной информационной структуре, при просмотре каждого элемента узла в локальном списке узлов вы можете четко знать, какие другие узлы говорят мне, брат, что вы смотрите, я чувствую себя круто в этот момент .
Срок действия отчета о сбое: время ожидания узла-кластера * 2.
После того, как приемник сообщений разрешит узел PFAIL и обновит связанный список отчетов об ошибках соответствующего узла в локальном списке, он проверит, превышает ли допустимые узлы отчетов в связанном списке отчетов об ошибках узла половину числа всех главных узлов. . Если не превышает, продолжаем разбирать сообщение пинга, если превышает, значитБолее половины узлов считают, что узел может быть отключен, и текущий узел пометит флаг состояния в информации о локальном узле узла PFAIL как FAIL., а затем передать кластеру сообщение об ошибке.После получения сообщения об ошибке все узлы в кластере изменят флаг состояния узла в своем локальном списке узлов на FAIL. После того, как все узлы пометят его как FAIL, подчиненный узел, соответствующий узлу FAIL, инициирует процесс пересылки. После завершения процесса нормализации узел будет официально отключен.Когда он восстановится, он обнаружит, что его слот был назначен узлу, и он преобразует себя в подчиненный узел этого узла и пропингует другие узлы в кластере. , прочее После того, как узел получит сообщение проверки связи от узла восстановления, он обновит свой флаг состояния. Кроме того, если восстановленный узел обнаружит, что его собственный слот все еще отвечает за себя, он будет связываться с другими узлами.После того, как другие главные узлы обнаружат, что узел восстановился, они отклонят выбор своих подчиненных узлов и, наконец, очистят их статус FAIL.
1.5 Взлеты и падения от узла к продвижению
Если мастер-узел выходит из строя в кластере, он помечается как FAIL другими мастер-узлами.Для нормального использования кластера один из соответствующих ему подчиненных узлов будет повышен до нового главного узла, отвечающего за всю работу исходного узла. главный узел. .
Не все ведомые узлы могут быть назначены, это то же самое, что и повышение по службе обычного персонала. Только когда соединение между ведомым узлом и главным узлом будет разорвано на определенный период времени, они будут первоначально иметь право на выдвижение. Как правило, время ожидания кластера-узла *10, 10 — это эффективный коэффициент подчиненного узла по умолчанию.
Вообще говоря, неисправный главный узел будет иметь несколько подчиненных узлов, соответствующих требованиям повышения, так как же выбрать наиболее подходящий из этих подчиненных узлов для повышения до главного узла для возобновления работы? Роль подчиненного узла состоит в том, чтобы служить резервной копией главного узла.Каждая операция на главном узле будет асинхронно резервироваться на нескольких подчиненных узлах, но это определяется конкретной структурой главного-ведомого узла.Как правило, каждый подчиненный узел не подключен к главному узлу отличается.Чтобы лучше заменить работу исходного главного узла, необходимо выбрать подчиненный узел, который находится ближе всего или даже полностью синхронизирован с данными главного узла от этих подчиненных узлов, чтобы завершить финальное продвижение..
Происхождение продвижения подчиненного узла является подчиненным узлом. Ведомый узел связывается с другими узлами в запланированной задаче.Когда главный узел обнаруживает, что он не прошел, он решает, имеет ли старший узел право на повышение и назначение. Если есть, он назначит время для избрания себя в соответствии с соответствующими правилами. По достижении заданного момента времени инициируется процесс выборов для собственного продвижения, и голоса выбираются другими обычными главными узлами в кластере. Если количество голосов, полученных вами, превышает половину нормального количества мастер-узлов, для завершения этого предвыборного продвижения будет выполнена замена исходного мастер-узла.
Установить правила времени выборов: выборы не будут инициированы сразу после того, как главный узел окажется в состоянии FAIL. но после Фиксированная задержка (500 мс) + случайная задержка (0-500 мс) + рейтинг смещения репликации подчиненного узлаЧерез 1000 мс инициируется процесс выбора самого себя. Фиксированная задержка предназначена для того, чтобы гарантировать, что статус FAIL главного узла известен всем главным узлам, а случайная задержка предназначена для того, чтобы попытаться избежать ситуации, когда несколько подчиненных узлов инициируют выборы одновременно.1000 мс гарантирует, что подчиненный узел с наибольшим смещением репликации, то есть подчиненный узел, ближайший к исходным данным главного узла, инициирует выбор первым. Поэтому, как правило, выбор повышения с узла проходит успешно один раз.Главный узел не различает, какой подчиненный узел является наиболее подходящим для продвижения.В основном это зависит от времени инициации выборов, чтобы сделать наиболее подходящий узел успешным.
Инициирование выборов с узла в основном делится на два этапа.:
- Эпоха глобальной конфигурации автоинкрементного кластера обновляется до эпохи текущего узла (эта эпоха конфигурации здесь подробно не описывается. Если вы не понимаете, вы можете просто понять это как номер версии. Каждый узел имеет своя собственная эпоха, а кластер имеет глобальную эпоху);
- Передайте сообщение о выборах FAILOVER_AUTH_REQUEST в кластер, и сообщение будет содержать эпоху текущего узла.
После того, как подчиненный узел передаст сообщение о выборах, он ожидает ответа главного узла FAILOVER_AUTH_ACK в течение времени NODE_TIMEOUT*2. Если он получит ответ от большинства мастер-узлов, это означает, что выборы прошли успешно, и сообщение ping\pong будет использовано для присяги на суверенитет. Если получено недостаточно ответов, выборы будут прерваны, а выборы будут повторно инициированы другими узлами.
Главный узел имеет один и только один голос в каждую эпоху глобальной конфигурации.Как только он проголосует за подчиненный узел, он будет игнорировать сообщения о выборах других узлов. Как правило, существует лишь очень небольшая вероятность того, что несколько подчиненных узлов будут конкурировать в одну и ту же эпоху конфигурации, которая определяется временем выбора и шагами выбора подчиненных узлов. Ответ главного узла на голосование FAILOVER_AUTH_ACK будет возвращать ту же эпоху, что и полученное сообщение о выборах, а подчиненный узел будет распознавать только ответ на голосование, который соответствует текущей эпохе узла, что позволяет избежать исторических сообщений о распознавании, которые запаздывают из-за сетевых задержек. и другие факторы.
После успешного продвижения подчиненного узла необходимо выполнить последние три шага при замене исходного главного узла:
- Отменить работу репликации текущего узла и стать главным узлом;
- Отозвать слоты, за которые отвечает исходный главный узел, и делегировать эти слоты себе;
- Распространите сообщение pong, чтобы уведомить все узлы о том, что они завершили нормализацию, и информацию о слотах, за которые они несут ответственность после нормализации.
2. Кластер джедаев
Jedis — это Java-клиент Redis, а JedisCluster — это клиент кластера, предоставляемый Jedis в соответствии с характеристиками кластера Redis. Выше описан подробный процесс работы с ключами в кластере Redis.Как правило, когда клиент подключен к определенному узлу через redis-cli, если ключ, с которым нужно работать, находится не на этом узле, сервер вернет ошибку перенаправления MOVED. , Для продолжения требуется ручное подключение к узлу перенаправления. Или добавьте параметр -c, когда redis-cli подключается к сервисному узлу, вы можете использовать механизм автоматического перенаправления, предоставляемый redis-cli, который будет автоматически перенаправлять при работе с ключами других сервисных узлов, избегая ручного перенаправления клиентом. Как Java-клиент для работы с кластером Redis, JedisCluster также соответствует спецификации клиентского подключения, предоставленной RedisCluster.В этом разделе рассматривается, как это делается с точки зрения исходного кода.
2.1 Работа по инициализации
Независимо от того, используете ли вы Spring для интеграции jedis или используете jedis напрямую, первым шагом является инициализация клиента, который находится непосредственно в JedisCluster. JedisCluster фактически является продвинутым клиентом.Он наследует BinaryJedisCluster.Инициализация клиента фактически отвечает за этот класс.Кроме того, он также реализует три интерфейса: JedisCommands, MultiKeyJedisClusterCommands и JedisClusterScriptingCommands, который инкапсулирует одноклавишные команды и многоклавишные команды операций , А также специальные методы, такие как команды выполнения скриптов для разработчиков.
Конструкторов JedisCluster много, но в конце вызывается конструктор родительского класса BinaryJedisCluster, по сути здесь инициализируется обработчик соединения и задается максимальное количество повторных попыток.
public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode,
int connectionTimeout, int soTimeout, int maxAttempts,
String password, GenericObjectPoolConfig poolConfig) {
this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig,
connectionTimeout, soTimeout, password);
this.maxAttempts = maxAttempts;
}
JedisSlotBasedConnectionHandler фактически вызывает родительский класс JedisClusterConnectionHandler. Конструктор, а вот и ядро инициализации JedisCluster.
public JedisClusterConnectionHandler(Set<HostAndPort> nodes,
final GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password) {
// 创建集群信息的缓存对象
this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, password);
// 初始化连接池与缓存信息
initializeSlotsCache(nodes, poolConfig, password);
}
СоздайтеJedisClusterInfoCacheКогда вы смотрите на конструкцию экземпляра, вы можете знать, что только информация о конфигурации соединения назначается атрибуту экземпляра, и нет никакой другой операции. Итак, какую информацию он кэширует? Глядя на его исходный код, вы можете найти следующие два важных свойства:Отношение отображения между узлом и его соответствующим пулом соединений и отображение между слотом и пулом соединений, соответствующим узлу, где расположен слот, сохраняются соответственно.
JedisClusterInfoCache.java
private final Map<String, JedisPool> nodes = new HashMap<String, JedisPool>();
private final Map<Integer, JedisPool> slots = new HashMap<Integer, JedisPool>();
Инициализация данных кеша осуществляется путем обхода всех узлов, создания экземпляра jedis каждого узла и подключения для получения узлов и, в свою очередь, отвечает за данные слота. Вообще говоря, он выскочит из обхода после подключения первого узла в конфигурации для получения соответствующей информации. Код метода initializeSlotsCache выглядит следующим образом:
JedisClusterConnectionHandler.java
private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig, String password) {
for (HostAndPort hostAndPort : startNodes) {
Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
if (password != null) {
jedis.auth(password);
}
try {
// 获取节点及所负责的槽位信息
cache.discoverClusterNodesAndSlots(jedis);
break;
} catch (JedisConnectionException e) {
// try next nodes
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
Получение и обновление кэшированных данных на самом деле реализуется методом discoveryClusterNodesAndSlots в JedisClusterInfoCache.В основном с помощью команды кластерных слотов, чтобы получить данные о распределении слотов в кластере, а затем проанализировать возвращаемый результат команды, инициализировать пул соединений для каждого главного узла, а затем подключить узел к пулу соединений, а также все слоты и пулы соединений, за которые отвечает узел Отношения сопоставления кэшируются в двух упомянутых выше картах.. Исходный код выглядит следующим образом:
JedisClusterInfoCache.java
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 使用读写锁控制缓存更新时的线程安全
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
// cluster slots 命令返回结果的每个元素中第三部分为主节点信息,后面的都是从节点信息
private static final int MASTER_NODE_INDEX = 2;
public void discoverClusterNodesAndSlots(Jedis jedis) {
w.lock();
try {
reset(); // 销毁连接池、清空缓存
// 根据cluster slots 命令获取槽位分布信息
List<Object> slots = jedis.clusterSlots();
for (Object slotInfoObj : slots) {
List<Object> slotInfo = (List<Object>) slotInfoObj;
if (slotInfo.size() <= MASTER_NODE_INDEX) {
continue;
}
// 获取当前槽位节点负责的所有槽位
List<Integer> slotNums = getAssignedSlotArray(slotInfo);
// hostInfos
int size = slotInfo.size();
for (int i = MASTER_NODE_INDEX; i < size; i++) {
// 获取节点信息数据
List<Object> hostInfos = (List<Object>) slotInfo.get(i);
if (hostInfos.size() <= 0) {
continue;
}
// 生成节点对象
HostAndPort targetNode = generateHostAndPort(hostInfos);
// 初始化节点连接池,并将节点与其连接池缓存
setupNodeIfNotExist(targetNode);
if (i == MASTER_NODE_INDEX) {
// 若节点是主节点,则将其负责的每个槽位与其连接池建立映射关系缓存
assignSlotsToNode(slotNums, targetNode);
}
}
}
} finally {
w.unlock();
}
}
Вышеупомянутый метод DiscoverClusterNodesAndSlots в основном анализирует возвращаемый результат команды cluster slots.Если вы не знакомы с этим, рекомендуется подключиться к узлу в кластере для выполнения команды, и это будет ясно из результатов. Оглядываясь назад, инициализация здесь в основном делится на следующие части:
- Подключите узел и выполните команду cluster slots, чтобы получить информацию о распределении слотов и узле кластера;
- Инициализируйте пул соединений для каждого узла и установите кэш отношения сопоставления с узлом;
- Создайте кэш сопоставления между слотами, отвечающими за каждый главный узел, и пулом соединений главного узла один за другим.
Информация о сопоставлении, кэшированная при инициализации, играет решающую роль в использовании JedisCluster. Но это также потому, что JedisCluster кэширует данные узла в локальной памяти и поддерживает пул соединений для каждого узла.При использовании огромного кластера с особенно большим количеством узлов клиент также будет потреблять больше памяти.
2.2 Основные детали операции
JedisCluster реализует команду с одним ключом, инкапсулированную интерфейсом JedisCommands. Здесь в качестве примера берется подробный процесс анализа команды операции с одним ключом. Код выглядит следующим образом:
JedisCluster.java
@Override
public String set(final String key, final String value) {
return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
@Override
public String execute(Jedis connection) {
return connection.set(key, value);
}
}.run(key);
}
Из кода видно, что фактическая операция набора по-прежнему зависит от jedis. Как было сказано выше в разделе инициализации, jedisPool будет создаваться для каждой ноды кластера, а созданный во время инициализации connectionHandler используется здесь классом реализации JedisClusterCommand, так что нетрудно понять,connectionHandler предоставляет услуги по подключению к внешнему миру в соответствии с кэшированными данными JedisClusterInfoCache.. Либо вы даете мне узел, а я даю вам экземпляр jedis, либо вы даете мне слот, и я даю вам экземпляр jedis. В этом можно убедиться, взглянув на исходный код JedisClusterConnectionHand-ler. Таким образом, JedisClusterCommand будет обрабатывать соответствующую информацию при работе с ключом и получать необходимые параметры для установления соединения. Ниже приведена реализация метода run(key) (код чуть длиннее, но логика понятна и комментарии подробные):
JedisClusterCommand.java
// 存放当前操作的ask重定向后的连接
private ThreadLocal<Jedis> askConnection = new ThreadLocal<Jedis>();
public T run(String key) {
if (key == null) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
}
return runWithRetries(SafeEncoder.encode(key), this.maxAttempts, false, false);
}
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
if (attempts <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
}
Jedis connection = null;
try {
if (asking) {
// 若是ask重定向操作,则从ThreadLocal中获取重定向后的jedis
connection = askConnection.get();
connection.asking();
asking = false; // 若ask重定向成功,撤销ask重定向标记
} else {
if (tryRandomNode) { // 随机连接至某个ping-pong正常的节点
connection = connectionHandler.getConnection();
} else {
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key)); // 根据槽位算法计算key对应的slot,再根据slot获取对应节点的jedis
}
}
return execute(connection);
} catch (JedisNoReachableClusterNodeException jnrcne) {
throw jnrcne;
} catch (JedisConnectionException jce) {
// 发生连接异常时,释放连接,开始递归重试
releaseConnection(connection);
connection = null;
if (attempts <= 1) {
// 重试次数递减到1次时,代表目标节点可能发生故障,更新缓存数据,抛出原始异常
this.connectionHandler.renewSlotCache();
throw jce;
}
// 递减重试次数开始重试
return runWithRetries(key, attempts - 1, tryRandomNode, asking);
} catch (JedisRedirectionException jre) { // 发生了重定向异常
// 释放当前占用连接
releaseConnection(connection);
connection = null;
if (jre instanceof JedisAskDataException) {
// ASK重定向代表当前槽位正在迁移,直接获取ask异常信息里的目标节点的jedis实例放入ThreadLocal,设置asking标志,重试请求目标节点操作
asking = true;
askConnection.set(this.connectionHandler
.getConnectionFromNode(jre.getTargetNode()));
} else if (jre instanceof JedisMovedDataException) {
// MOVED重定向代表本地缓存的槽位数据跟集群不一致,需要更新缓存数据后重试
this.connectionHandler.renewSlotCache(connection);
} else {
throw new JedisClusterException(jre);
}
return runWithRetries(key, attempts - 1, false, asking); // 重试
} finally {
releaseConnection(connection);
}
}
Прочитав вышеприведенный код, нам не составит труда разобратьсяОсновной процесс работы с ключом JedisCluster.Вычислить слот ключа - "Получить джедай целевого узла по слоту из кэша - "Выполнить операцию ключа. В этом процессе, если возникает исключение соединения, максимальное настроенное количество повторных попыток будет повторено -1 раз.Если проблема с соединением все еще существует, информация кеша будет обновлена, и будет выбрано исходное исключение соединения; если возникает исключение перенаправления, конкретные исключения перенаправления обрабатываются по-другому.Обновлять кеш при получении перенаправления MOVED, и попробуй еще раз. иПолучено перенаправление ASKКогда целевой узел проанализирован напрямую и получено соединение, повторите попытку перейти на ветку ask,не обновляет кеш. Это связано с тем, что при возникновении исключения перенаправления ASK слот переносится и не заполняется. Часть ключа слота находится в целевом узле, а часть ключа — в исходном узле. Слот не может быть точно привязан к узел, поэтому кеш не будет обновляться.После завершения миграции, когда старый кеш используется для запроса ключа, будет получено исключение перенаправления MOVED, возвращаемое redis, и кеш будет обновлен для поддержания точности кэшированные данные.
При возникновении исключения подключения повторите попытку не более 1 раза перед обновлением кеша. С одной стороны, это позволяет избежать неправильной оценки сбоев узлов и прерывания запросов из-за сети, блокировки чтения и записи и т. д., а с другой стороны, позволяет избежать частых обновлений кеша. Чтобы обеспечить потокобезопасность кэшированных данных в многопоточных сценариях, блокировки чтения-записи используются для управления чтением кэша и обновлениями, частые обновления неизбежно приведут к блокировке большинства запросов на чтение, что повлияет на производительность. Метод renewSlotCache из connectionHandler внутренне вызывает метод renewClusterSlots(Jedis jedis) из JedisClusterInfoCache. Разница в том, что экземпляр jedis, переданный при отсутствии параметра, имеет значение null.
JedisClusterInfoCache.java
public void renewClusterSlots(Jedis jedis) {
//该变量默认false,当需要更新集群缓存信息时,若有一个线程获得写锁,便会设置该标志为true,这样在更新期间,其他线程便不需要阻塞等待写锁,直接返回重试,在读锁出等待该线程更新完成。持有锁的线程更新完缓存后,会在释放锁前恢复该标志为false
if (!rediscovering) {
try {
w.lock();
rediscovering = true;
if (jedis != null) {
try {
// 通过cluster slots命令获取新的槽位信息,更新缓存
discoverClusterSlots(jedis);
return;
} catch (JedisException e) {
// 如果当前连接更新缓存发生JedisException,则从所有节点重试更新
}
}
for (JedisPool jp : getShuffledNodesPool()) {
try {
jedis = jp.getResource();
discoverClusterSlots(jedis);
return;
} catch (JedisConnectionException e) {
// 重试下一个节点
} finally {
if (jedis != null) {
jedis.close();
}
}
}
} finally {
// 恢复标志位,释放锁
rediscovering = false;
w.unlock();
}
}
}
JedisCluster использует блокировки чтения-записи для обеспечения безопасности потоков данных кеша, поэтому, когда поток обновляет кеш, другие потоки будут заблокированы при чтении карты слотов в кеше. В книге «Разработка, эксплуатация и обслуживание Redis» Фу Лэй считает, что это все еще можно оптимизировать.Выполнение команды кластерных слотов помещается перед блокировкой записи, и в то же время оценивается, является ли это то же самое, что и локальный кеш.Разница заключается в том, что его необходимо обновить.Добавить блокировку записи, сократив тем самым время блокировки других потоков и минимизировав влияние на правильную часть кеша слота операции.
2.3 Многоклавишная операция и выполнение скрипта
Глядя на диаграмму классов JedisCluster в рабочей части инициализации, я упомянул, что она реализует многоклавишные команды операций и команды выполнения сценариев, указанные двумя интерфейсами MultiKeyJedisClusterCommands и JedisClusterScriptingCommands. Всем известно, что разные ключи могут храниться в разных слотах в режиме кластера, поэтому операция с несколькими ключами означает, что могут быть задействованы несколько узлов.
Режим, в котором JedisCluster выполняет команды, заключается в получении ссылки от обработчика соединений, а анонимный внутренний класс JedisClusterCommand использует ссылку (экземпляр Jedis) для выполнения конкретной команды.Этот процесс согласуется с командой с одним ключом. Разница в том, что операция с несколькими ключами вызывает метод JedisClusterCommand.run(keys.length, keys). То же, что в итоге это все runWithRetries(byte[] ключ, **int **попытки, **boolean **tryRandomNode, **boolean **спрос) для завершения операции.
Вот пример команды exists с несколькими ключами. Код выглядит следующим образом:
JedisCluster.java
@Override
public Long exists(final String... keys) {
return new JedisClusterCommand<Long>(connectionHandler, maxAttempts) {
@Override
public Long execute(Jedis connection) {
return connection.exists(keys);
}
}.run(keys.length, keys);
}
JedisClusterCommand.java
public T run(int keyCount, String... keys) {
if (keys == null || keys.length == 0) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
}
if (keys.length > 1) {
int slot = JedisClusterCRC16.getSlot(keys[0]);
for (int i = 1; i < keyCount; i++) {
int nextSlot = JedisClusterCRC16.getSlot(keys[i]);
if (slot != nextSlot) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster "
+ "because keys have different slots.");
}
}
}
return runWithRetries(SafeEncoder.encode(keys[0]), this.maxAttempts, false, false);
}
Код очень простой и понятный.Для нескольких ключей он сначала проверит, находится ли он в слоте.После определения, что это слот, он возьмет первый ключ для вычисления слота и запросит у connectionHandler jedis пример. следовательноJedisCluster не поддерживает операции с несколькими ключами, которые не находятся в одном слоте (на самом деле кластер redis не предоставляет эту функцию). Если несколько ключей, переданных при вызове метода команды с несколькими ключами, не являются одним и тем же слотом, будет выдано исключение JedisClu-sterException, и оно сообщит вам, что нет никакого способа запланировать выполнение команды в кластере, потому что эти ключи расположены в разных слотах.
В реальной разработке, если понятно, что определенный тип ключа будет иметь мультиключевые операции, мы можем принудительно расположить его в одном слоте и в одном узле по хэштегу при сохранении. Кроме того, если вам действительно нужно работать с ключами на нескольких узлах, вы можете пройти сопоставление от кэшированных узлов в кеше к пулу соединений, в каждомглавный узелпоследнее исполнение.
Выполнение сценария на самом деле зависит от джедаев, поэтому я не буду здесь вдаваться в подробности о джедаях. Выполнение скрипта также включает один ключ и несколько ключей, но принцип тот же, что и выше. следовательно,JedisCluster также не поддерживает сценарии с несколькими ключами в разных слотах..
2.4 Обзор структуры класса
Несколько классов, участвующих в JedisCluster, следующие:
JedisCommand инкапсулирует выполнение кластерных команд и абстрагирует два основных режима: одноключевой и многоключевой. Лично я понимаю, что идея кодирования здесь использует режим метода шаблона для инкапсуляции основного процесса выполнения, а конкретное выполнение реализуется классом реализации для вызова фактического API в соответствии с конкретными требованиями.JedisCluster — это API-класс, ориентированный на разработчиков, который реализует три типа командных интерфейсов и предоставляет дружественные методы для вызова бизнес-кода.
JedisClusterConnectionHandler отвечает за маршрутизацию нескольких пулов соединений.В соответствии с кешированным отношением сопоставления он определяет правильный пул соединений и возвращает его ссылку на верхний уровень. JedisSlotBasedConnectionHandler на самом деле обрабатывает только базовые функции родительского класса, предоставляет дружественный метод вызова для верхнего уровня и напрямую возвращает соединение, требуемое для верхнего уровня.
Суммировать
Кластер Redis обеспечивает более высокую производительность, лучшую масштабируемость и доступность благодаря сегментированному хранилищу, репликации данных «ведущий-ведомый», а также разумным и научным стратегиям аварийного переключения, а также удовлетворяет двум характеристикам AP теоремы CAP. Что касается согласованности, можно сказать, что кластерный режим со стратегией клиента обеспечивает «слабую согласованность». Автор считает, что в реальной разработке действительно необходимо разобраться с этими вещами, прежде чем их использовать, чтобы можно было заранее избежать многих онлайн-проблем. Эта статья посвящена разбору, лично мне кажется, что даже на основе имеющихся данных, разбор проверенной и глубоко обдуманной мной статьи даст более глубокое понимание смежных технологий, чем просто ее прочтение.
Ссылаться на
- Спецификация кластера Redis
- «Разработка, эксплуатация и обслуживание Redis» — Фу Лэй