1. Предпосылки
Вы определенно знакомы с блокировками. В нашем коде на Java часто встречается ключевое слово synchronized и реентерабельные блокировки ReentrantLock. Как правило, мы используем их для управления одновременным доступом к ресурсам в многопоточной среде, но с распространением С быстрым развитием модели, локальная блокировка часто не может удовлетворить наши потребности, и описанный выше метод блокировки в нашей распределенной среде потеряет свою эффективность. Поэтому для достижения эффекта локальных блокировок в распределенной среде тоже придумали свои стратегии, сегодня поговорим о процедурах реализации общих распределенных блокировок.
2. Распределенная блокировка
2.1 Зачем нужны распределенные блокировки
Мартин Клеппманн, исследователь распределенных систем из Кембриджского университета в Великобритании, провел жаркую дискуссию с Антирезом, отцом Redis, о том, безопасен ли RedLock (красный замок, упомянутый ниже). Мартин считает, что обычно мы используем распределенные блокировки в двух сценариях:
- Эффективность: Использование распределенных блокировок позволяет избежать повторения одной и той же работы на разных узлах, что приводит к пустой трате ресурсов. Например, после оплаты пользователем разные узлы могут отправить несколько текстовых сообщений.
- Корректность: добавление распределенных блокировок также может избежать искажения правильности.Если два узла работают с одними и теми же данными, например, несколько компьютеров узла работают с одним и тем же заказом с разными процессами, что может привести к отображению окончательного состояния заказа.ошибка, в результате потери.
2.2 Некоторые особенности распределенных блокировок
Когда мы определяем, что требуются распределенные блокировки на разных узлах, нам нужно понимать, какими характеристиками должны обладать распределенные блокировки:
- Взаимное исключение: Как и наши локальные блокировки, взаимное исключение является самым основным, но распределенные блокировки должны гарантировать взаимное исключение разных потоков на разных узлах.
- Повторный вход: если тот же поток на том же узле получает блокировку, он также может снова получить блокировку.
- Тайм-аут блокировки: как и локальные блокировки, тайм-ауты блокировки поддерживаются для предотвращения взаимоблокировок.
- Высокая эффективность и высокая доступность. Блокировка и разблокировка должны быть эффективными, и в то же время должна быть обеспечена высокая доступность, чтобы предотвратить сбой распределенной блокировки, который может увеличить деградацию.
- Поддержка блокировки и неблокировки: поддержка блокировки и попытки блокировки и попытки блокировки (длительный тайм-аут), например ReentrantLock.
- Поддерживает справедливые и нечестные блокировки (необязательно): Честные блокировки означают, что блокировки получаются в том порядке, в котором запрашиваются блокировки, а нечестные блокировки неупорядочены. Как правило, это менее успешно.
2.3 Распространенные распределенные блокировки
После того, как мы поймем некоторые особенности, мы обычно реализуем распределенные блокировки следующими способами:
- MySql
- Zk
- Redis
- Распределенные замки собственной разработки: такие как Chubby от Google.
Принципы реализации этих распределенных блокировок представлены ниже отдельно.
Распределенная блокировка 3Mysql
Прежде всего, поговорим о принципе реализации распределенной блокировки Mysql.Условно говоря, это относительно легко понять.Ведь база данных тесно связана с нашими разработчиками в обычной разработке. Для распределенных блокировок мы можем создать таблицу блокировок:
Методы lock(), trylock(long timeout) и trylock(), о которых мы упоминали ранее, могут быть реализованы с помощью следующего псевдокода.3.1 lock()
Блокировка обычно представляет собой блокировку получения блокировки, что означает, что она не остановится, пока блокировка не будет получена, тогда мы можем написать бесконечный цикл для выполнения ее операции:
mysqlLock.lcok — это sql. Чтобы добиться эффекта повторной блокировки, мы должны сначала запросить. Если есть значение, то нам нужно сравнить, является ли node_info непротиворечивым. Здесь node_info может быть представлен IP-адресом машины и имя потока.Если они непротиворечивы, просто добавьте значение счетчика повторных блокировок и верните false, если оно несовместимо. Если значения нет, вставьте часть данных напрямую. Псевдокод выглядит следующим образом:
Следует отметить, что этот фрагмент кода должен добавлять транзакции, и атомарность этой последовательности операций должна быть гарантирована.
3.2 tryLock() и tryLock(длительный тайм-аут)
tryLock() — это неблокирующая блокировка получения. Если ее невозможно получить, происходит немедленный возврат. Код может быть следующим:
tryLock(long timeout) реализован следующим образом:mysqlLock.lock такой же, как и выше, но следует отметить, что выбор ... для обновления блокирует блокировку строки получения.Если один и тот же ресурс имеет большой объем параллелизма, он может выродиться в блокировку блокировки получения.3.3 unlock()
Если разблокировано, если счетчик здесь равен 1, его можно удалить, а если он больше 1, его нужно вычесть на 1.
3.4 Тайм-аут блокировки
Мы можем столкнуться с тем, что наш машинный узел зависает, тогда блокировка не будет снята, мы можем запустить запланированную задачу, и, рассчитав общее время, которое мы обычно обрабатываем, например, 5 мс, тогда мы можем немного расширить его, когда блокировка не снимается более 20мс, можно считать, что нода зависла и снимать сразу.
3.5 Сводка по MySQL
- Применимые сценарии: распределенные блокировки Mysql обычно применимы к ресурсам, которые не существуют в базе данных.Если база данных существует, например, заказ, вы можете напрямую добавить блокировку строки к этим данным без необходимости выполнения утомительных шагов, описанных выше, например, заказ, тогда мы можем использовать select * from order_table, где id = 'xxx' для обновления добавляет блокировку строки, тогда другие транзакции не могут его изменить.
- Преимущества: просто для понимания, нет необходимости поддерживать дополнительное стороннее промежуточное ПО (например, Redis, Zk).
- Недостатки: Несмотря на то, что это легко понять, это громоздко реализовать, нужно учитывать тайм-ауты блокировки, добавлять транзакции и т.д. Производительность ограничена базой данных, и производительность обычно ниже, чем у кэша. Это не очень подходит для сценариев с высокой степенью параллелизма.
3.6 Оптимистическая блокировка
Мы ввели пессимистические блокировки ранее. Здесь я хотел бы упомянуть оптимистические блокировки. В наших реальных проектах часто используются оптимистичные блокировки, потому что потребление производительности наших блокировок строк относительно велико. Обычно мы не будем такими жесткими для некоторых соревнований , но также необходимо убедиться, что наше параллельное последовательное выполнение обрабатывается с использованием оптимистичных блокировок.Мы можем добавить поле номера версии в нашу таблицу, затем после запроса номера версии нам нужно полагаться на номер версии, который мы запрашивали при обновлении или удаление.Определить, совпадают ли текущая база данных и запрошенный номер версии.Если они равны, они могут быть выполнены.Если они не равны, они не могут быть выполнены. Такая стратегия очень похожа на нашу CAS (Compare And Swap), сравнение и обмен — это атомарная операция. Таким образом, мы можем избежать накладных расходов на добавление select * для обновления блокировок строк.
4. ZooKeeper
ZooKeeper также является распространенным методом реализации распределенных блокировок.По сравнению с базами данных, если вы не знаете ZooKeeper, вам может быть сложнее начать работу. ZooKeeper — распределенный сервис координации приложений, основанный на алгоритме Paxos. Узлы данных Zk аналогичны файловым каталогам, поэтому мы можем использовать эту функцию для реализации распределенных блокировок. Мы берем ресурс в качестве каталога, а затем узел в этом каталоге является клиентом, который нам нужен для получения блокировки.Регистрация клиента, который не получил блокировку, должна зарегистрировать Watcher на предыдущем клиенте, который может быть представлен по следующему рисунку.
/lock — это каталог, который мы используем для блокировки, /resource_name — это ресурс, который мы блокируем, а узлы под ним располагаются в том порядке, в котором мы блокируем.4.1Curator
Curator инкапсулирует базовый API Zookeeper, что упрощает и делает более удобным использование Zookeeper, а также инкапсулирует функцию распределенных блокировок, так что нам не нужно реализовывать ее самостоятельно.
Куратор реализует реентерабельную блокировку (InterProcessMutex) и нереентерабельную блокировку (InterProcessSemaphoreMutex). Блокировки чтения-записи также реализованы в блокировках с повторным входом.
4.2InterProcessMutex
InterProcessMutex — это реентерабельная блокировка, реализованная Curator. Мы можем реализовать нашу реентерабельную блокировку с помощью следующего фрагмента кода:
Мы используем acuire для блокировки и отпускания для разблокировки.
Процесс блокировки выглядит следующим образом:
- Во-первых, сделайте реентерабельное суждение: реентерабельная блокировка здесь записана в карте ConcurrentMap
threadData. добавить 1. На самом деле, наш предыдущий Mysql также может быть оптимизирован этим методом.Значение поля count не требуется.Поддержание этого локально может повысить производительность. - Затем создайте узел в нашем каталоге ресурсов: например, создайте здесь узел /0000000002.Для этого узла необходимо установить значение EPHEMERAL_SEQUENTIAL, которое является временным узлом и упорядочено.
- Получите все дочерние узлы в текущем каталоге и определите, является ли ваш собственный узел первым дочерним узлом.
- Если это первый, блокировка получена, и она может вернуться.
- Если это не первый, это доказывает, что кто-то получил блокировку раньше, и тогда вам нужно получить предыдущий узел вашего собственного узла. Предыдущий узел /0000000002 — это /0000000001.После того, как мы получим этот узел, мы регистрируем на нем наблюдателя (здесь наблюдатель фактически вызывает object.notifyAll(), который используется для разблокировки).
- object.wait(timeout) или object.wait(): блокировка и ожидание здесь соответствуют нашему наблюдателю на шаге 5.
Конкретный процесс разблокировки:
- Во-первых, определите повторную блокировку: если есть повторная блокировка, вам нужно только уменьшить количество блокировок на 1. Если количество блокировок равно 0 после уменьшения на 1, продолжайте следующие шаги и вернитесь напрямую, если это не 0.
- Удалить текущий узел.
- Удалите данные повторной блокировки в threadDataMap.
4.3 Блокировка чтения-записи
Куратор предоставляет блокировку чтения-записи, классом реализации которой является InterProcessReadWriteLock, и каждый узел здесь будет иметь префикс:
private static final String READ_LOCK_NAME = "__READ__";
private static final String WRITE_LOCK_NAME = "__WRIT__";
По разным префиксам это блокировка чтения или блокировка записи.Для блокировки чтения, если вы обнаружите, что перед ней стоит блокировка записи, вам нужно зарегистрировать наблюдателя на ближайшую к себе блокировку записи. Логика блокировки записи остается такой же, как мы анализировали в 4.2 ранее.
4.4 Тайм-аут блокировки
Zookeeper не нужно настраивать время ожидания блокировки.Поскольку мы устанавливаем узел как временный узел, каждая из наших машин поддерживает сеанс ZK.Через этот сеанс ZK может определить, не работает ли машина. Если наша машина зависнет, соответствующий временный узел будет удален, поэтому нам не нужно заботиться о тайм-ауте блокировки.
4.5 Резюме ЗК
- Преимущества: ZK не нужно заботиться о времени ожидания блокировки, и у него есть готовые сторонние пакеты для реализации, что более удобно, и поддержка блокировок на чтение-запись.ZK приобретает блокировки в порядке блокировки, поэтому он это честный замок. Для обеспечения высокой доступности используйте кластер ZK.
- Недостатки: ЗК требует дополнительного сопровождения и увеличивает затраты на обслуживание.Производительность не сильно отличается от Mysql, но все же относительно слабая. И требует от разработчиков понимания, что такое ZK.
5.Redis
Распределенные блокировки все ищут в интернете.Боюсь, что больше всего реализован Redis.Из-за хорошей производительности и простой реализации Redis очень популярен среди многих людей.
5.1 Простая реализация распределенной блокировки Redis
Студенты, знакомые с Redis, должны быть знакомы с методом setNx (установить, если не существует).Если он не существует, обновите его, что может быть хорошо использовано для реализации нашей распределенной блокировки. Для блокировки ресурсов нам нужно только
setNx resourceName value
Здесь есть проблема.После блокировки, если машина не работает, блокировка не будет снята, поэтому будет добавлено время истечения.Добавление времени истечения требует той же атомарной операции, что и setNx.До Redis2.8 нам нужно было используйте сценарии Lua, чтобы связаться с нами.Однако, после redis 2.8, redis поддерживает операции nx и ex как одну и ту же атомарную операцию.
set resourceName value ex 5 nx
5.2Redission
Все Javaers знают Jedis.Jedis является клиентом Java-реализации Redis, а его API обеспечивает более полную поддержку команд Redis. Redission также является клиентом Redis, который проще, чем Jedis. Jedis просто использует блокирующий ввод-вывод для взаимодействия с Redis, а Redission поддерживает неблокирующий ввод-вывод через Netty. Последняя версия Jedis, 2.9.0, не обновлялась почти 3 года в 2016 году, а последней версией Redission является обновление 2018.10.
Redission инкапсулирует реализацию блокировок, которые наследуют интерфейс java.util.concurrent.locks.Lock. Давайте будем использовать блокировку Redission точно так же, как нашу локальную блокировку. Давайте представим, как она реализует распределенные блокировки.
Redission не только предоставляет некоторые методы (lock, tryLock), которые идут с Java, но и обеспечивает асинхронную блокировку, более удобную для асинхронного программирования. Так как есть много внутренних исходников, то исходник выкладывать не буду.Здесь мы используем текстовое описание для анализа того как он блокируется.Вот метод tryLock:
- Попытка блокировки: Сначала он попытается заблокировать. Поскольку операция гарантированно будет атомарной, можно использовать только сценарии lua. Соответствующие сценарии lua следующие:Видно, что он не использует наш sexNx для работы, а использует хэш-структуру.Каждый из наших ресурсов, который необходимо заблокировать, можно рассматривать как HashMap, информация узла заблокированного ресурса — это ключ, а количество замки - это ценность. Таким образом, можно хорошо добиться эффекта повторного входа, а повторную блокировку можно выполнить, только прибавив 1 к значению. Конечно, локальный подсчет, о котором мы упоминали ранее, также можно использовать для оптимизации.
- Если попытка блокировки не удалась, оцените, истекло ли время ожидания, и верните false, если время ожидания истекло.
- Если тайм-аута после сбоя блокировки нет, вам нужно подписаться на канал с именем redisson_lock__channel+lockName, чтобы подписаться на сообщения разблокировки, а затем заблокировать до тайм-аута или есть сообщения разблокировки.
- Повторяйте шаги 1, 2 и 3, пока блокировка не будет окончательно получена или не истечет время ожидания определенного шага получения блокировки.
Для нашего метода разблокировки, который относительно прост, он также разблокируется через lua-скрипт, если это реентерабельная блокировка, она уменьшается только на 1. Если он разблокирован неблокирующим потоком, разблокировка завершается ошибкой.
Redission также реализует справедливые блокировки.Для справедливых блокировок он использует структуру списка и структуру хэш-наборов, чтобы сохранить наши узлы в очереди и время истечения срока действия наших узлов.Эти две структуры данных используются, чтобы помочь нам добиться справедливых блокировок.Введение вводится, и вы можете обратиться к исходному коду, если вы заинтересованы.
5.3RedLock
Давайте представим такой сценарий: после того, как машина A подаст заявку на блокировку, если мастер Redis выйдет из строя, а подчиненное устройство не синхронизируется с блокировкой в это время, то машина B снова подаст заявку на эту блокировку, когда она снова будет применена. Чтобы решить эту проблему, автор Redis предложил алгоритм Red Lock RedLock, а также реализовал RedLock в Redission.
С помощью приведенного выше кода нам нужно реализовать несколько кластеров Redis, а затем заблокировать и разблокировать красный замок. Конкретные шаги заключаются в следующем:
- Во-первых, сгенерируйте Rlocks нескольких кластеров Redis и создайте из них RedLocks.
- Три кластера блокируются по очереди, и процесс блокировки такой же, как в 5.2.
- Если блокировка выходит из строя в процессе циклической блокировки, необходимо определить, превышает ли количество отказов блокировки максимальное значение Максимальное значение здесь основано на количестве кластеров Например, если их три, только один отказ разрешено, а если пять, то допускается только одна неудача.Если вы провалите две, вы должны убедиться, что большинство из них преуспеют.
- В процессе блокировки необходимо судить о том, истекло ли время блокировки, возможно, мы можем установить блокировку только на 3 мс, а первая кластерная блокировка уже израсходовала 3 мс. Тогда это считается отказом замка.
- Если блокировка не удалась на шагах 3 и 4, будет выполнена операция разблокировки, и разблокировка запросит разблокировку для всех кластеров.
Можно видеть, что основной принцип RedLock заключается в использовании нескольких кластеров Redis и использовании большинства кластеров для успешной блокировки, что снижает вероятность сбоя кластера Redis и вызывает проблемы с распределенными блокировками.
5.4 Резюме Redis
- Преимущества: Для Redis реализация проста, а производительность выше, чем у ZK и Mysql. Если вам не нужны особенно сложные требования, вы можете использовать setNx для самостоятельной реализации.Если вам нужны сложные требования, вы можете использовать или учиться у Redission. Для некоторых более строгих сценариев можно использовать RedLock.
- Недостатки: необходимо поддерживать кластеры Redis, и необходимо поддерживать больше кластеров, если необходимо реализовать RedLock.
6. Проблемы безопасности распределенных замков
Мы представили красный замок выше, но Мартин Клеппманн считает, что он все еще небезопасен. В опровержении Мартина есть несколько моментов. Я думаю, что это не ограничивается RedLock. Алгоритмы, упомянутые выше, в основном имеют эту проблему. Давайте обсудим эти проблемы:
- Долгая пауза GC: учащиеся, знакомые с Java, должны быть знакомы с GC, во время GC будет происходить STW (stop-the-world), например сборщик мусора CMS, у него будет два этапа STW, чтобы предотвратить дальнейшее изменение ссылок. Тогда может быть ситуация на следующем рисунке (цитата из статьи Мартина, опровергающей Рэдлока):клиент1 получил блокировку и установил время ожидания блокировки, но после того, как клиент1 был STW, время STW было относительно большим, что привело к снятию распределенной блокировки, клиент2 получил блокировку, в это время клиент1 восстанавливает блокировку, затем клиент1 появляются , 2 получают блокировку одновременно, в это время возникает проблема ненадежности распределенной блокировки. На самом деле это не ограничивается RedLock.Для нашего ЗК такая же проблема у Mysql.
- Скачки часов: если время сервера Redis подскочит, это определенно повлияет на время истечения срока действия нашей блокировки, тогда время истечения срока действия нашей блокировки будет не таким, как мы ожидали, и клиент 1 и клиент 2 также получат одинаковую блокировку, тогда будет небезопасность , и это также появится для Mysql. Однако, поскольку ZK не устанавливает время истечения, прыжок не будет затронут.
- Длительный сетевой ввод-вывод: эта проблема очень похожа на STW нашего GC, то есть после того, как мы получаем блокировку, мы делаем сетевой вызов, и время вызова может быть больше, чем время истечения срока действия нашей блокировки, так что это также будет Если есть небезопасная проблема, у этого Mysql также будет эта проблема, а у ZK этой проблемы не будет.
По этим трем вопросам в Интернете было инициировано множество дискуссий, в том числе с авторами Redis.
6.1 STW GC
Для этой задачи видно что в основном все проблемы возникнут.Мартин дал решение.Для ZK он сгенерирует автоинкрементную последовательность.Когда мы реально оперируем ресурсами,нам нужно определить является ли текущая последовательность самой последней Немного похоже на нашу оптимистическую блокировку. Разумеется, автор Redis опроверг это решение, поскольку можно генерировать самоинкрементирующуюся последовательность, блокировку вообще не нужно, то есть можно сделать по решению, аналогичному оптимистической блокировке Mysql.
Я думаю, что это решение увеличивает сложность. Когда мы работаем с ресурсами, нам нужно повысить оценку того, является ли серийный номер последним. Независимо от того, какой метод оценки используется, это увеличит сложность. Я представлю Chubby Google и предложу лучшее решение позже.
6.2 Скачки часов
Мартин считает, что причина небезопасности RedLock также связана со скачком часов, потому что срок действия блокировки сильно зависит от времени, а ZK не обязательно должен зависеть от времени, он зависит от сеанса каждой ноды. Автор Redis тоже дал ответ: для прыжков во времени он делится на искусственную настройку и автоматическую настройку NTP.
- Искусственная корректировка: полностью управляема, если эффект искусственной корректировки не корректируется искусственно.
- Автоматическая настройка NTP: это можно контролировать в пределах контролируемого диапазона времени прыжка с помощью определенной оптимизации.Хотя это будет прыгать, это вполне приемлемо.
6.3 Длительный сетевой ввод-вывод
Это не является предметом их обсуждения.Я думаю, что оптимизация этой проблемы может контролировать период ожидания сетевых вызовов.Если период ожидания всех сетевых вызовов суммируется, то наше время истечения блокировки на самом деле должно быть больше, чем это время , Конечно, мы также можем передавать сетевые вызовы Optimize, такие как последовательный, в параллельный, асинхронный и т. д. Вы можете ознакомиться с двумя моими статьями:Распараллеливание — ваш убийца параллелизма,Асинхронный — ваш убийца параллелизма
7. Некоторые оптимизации Chubby
Когда вы будете искать ZK, вы обнаружите, что все они писали, что ZK — это реализация Chubby с открытым исходным кодом, а внутренний принцип работы Chubby аналогичен ZK. Но позиционирование Chubby заключается в том, что распределенные блокировки немного отличаются от ZK. Чабби также использует приведенную выше схему самоинкрементной последовательности для решения проблемы распределенной незащищенности, но он предоставляет различные методы проверки:
- CheckSequencer(): вызовите Chubby API, чтобы проверить, действителен ли секвенсор в данный момент.
- Получите доступ к серверу ресурсов, чтобы проверить, чтобы определить последний серийный номер текущего сервера ресурсов и размер нашего серийного номера.
- lock-delay: чтобы предотвратить вторжение нашей логики проверки на наш сервер ресурсов, он предоставляет метод, при котором, когда клиент теряет соединение, он не снимает блокировку немедленно, а блокирует ее в течение определенного периода времени (по умолчанию 1 минута). Другие клиенты берут эту блокировку, а это значит, что дается определенный буфер для ожидания восстановления STW, и если время STW нашего GC больше 1 минуты, вам следует проверить свою программу, а не сомневаться в вашей распределенной блокировке.
8. Резюме
В этой статье в основном рассказывается о различных методах реализации распределенных блокировок, а также о некоторых их преимуществах и недостатках. Наконец, я также говорил о безопасности распределенных замков.Уровни безопасности, требуемые для разных предприятий, совершенно разные.Нам нужно выбрать наиболее подходящее решение в соответствии с нашими собственными бизнес-сценариями и с помощью анализа различных измерений.
Наконец, эта статья была включена в JGrowing, всеобъемлющий и отличный маршрут изучения Java, совместно созданный сообществом.Если вы хотите участвовать в обслуживании проектов с открытым исходным кодом, вы можете создать его вместе.Адрес github:GitHub.com/Java растет…Пожалуйста, дайте мне маленькую звезду.
Наконец, сделайте рекламу.Если вы считаете, что в этой статье есть статья для вас, вы можете подписаться на мой технический общедоступный аккаунт или присоединиться к моей группе технического обмена для большего количества технических обменов. Ваше внимание и пересылка - самая большая поддержка для меня, O(∩_∩)O.