** См. статью о поддержке плагина docker-maven-plugin.блюз доступен empty.com/2017/11/02/… **
1. Предпосылки
Архитектура распределенной системы или микросервиса в основном принимает структуру подбазы данных и подтаблицы, и потребность в генерации глобально уникального идентификатора становится очень острой. Традиционное одно приложение, использующее одну базу данных, может быть легко реализовано путем самоувеличения идентификатора в базе данных. После подбазы данных сначала требуется ключ подбазы, и ключ подбазы не должен повторяться, поэтому традиционный метод не может удовлетворить спрос. Подводя итог, каковы требования бизнес-системы к идентификационным номерам?
1. Глобальная уникальность: не должно быть повторяющихся идентификационных номеров.Поскольку это уникальный идентификатор, это самое основное требование.
2. Тенденция усиливается: в движке MySQL InnoDB используются кластеризованные индексы.Поскольку большинство СУБД используют структуру данных B-дерева для хранения данных индекса, мы должны попытаться использовать упорядоченные первичные ключи, чтобы обеспечить производительность записи при выборе первичных ключей. .
3. Монотонно возрастающий: Убедитесь, что следующий идентификатор должен быть больше, чем предыдущий идентификатор, например, номер версии транзакции, инкрементное сообщение IM, сортировка и другие специальные требования.
4. Информационная безопасность: если идентификатор является непрерывным, злоумышленникам очень легко подобрать работу, просто загрузите указанный URL-адрес последовательно, если это номер заказа, это еще более опасно.Конкуренты могут напрямую узнать наш ежедневный заказ количество. Поэтому в некоторых сценариях приложений идентификаторы должны быть нерегулярными и нерегулярными.
где пункты 3 и 4 исключают друг друга. Помимо функциональных требований, существуют требования к производительности и надежности:
- Средняя задержка и задержка TP999 должны быть как можно меньше;
- Доступность 5 девяток;
- Высокий QPS.
2. Продвинутый курс
Поскольку проект был разделен с монолитного приложения на микросервисную архитектуру, я провел некоторое исследование части глобального идентификатора.
2.1 uuid
В начале разделения бизнеса первичный ключ id использует строку uuid.
Стандартная форма UUID (Universally Unique Identifier) содержит 32 шестнадцатеричных числа, разделенных на пять сегментов дефисами, и 36 символов в виде 8-4-4-4-12. Такая строка:dc5adf0a-d531-11e5-95aa-3c15c2d22392
. 128 бит, не беспокойтесь о том, что этого недостаточно. Сгенерированный метод также очень прост:
UUID userId = UUID.randomUUID();
uuid уникален в мире, генерируется локально, без использования сети, и сгенерированная производительность абсолютно удовлетворительна. Недостаток тоже очевиден — занимает больше места, по сравнению с типом INT, требует больше места для хранения UUID. После использования UUID URL-адреса становятся подробными и недостаточно дружелюбными. Когда ID используется в качестве первичного ключа, в определенных средах будут некоторые проблемы, например, в сценарии с первичным ключом БД UUID очень не подходит:
- У MySQL официально есть четкая рекомендация, что первичный ключ должен быть как можно короче, а UUID длиной 36 символов не соответствует требованиям.
- Плохо для индекса MySQL: при использовании в качестве первичного ключа базы данных под движком InnoDB беспорядок UUID может привести к частым изменениям в расположении данных, что серьезно повлияет на производительность.
2.2 Создание базы данных
Возьмите MySQL в качестве примера, используйте настройку поляauto_increment_increment
иauto_increment_offset
Чтобы обеспечить автоматическое увеличение идентификатора, каждый бизнес использует следующий SQL для чтения и записи MySQL, чтобы получить идентификационный номер. СсылкаLeafИдея реализации:
- Сервер id извлекает сегмент номера из базы данных пакетами, кэширует сегмент номера локально и устанавливает порог, когда он достигает 0,8 (отношение емкости используемого сегмента номера), он автоматически получает новый сегмент номера и обновляет локально кэшированный сегмент номера. .
- Клиент id, то есть конкретный вызывающий экземпляр службы, также делает кэш локально, что аналогично кешу сервера id. Цель этого — уменьшить нагрузку на сервер id и уменьшить потребление сети rpc звонки.
Недостатками вышеописанной схемы являются:
- Сегмент номера теряется, независимо от того, какой клиент или сервер перезагружается, сегмент номера будет потрачен впустую.
- Числовой сегмент увеличивается напрямую, а не достаточно случайно, и слишком много информации открывается внешнему миру.
- Время простоя БД может сделать всю систему недоступной. Хотя кеш можно использовать для краткосрочного предоставления номера счета после того, как БД не работает, база данных по-прежнему сильно зависит. Общий подход, принятый Leaf, — аварийное восстановление с высокой доступностью:
Принимается режим одного ведущего и двух ведомых, одновременно развертывается вспомогательное машинное отделение, а для синхронизации данных между ведущим и ведомым используется метод полусинхронизации. В то же время DBProxy используется для переключения master-slave. Конечно, это решение в некоторых случаях будет вырождаться в асинхронный режим, и даже в крайних случаях все равно будет вызывать несогласованность данных, но вероятность возникновения очень мала.
3. Схема снежинки
3.1 Введение
Учитывая недостатки приведенных выше схем, автор исследовал другие схемы генерации, и снежинка является одной из них. Проблема нарастания тренда и недостаточной случайности может быть полностью решена в снежинке.Идентификатор снежинки имеет длину 64 бита и состоит из следующих трех частей:
-
Первый бит равен 0, не нужно.
-
отметка времени — 41 бит, с точностью до мс, что означает, что она может представлять до (2^41-1)/(1000360024*365)=139,5 лет. Кроме того, пользователи могут сами определить начальную эпоху, а затем использовать ( Текущая time - начальная эпоха) для расчета времени, а это значит, что временная часть не будет повторяться через 140 лет.В официальном документе здесь написано 41 бит, что должно быть неправильно. Кроме того, есть еще одна очень важная причина для использования здесь времени, то есть его можно сортировать непосредственно по времени.Для приложений, таких как твиттер, которые часто обновляются, сортировка по времени особенно важна.
-
идентификатор машины — 10 бит, эта часть фактически состоит из двух частей, datacenterId и workerId, которые указаны в файле конфигурации.
- datacenterId удобен для создания нескольких служб, которые генерируют uid, и гарантирует, что uid не повторяется.Например, в datacenter0 машины 0, 1 и 2 образуют службу, которая генерирует uid, а datacenter1 также нуждается в службе, которая генерирует uid в на этот раз Получение uid, очевидно, является самым быстрым и удобным, поэтому его можно построить в собственном центре, лишь бы datacenterId был уникальным. Если нет datacenterId, т. е. используется 10 бит, то вы должны знать идентификатор, который используется в настоящее время, прежде чем создавать новую службу, в противном случае нельзя гарантировать, что сгенерированный идентификатор будет уникальным, например, два созданных uid В службе есть машины с идентификатором машины 100. Если время сервера одинаково, неизбежно будет сгенерирован один и тот же идентификатор.
- WorkerId — это кодовое имя фактического сервера, максимальное значение — 32, и workerId в одном и том же центре обработки данных не может повторяться. Он будет зарегистрирован в консуле, чтобы гарантировать, что workerId не занят другими машинами, и в нем будет храниться значение host:port.После успешной регистрации сервисы могут быть предоставлены внешнему миру.
-
id последовательности — 12 бит, id может представлять 4096 чисел, это в случае одного и того же времени, увеличивать значение до тех пор, пока оно не станет 0, то есть цикл заканчивается, дальше можно только ждать, пока не придет следующая мс, в общем 4096 / Запрос мс маловероятен, так что достаточно использовать.
3.2 Идеи реализации
Решение со снежинкой, когда генерируется сервер идентификаторов, не зависит от БД, что может обеспечить производительность, а сгенерированный идентификатор является достаточно случайным. Каждую миллисекунду рабочий может генерировать 4096 идентификаторов, и если это число превысит, он будет заблокирован до следующей миллисекунды. Для тех систем с большим количеством параллелизма этого явно недостаточно, поэтому на этот раз нужно различать datacenterId и workerId, эти два идентификатора 5 бит соответственно, всего 10 бит, а максимальное значение 1024 (0- 1023) , В этом случае теоретическое максимальное количество идентификаторов, которые снежинка может сгенерировать за одну миллисекунду, составляет около 42 Вт, что является очень большим числом элементов и теоретически может удовлетворить параллелизм большинства систем.
Это решение зависит от системных часов, и необходимо учитывать проблему обратного вызова часов. LastTimestamp последнего запроса кэшируется локально.Когда поток приходит для получения ID, он сначала проверяет, меньше ли текущее время, чем timestamp, сгенерированный последним ID. Если оно меньше, чем системные часы были изменены, резервный вариант должен вызвать исключение до последнего времени генерации идентификатора! Это может решить проблему изменения системных часов во время работы.
Другая ситуация заключается в том, что при запуске серверной службы происходит обратный набор системного времени (хоть и крайнее, но все же указанное в учете), что может конфликтовать с ранее сгенерированным id и не является глобально уникальным. Решение здесь состоит в том, чтобы использовать консул компонента обнаружения и регистрации службы проекта, сохранить последний lastTimestamp в кластере консула, а ключом является соответствующий идентификатор машины. Непротиворечивость консула основана на алгоритме raft и использует протокол Gossip:
Consul uses a gossip protocol to manage membership and broadcast messages to the cluster. All of this is provided through the use of the Serf library.
Подробный алгоритм протокола см.Gossip.
Каждый раз, когда экземпляр сервера запускается, при создании экземпляра идентификатора для создания bean-компонента он сначала проверяет текущее время и размер lastTimestamp, соответствующий рабочему процессу в кластере консула.Если текущее время слишком мало, будет выдано исключение, и служба не запустится, и будет выдан сигнал тревоги.
При создании экземпляра проверьте:
public IdServiceImpl(long workerId, ConsulClient consulClient) {
if (workerId > idMeta.MAX_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", idMeta.MAX_ID));
}
this.workerId = workerId;
this.consulClient = consulClient;
validateStoredTimestamp();
log.info("worker starting. timestamp left shift {}, worker id bits {}, sequence bits {}, workerid {}", idMeta.TIMESTAMP_LEFT_SHIFT_BITS, idMeta.ID_BITS, idMeta.SEQUENCE_BITS, workerId);
}
Проверить функцию:
/**
* checks for timestamp by workerId when server starts.
* if server starts for the first time, just let it go and log warns.
* if current timestamp is smaller than the value stored in consul server, throw exception.
*/
private void validateStoredTimestamp() {
long current = timeGen();
Response<GetValue> keyValueResponse = consulClient.getKVValue(String.valueOf(workerId));
if (keyValueResponse.getValue() != null) {
lastTimestamp = Long.parseLong(keyValueResponse.getValue().getDecodedValue());
validateTimestamp(current, lastTimestamp, Periods.START);
} else {
log.warn(String.format("clock in consul is null. Generator works as for the 1st time."));
}
}
validateTimestamp:
/**
* 如果当前时间戳小于上一次ID生成的时间戳,说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!!!
*
* @param lastTimestamp 上一次ID生成的时间戳
* @param timestamp 当前时间戳
*/
private void validateTimestamp(long timestamp, long lastTimestamp, Periods period) {
if (timestamp < lastTimestamp) {
log.error(String.format("clock is moving backwards. Rejecting requests until %d.", lastTimestamp));
throw new IllegalStateException(String.format("Clock moved backwards in %s. Refusing to generate id for %d milliseconds", period, lastTimestamp - timestamp));
}
}
Метод получения идентификатора:
/**
* 生成ID(线程安全)
*
* @return id
*/
public synchronized long genId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!!!
validateTimestamp(timestamp, lastTimestamp, Periods.RUNNING);
//如果是同一时间生成的,则进行毫秒内sequence生成
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & IdMeta.SEQUENCE_MASK;
//溢出处理
if (sequence == 0) {//阻塞到下一毫秒,获得新时间戳
timestamp = tilNextMillis(lastTimestamp);
}
} else {//时间戳改变,毫秒内sequence重置
sequence = 0L;
}
//上次生成ID时间截
lastTimestamp = timestamp;
consulClient.setKVValue(String.valueOf(workerId), String.valueOf(lastTimestamp));
//移位并通过或运算组成64位ID
return ((timestamp - idMeta.START_TIME) << idMeta.TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << idMeta.ID_SHIFT_BITS) | sequence;
}
Адрес источника этой статьи:
Гитхаб:GitHub.com/mayets2012/is…
Облако кода:git ee.com/canets/snow f…
Подписывайтесь на свежие статьи, приглашаю обратить внимание на мой публичный номер
Ссылаться на: