Uuid аппаратного обеспечения что это
UUID версии 7, или как не потеряться во времени при создании идентификатора
В течение многих лет я противостоял засилью UUID как ключей в базах данных, но со временем и практикой до меня дошло. Они действительно удобны, когда речь идёт о распределённых системах. Генерировать новый идентификатор на разных концах планеты не так-то просто. Создание псевдослучайных идентификаторов решает эту проблему.
Хотя, подобные решения, не всегда хороши. В отличие от обыкновенных цифровых значений, которые легко кешировать и сортировать, UUID не так гибки в использовании. UUID версии 7 предназначен как раз для того, чтобы разобраться с подобными проблемами.
Добро пожаловать в мир отсортированных случайностей.
Сами по себе UUID это не просто набор случайных битов. Существует несколько вариантов их генерации. @AloneCoder в своей статье как генерируются UUID в подробностях описывает уже существующие форматы идентификаторов, версии с первой по пятую.
UUID в базах данных
Почему нам необходимо генерировать UUID, а не просто брать случайные данные? Ну, ответов может быть множество. Сохранение данных о хосте, который сгенерировал последовательность, сохранение времени и тому подобных значений, позволяет сделать UUID более информативными. Подобный подход можно использовать при создании распределённых вычислительных систем. Например, вместо того, чтобы грузить базу данных запросами с датой, можно просто выбрать те идентификаторы, которые содержат в себе эту дату.
Всё бы хорошо, но вот именно это и не очень-то просто. Выбирать даты из строковых значений UUID это та ещё свистопляска. Почему? Ну, давайте посмотрим на последовательность генерации UUIDv1.
Берутся младшие 32 бита текущей временной метки UTC. Это будут первые 4 байта (8 шестнадцатеричных символов) UUID [ TimeLow ].
Берутся средние 16 битов текущей временной метки UTC. Это будут следующие 2 байта (4 шестнадцатеричных символа) [ TimeMid ].
Следующие 2 байта (4 шестнадцатеричных символа) конкатенируют 4 бита версии UUID с оставшимися 12 старшими битами текущей временной метки UTC (в которой всего 60 битов) [ TimeHighAndVersion ].
Как всё замечательно запутано. На самом деле, распарсить дату из такого идентификатора достаточно просто, но парсинг это парсинг. Это не весело и нагружает процессор.
Герой дня
Основная разработка ведётся силами двух разработчиков: bradleypeabody и kyzer-davis. Хабрачеловеки и хабраалиены могут поучаствовать в обсуждении и написании формата на гитхабе https://github.com/uuid6/uuid6-ietf-draft/.
Пять дней назад эта спецификация вызвала оживлённую дискуссию на hackernews.
При разработке спецификаций, были рассмотрены следующие форматы генерации UUID:
И так, что же такого особого в UUIDv7 и чем он отличается от предыдущих версий?
Эта версия идентификатора бинарно-сортируемая. То есть, вам больше не нужно конвертировать значения UUID в какой-либо другой формат, чтобы понять какое из них больше и меньше.
Ну а нам-то какая разница? Можно же сделать select id, creation_date order by creation_date и жить себе спокойно.
Вы не поняли вопроса.
Тут дело не в том, как вам, программисту, удобнее делать SELECT. Вопрос в том, как база данных хранит индексы. Созданные последовательно, UUIDv4 будут выглядеть случайными. Соответственно, при записи значений этих индексов в базу данных, даже если значения были созданы в один и тот же промежуток времени, кластеризация будет нагружать индексы при записи.
Представьте, у вас есть высоконагруженная система. 100 серверов генерируют новые записи с UUID несколько раз в секунду, и всё это летит в Redis, которые грузит эти данные в Postgresql.
Ага. Вот тут вот жизнь с UUIDv7 становится проще. Значения индексов не настолько разбросаны и следить за ними намного проще.
Плюс, значения ключей можно очень просто и быстро отсортировать в бинарном формате. Возьмите первые 64 бита идентификатора, и сравните их как int64 с другим идентификатором, и вы уже знаете, какой был создан раньше.
Но, как же это работает?
Ок, в отношении самой даты — тут всё просто. Запишите число, как unix timestamp и у вас есть что-то бинарно-сортируемое. Только я вас прошу, не стоит записывать эту дату кусками, в разнобой. Просто и понятно, первые 36 битов содержат в себе одно число. Но, если вы пытаетесь записать миллисекунды, то всё становится сложнее.
Давайте поговорим о математике. О приближении и лимитах. Любимая тема, а? Давайте посмотрим на следующую запись секунды: 05,625. Пять целых, шестьсот двадцать пять секунд. Отбрасываем 5, поскольку это будет записано в unix timestamp.
Если вы будете сохранять это значение в float, то выглядеть это будет страшно, с бинарной точки зрения. А если мы применим немного матана? Для начала разложим единицу в следующий ряд.
Достаточно, просто, правда? Если сложить все числа в этом ряду, то вы получите единицу. А что если складывать не каждый? Ну, с этим можно что-то сделать. Давайте присвоим каждому числу из этого ряда один бит. Каждый бит будет показывать, если этот член присутствует в ряду или нет.
Берём нашу суб-секундную точность, 0,625 и начинаем записывать эту точность с помощью битов.
Первое число 1/2, то есть 0,5. Если наше значение точности больше этого числа, то выставляем битовое значение в 1 и вычитаем это число из нашего текущего значения точности. В итоге получаем, битовую последовательность 1 и 0,125 в остатке.
Что самое приятное, в этой системе, так это то, что если вы урезаете количество бит с конца, то вы будете терять точность, но всё-же сохраните более или менее приближенное значение.
Значение сохраняется, и при этом, вы можете играть с количеством битов, которые вы расходуете на его запись. И — что самое главное — подобная запись производит бинарно-сортируемые значения.
Более того, существует очень простой математический способ выполнения этой операции. Для того чтобы закодировать любое число, сделайте следующее:
Ну и, понятное дело, для того, чтобы раскодировать, нужно сделать обратное.
Что мы получаем в итоге? Возможность сохранения времени с суб-секундной точностью в бинарно-сортируемом формате.
А в случае коллизий?
Если вы генерируете значения на одном узле, то существует вероятность, что даже при наличии суб-секундной точности вы можете создать два идентификатора в один промежуток времени.
Для этого стандарт предусматривает наличие счётчика произвольной длинны, который должен монотонно увеличиваться, если даты совпадают.
И в дополнение, можно задать произвольное количество битов, которые будут идентифицировать компьютер, сгенерировавший значение. (Записывать MAC адрес в это поле не стоит, ибо уж слишком часто это вызывало вопросы с точки зрения безопасности).
Плюс, всё пространство, которое не используется для времени, счётчика и номера компьютера (порядка 54х бит) необходимо заполнять случайными значениями для предотвращения каких-либо совпадений на разных узлах.
При генерации стандартными методами UUID для практических целей уникальны. Их уникальность не зависит от центрального регистрирующего органа или координации между сторонами, их генерирующими, в отличие от большинства других схем нумерации. Хотя вероятность того, что UUID будет дублироваться, не равна нулю, она достаточно близка к нулю, чтобы ею можно было пренебречь.
Таким образом, любой может создать UUID и использовать его для идентификации чего-либо с почти уверенностью, что идентификатор не дублирует идентификатор, который уже был или будет создан для идентификации чего-то еще. Информация, помеченная UUID независимыми сторонами, может быть позже объединена в единую базу данных или передана по тому же каналу с незначительной вероятностью дублирования.
Принятие UUID широко распространено, и многие вычислительные платформы предоставляют поддержку для их генерации и анализа их текстового представления.
СОДЕРЖАНИЕ
История
Стандарты
UUID стандартизированы Open Software Foundation (OSF) как часть распределенной вычислительной среды (DCE).
Engineering Task Force Интернет (IETF) опубликовала стандарты-Track RFC 4122, технически эквивалент ITU-T Rec. X.667 | ИСО / МЭК 9834-8.
Формат
В каноническом текстовом представлении 16 октетов UUID представлены в виде 32 шестнадцатеричных (base-16) цифр, отображаемых в пяти группах, разделенных дефисами, в форме 8-4-4-4-12, всего 36 символов. (32 шестнадцатеричных символа и 4 дефиса). Например:
Четырехбитовые поля M и 1-3-битные поля N кодируют формат самого UUID.
Строка канонического формата 8-4-4-4-12 основана на структуре записи для 16 байтов UUID:
Имя | Длина (байты) | Длина (шестнадцатеричные цифры) | Длина (бит) | СОДЕРЖАНИЕ |
---|---|---|---|---|
time_low | 4 | 8 | 32 | целое число, дающее младшие 32 бита времени |
time_mid | 2 | 4 | 16 | целое число, дающее средние 16 бит времени |
time_hi_and_version | 2 | 4 | 16 | 4-битная «версия» в старших разрядах, за которыми следуют старшие 12 бит времени |
clock_seq_hi_and_res clock_seq_low | 2 | 4 | 16 | 1–3-разрядный «вариант» в наиболее значимых битах, за которым следует 13–15-разрядная тактовая последовательность. |
узел | 6 | 12 | 48 | 48-битный идентификатор узла |
Эти поля соответствуют полям UUID версий 1 и 2 (то есть UUID на основе времени), но одно и то же представление 8-4-4-4-12 используется для всех UUID, даже для UUID, построенных по-разному.
RFC 4122 Раздел 3 требует, чтобы символы создавались в нижнем регистре, при этом регистр не учитывался при вводе.
Идентификаторы GUID Microsoft иногда обозначаются фигурными скобками:
Этот формат не следует путать с « реестром Windows форматом», который относится к формату в фигурных скобках.
RFC 4122 определяет пространство имен Uniform Resource Name (URN) для UUID. UUID, представленный как URN, выглядит следующим образом:
Кодирование
Варианты
Поле «вариант» UUID или позиция N указывают их формат и кодировку. RFC 4122 определяет четыре варианта длины от 1 до 3 бит:
Варианты 1 и 2 используются текущей спецификацией UUID. В текстовом представлении варианты 1 и 2 одинаковы, за исключением битов варианта. В двоичном представлении существует разница в порядке байтов. Когда для преобразования между прямым порядком байтов варианта 1 и прямым порядком байтов варианта 2 требуется перестановка байтов, поля выше определяют перестановку. Первые три поля представляют собой 32- и 16-разрядные целые числа без знака и подлежат замене местами, в то время как последние два поля состоят из неинтерпретируемых байтов и не подлежат замене местами. Эта перестановка байтов применяется даже для версий 3, 4 и 5, где канонические поля не соответствуют содержимому UUID.
Версии
Для обоих вариантов 1 и 2 в стандартах определены пять «версий», и каждая версия может быть более подходящей, чем другие, в конкретных случаях использования. Версия указывается M в строковом представлении.
UUID версии 1 генерируются из времени и идентификатора узла (обычно MAC-адреса ); UUID версии 2 генерируются из идентификатора (обычно идентификатора группы или пользователя), времени и идентификатора узла; версии 3 и 5 производят детерминированные UUID, генерируемые хешированием идентификатора и имени пространства имен ; и UUID версии 4 генерируются с использованием случайного или псевдослучайного числа.
Nil UUID
Версия 1 (дата, время и MAC-адрес)
RFC 4122 позволяет заменять MAC-адрес в UUID версии 1 (или 2) случайным 48-битным идентификатором узла либо потому, что у узла нет MAC-адреса, либо потому, что его нежелательно раскрывать. В этом случае RFC требует, чтобы младший бит первого октета идентификатора узла был установлен в 1. Это соответствует биту многоадресной рассылки в MAC-адресах, и его установка служит для различения UUID, где идентификатор узла генерируется случайным образом. из UUID на основе MAC-адресов сетевых карт, которые обычно имеют одноадресные MAC-адреса.
Версия 2 (дата, время и MAC-адрес, версия безопасности DCE)
RFC 4122 резервирует версию 2 для UUID «безопасности DCE»; но не содержит подробностей. По этой причине во многих реализациях UUID не используется версия 2. Однако спецификация UUID версии 2 обеспечивается спецификацией служб аутентификации и безопасности DCE 1.1.
UUID версии 2 похожи на версию 1, за исключением того, что 8 младших битов тактовой последовательности заменяются номером «локального домена», а младшие 32 бита метки времени заменяются целочисленным идентификатором, имеющим значение в пределах указанного локальный домен. В системах POSIX номера локальных доменов 0 и 1 предназначены для идентификаторов пользователей ( UID ) и групп ( GID ) соответственно, а другие номера локальных доменов определяются сайтом. В системах, отличных от POSIX, все номера локальных доменов определяются сайтом.
Возможность включения 40-битного домена / идентификатора в UUID требует компромисса. С одной стороны, 40 бит позволяют использовать около 1 триллиона значений домена / идентификатора для каждого идентификатора узла. С другой стороны, когда значение часов усечено до 28 старших битов, по сравнению с 60 битами в версии 1, часы в UUID версии 2 будут «тикать» только один раз каждые 429,49 секунды, то есть чуть более 7 минут, поскольку в отличие от каждых 100 наносекунд для версии 1. И с тактовой последовательностью всего 6 бит, по сравнению с 14 битами в версии 1, только 64 уникальных UUID для каждого узла / домена / идентификатора могут быть сгенерированы за 7-минутный такт часов, по сравнению с 16 384 значения тактовой последовательности для версии 1. Таким образом, версия 2 может не подходить для случаев, когда требуются UUID для каждого узла / домена / идентификатора со скоростью, превышающей примерно один каждые семь минут.
Версии 3 и 5 (на основе имен пространств имен)
Чтобы определить UUID версии 3, соответствующий заданному пространству имен и имени, UUID пространства имен преобразуется в строку байтов, объединяется с именем ввода, затем хешируется с помощью MD5, что дает 128 бит. Затем 6 или 7 бит заменяются фиксированными значениями, 4-битной версией (например, 0011 2 для версии 3) и 2- или 3-битным «вариантом» UUID (например, 10 2 указывает на RFC 4122 UUID или 110 2 с указанием устаревшего идентификатора GUID Microsoft). Поскольку таким образом заранее определены 6 или 7 бит, только 121 или 122 бита вносят вклад в уникальность UUID.
UUID версии 5 аналогичны, но вместо MD5 используется SHA-1. Поскольку SHA-1 генерирует 160-битные дайджесты, дайджест обрезается до 128 бит перед заменой битов версии и варианта.
UUID версии 3 и версии 5 обладают тем свойством, что одно и то же пространство имен и имя будут сопоставляться с одним и тем же UUID. Однако ни пространство имен, ни имя не могут быть определены из UUID, даже если один из них указан, кроме как методом перебора. RFC 4122 рекомендует версию 5 (SHA-1) вместо версии 3 (MD5) и предостерегает от использования UUID любой версии в качестве учетных данных безопасности.
Версия 4 (случайная)
UUID версии 4 генерируется случайным образом. Как и в других UUID, 4 бита используются для обозначения версии 4 и 2 или 3 бита для обозначения варианта (10 2 или 110 2 для вариантов 1 и 2 соответственно). Таким образом, для варианта 1 (то есть для большинства UUID) случайный UUID версии 4 будет иметь 6 заранее определенных битов варианта и версии, оставляя 122 бита для случайно сгенерированной части, всего 2122 или 5,3 × 10 36 (5,3 × 10). undecillion ) возможные UUID версии 4 варианта 1. Существует вдвое меньше возможных UUID версии 4 варианта 2 (устаревших идентификаторов GUID), потому что доступно на один случайный бит меньше, а для варианта используется 3 бита.
Столкновения
Конфликт возникает, когда один и тот же UUID генерируется более одного раза и назначается разным референтам. В случае стандартных UUID версии 1 и версии 2, использующих уникальные MAC-адреса от сетевых карт, коллизии маловероятны, с повышенной вероятностью только тогда, когда реализация отличается от стандартов, случайно или намеренно.
Таким образом, вероятность найти дубликат в 103 триллионах UUID версии 4 составляет один на миллиард.
Использует
Обычно используется в протоколах Bluetooth для определения служб и общего профиля Bluetooth.
В COM
Существует несколько разновидностей идентификаторов GUID, используемых в объектной модели компонентов (COM) Microsoft :
Как ключи базы данных
Некоторые веб-фреймворки, такие как Laravel, поддерживают UUID «сначала временная метка», которые могут эффективно храниться в индексируемом столбце базы данных. Это делает COMB UUID с использованием формата версии 4, но где первые 48 бит составляют метку времени, выложенную, как в UUIDv1. Более определенные форматы, основанные на идее COMB UUID, включают:
Про uuid-ы, первичные ключи и базы данных
Статья посвящена альтернативным версиям Qt-драйверов для работы с базами данных. По большому счету отличий от нативных Qt-драйверов не так много, всего пара: 1) Поддержка типа UUID; 2) Работа с сущностью «Транзакция» как с самостоятельным объектом. Но эти отличия привели к существенному пересмотру кодовой реализации исходных Qt-решений и изменили подход к написанию рабочего кода.
Первичный ключ: UUID или Integer?
Впервые с идеей использовать UUID в качестве первичного ключа я познакомился в 2003 году, работая в команде дельфистов. Мы разрабатывали программу для автоматизации технологических процессов на производстве. СУБД в проекте отводилась существенная роль. На тот момент это была FireBird версии 1.5. По мере усложнения проекта появились трудности с использованием целочисленных идентификаторов в качестве первичных ключей. Опишу пару сложностей:
Проблема архитектурная: периодически заказчики присылали справочные данные с целью включения их в новую версию дистрибутива. Иногда справочники содержали первичные ключи уже имеющиеся в нашей базе. Приходилось устранять коллизии в процессе агрегирования данных. На этом проблемы не заканчивались: при разворачивании нового дистрибутива периодически возникали обратные коллизии.
Проблема программная: чтобы получить доступ к вставленной записи нужно было выполнить дополнительный SELECT-запрос, который возвращал максимальное значение первичного ключа (значение для только что вставленной записи). Причем этот процесс должен был проходить в пределах одной транзакции. Далее можно было обновлять или корректировать запись. Это сейчас я знаю, что некоторые драйверы БД возвращают значение первичного ключа для вставленной записи, но в 2003 году мы такими знаниями не обладали, да и не припомню что бы Делфи-компоненты возвращали что-то подобное.
Использование UUID-ов в качестве первичных ключей сводило к минимуму архитектурную проблему, и полностью решало программную. UUID-ключ генерировался перед началом вставки записи на стороне программы, а не в недрах сервера БД, таким образом дополнительный SELECT-запрос стал не нужен, и требование единой транзакции утратило актуальность. FireBird версии 1.5 не имел нативной поддержки UUID-ов, поэтому использовались строковые поля длинной в 32 символа (дефисы из UUID-ов удалялись). Факт использования строковых полей в качестве первичных ключей нисколько не смущал, нам не терпелось опробовать новый подход при работе с данными.
У UUID-ов есть свои минусы: 1) Существенный объем; 2) Более низкая скорость работы по сравнению с целочисленными идентификаторами. В рамках проекта достоинства оказались более значимы, чем указанные недостатки. В целом, опыт оказался положительным, поэтому в последующих решениях при создании реляционных связей предпочтение отдавалось именно UUID-ам.
Примечание: Более подробный анализ UUID vs Integer для СУБД MS SQL можно посмотреть в статье «Первичный ключ – GUID или автоинкремент?»
Первый драйвер для FireBird
В 2012 году мне снова довелось поработать с FireBird. Нужно было создать небольшую программу по анализу данных. Разработка велась с использованием QtFramework. Примерно в это же время у FireBird вышла версия 2.5 с нативной поддержкой UUID-ов. Я подумал: «Почему бы не добавить в Qt-драйвер для FireBird поддержку типа QUuid?» Так появилась первая версия Qt-драйвера с поддержкой UUID-ов. Этот вариант не сильно отличался от оригинальной версии драйвера и, в основном, был ориентирован на использование в однопоточных приложениях.
Появление сущности «Транзакция»
Следующая модификация Qt-драйвера для FireBird произошла в конце 2018 года. Наша фирма взялась за разработку проекта по анализу данных большого объема. Для фирмы выросшей из стартап-а эта работа была очень важна, как с финансовой, так и с репутацио́нной точек зрения. Сроки исполнения были весьма жесткие. В качестве СУБД была выбрана FireBird, несмотря на определенные сомнения в ее пригодности. Хорошим вариантом могла бы стать PostgreSQL, но у нашей команды на тот момент отсутствовал опыт эксплуатации данной СУБД.
Новая концепция не нуждалась в сущности «транзакция», как в самостоятельной единице, тем не менее, я не стал ее упразднять. Дальнейшая эксплуатация показала, что наличие объекта «транзакция» делает работу с базой данных более гибкой, дает больше инвариантов при написании кода. Например, разработчик может передать объект «Транзакция» в функцию в качестве параметра, явно говоря таким образом, что внутри нужно работать в контексте указанной транзакции. В функции можно проверить активна транзакция или нет, можно выполнить COMMIT или ROLLBACK. Для вспомогательных операций можно создавать альтернативную транзакцию, не затрагивающую основную. Таких возможностей нет у нативных Qt-драйверов.
Ниже приведен пример с тремя функциями, которые последовательно вызываю друг друга. Каждая функция запрашивает объект подключения к базе данных (Driver) у пула коннектов. Так как функции вызываются в одном потоке, они получают объект коннекта, ссылающийся на одно и тоже подключение к БД. Далее в каждой функции создается собственный независимый объект транзакции и все последующие запросы будут выполняются в его контексте.
Приведенный пример не будет работать с нативным Qt-драйвером, причина описана выше: ограничение на одно подключение и одну транзакцию
В примере экземпляры транзакций (1-3) созданы для наглядности. В рабочем коде их можно опустить. В этом случае транзакции будут создаваться неявно внутри объекта QSqlQuery. Неявные транзакции всегда завершаются ROLLBACK-ом для SELECT-запросов и попыткой COMMIT-а для всех остальных.
Ниже показано как можно использовать одну транзакцию для трех sql-запросов. Подтвердить или откатить транзакцию можно в любой из трех функций.
Драйвер для PostgreSQL
Драйвер для MS SQL
Чего нет в классе Driver
Описываемые здесь драйверы не повторяют один в один функционал Qt-решений. В классе оставлены следующие методы:
С введением сущности «Транзакция» они утратили актуальность и нужны исключительно для отладки и диагностирования их вызовов из Qt-компонентов.
Ряд функций не используются нами в работе, поэтому они либо не реализованы, либо реализованы и помечены внутри программными точками останова, то есть разработчику при первом вызове придется их отладить. Вот эти функции:
Заморожена поддержка механизма событий. Обсудив с коллегами этот функционал, мы пришли к заключению, что на данном этапе в нем нет необходимости. Возможно, в будущем решение будет пересмотрено, но пока у нас нет серьезных доводов в пользу событийного механизма.
Новые функции
Лицензионные ограничения
Зависимости
В реализации драйверов используется система логирования ALog, которая является составной частью библиотеки общего назначения SharedTools.
Демо-примеры
Специально для этой статьи был создан демонстрационный проект. Он содержит примеры работы с тремя СУБД: FireBird, PostgreSQL, MS SQL. Репозиторий с драйверами расположен здесь, он подключен в проект как субмодуль. Библиотека SharedTools так же подключена как субмодуль.
Проект создан с использованием QtCreator, сборочная система QBS. Есть четыре сборочных сценария:
Драйвера в первую очередь разрабатывались для работы в Linux, поэтому эксплуатационное тестирование выполнялось именно для этой ОС. В Windows будет работать FireBird-драйвер (проверено), для остальных драйверов тестирование не проводилось.
Демо-примеры записывают следующие логи:
При первом запуске, примеры проверяют наличие тестовой базы данных. Если базы не обнаружено, в лог-файл будет выведен скрипт для ее создания.
Заключение
Черновой вариант статьи не предполагал наличие этого раздела, за что старый товарищ и, по совместительству, корректор подверг меня критике: «Мол, непонятна мотивация, целеполагание неясно. Зачем ты вообще писал эту статью?!» Что ж, исправляюсь!