Как создать упорядоченный по времени UID в Python? - PullRequest
9 голосов
/ 13 мая 2019

Возможно ли это?Я слышал, что у Кассандры что-то похожее: https://datastax.github.io/python-driver/api/cassandra/util.html

Я использовал ISO timestamp, соединенный с uuid4, но это оказалось слишком большим (58 символов) и, вероятно, излишним.

Сохранение порядкового номера не работает в моем контексте (DynamoDB NoSQL)

Стоит отметить, что для моего приложения не имеет значения, если элементы создаются в партии / в ту же секундув случайном порядке, пока uid не свернется.

У меня нет особых ограничений на максимальную длину, в идеале я хотел бы видеть разные шансы на столкновение для разных длин, но это требуетбыть меньше 58 (моя первоначальная попытка)

Используется с DynamoDB (база данных NoSQL) в качестве ключа сортировки

Ответы [ 3 ]

9 голосов
/ 16 мая 2019

Почему uuid.uuid1 не является последовательным

uuid.uuid1(node=None, clock_seq=None) эффективно:

  • 60 битов метки времени (представляющих количество интервалов в 100 нс после 1582-10-15 00:00:00)
  • 14 бит "тактовой последовательности"
  • 48 бит «информации об узле» (генерируется из mac-адреса сетевой карты или из имени хоста или из RNG).

Если вы не предоставите никаких аргументов, то вызывается системная функция для генерации uuid. В этом случае:

  • Неясно, является ли «последовательность часов» последовательной или случайной.
  • Неясно, безопасно ли использоваться в нескольких процессах (может ли clock_seq повторяться в разных процессах или нет?). В Python 3.7 эта информация теперь доступна .

Если вы укажете clock_seq или node, то «используется чистая реализация Python». В этом случае даже с «фиксированным значением» для clock_seq:

  • часть метки времени гарантированно будет последовательной для всех вызовов в текущем процессе, даже в поточном исполнении.
  • clock_seq часть генерируется случайным образом. Но это не критично, потому что временная метка является последовательной и уникальной.
  • Это НЕ безопасно для нескольких процессов (процессы, которые вызывают uuid1 с одним и тем же clock_seq, node, могут возвращать конфликтующие значения при вызове в течение "того же интервала времени 100 нс")

Решение, которое повторно использует uuid.uuid1

Легко видеть, что вы можете сделать uuid1 последовательным, указав clock_seq или node аргументов (для использования реализации Python).

import time

from uuid import uuid1, getnode

_my_clock_seq = getrandbits(14)
_my_node = getnode()


def sequential_uuid(node=None):
    return uuid1(node=node, clock_seq=_my_clock_seq)
    # .hex attribute of this value is 32-characters long string


def alt_sequential_uuid(clock_seq=None):
    return uuid1(node=_my_node, clock_seq=clock_seq)


if __name__ == '__main__':
    from itertools import count
    old_n = uuid1()  # "Native"
    old_s = sequential_uuid()  # Sequential

    native_conflict_index = None

    t_0 = time.time()

    for x in count():
        new_n = uuid1()
        new_s = sequential_uuid()

        if old_n > new_n and not native_conflict_index:
            native_conflict_index = x

        if old_s >= new_s:
            print("OOops: non-sequential results for `sequential_uuid()`")
            break

        if (x >= 10*0x3fff and time.time() - t_0 > 30) or (native_conflict_index and x > 2*native_conflict_index):
            print('No issues for `sequential_uuid()`')
            break

        old_n = new_n
        old_s = new_s

    print(f'Conflicts for `uuid.uuid1()`: {bool(native_conflict_index)}')

Проблемы с несколькими процессами

НО если вы выполняете несколько параллельных процессов на одной и той же машине, то:

  • node, по умолчанию uuid.get_node() будет одинаковым для всех процессов;
  • clock_seq имеет небольшой шанс быть одинаковым для некоторых процессов (шанс 1/16384)

Это может привести к конфликтам! Это общая проблема для использования uuid.uuid1 в параллельных процессах на одной машине, если у вас нет доступа к SafeUUID из Python3.7.

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

Даже если вы используете SafeUUID и устанавливаете уникальные node, все равно возможно иметь непоследовательные (но уникальные) идентификаторы, если они генерируются в разных процессах.

Если допустимы некоторые накладные расходы, связанные с блокировкой, вы можете сохранить clock_seq в некотором внешнем атомарном хранилище (например, в «заблокированном» файле) и увеличивать его при каждом вызове: это позволяет то же значение для node во всех параллельных процессах, а также для последовательных идентификаторов. Для случаев, когда все параллельные процессы являются подпроцессами, созданными с помощью multiprocessing: clock_seq может быть «разделен» с помощью multiprocessing.Value

В результате вы всегда должны помнить:

  • Если вы выполняете несколько процессов на одном компьютере, вы должны:

    • Обеспечить уникальность node. Проблема для этого решения: вы не можете быть уверены, что последовательные идентификаторы из разных процессов генерируются в течение одного и того же интервала времени 100 нс. Но это очень «легкая» операция, которая выполняется один раз при запуске процесса и достигается путем «добавления» чего-либо в узел по умолчанию, например, int(time.time()*1e9) - 0x118494406d1cc000 или путем добавления некоторого счетчика из атомарного уровня в машинном уровне.

    • Убедитесь, что "на уровне машины clock_seq" и то же node для всех процессов на одной машине. Таким образом, у вас будут некоторые накладные расходы для «блокировки» clock_seq, но идентификаторы гарантированно будут последовательными, даже если они генерируются в разных процессах в течение одного и того же интервала времени 100 нс (если только вы не вызываете uuid из нескольких потоков в одном и том же процесс).

  • Для процессов на разных машинах:

    • либо вы должны использовать какую-то "глобальную счетную службу";

    • или невозможно создать последовательные идентификаторы на разных машинах в течение одного интервала времени 100 нс.

Уменьшение размера идентификатора

Общий подход к генерации UUID: , довольно простой , поэтому легко реализовать нечто подобное с нуля и, например, использовать меньше битов для node_info part:

import time
from random import getrandbits

_my_clock_seq = getrandbits(14)
_last_timestamp_part = 0
_used_clock_seq = 0


timestamp_multiplier = 1e7  # I'd recommend to use this value

# Next values are enough up to year 2116:
if timestamp_multiplier == 1e9:
    time_bits = 62  # Up to year 2116, also reduces chances for non-sequential id-s generated in different processes
elif timestamp_multiplier == 1e8:
    time_bits = 60  # up to year 2335
elif timestamp_multiplier == 1e7:
    time_bits = 56  # Up to year 2198.
else:
    raise ValueError('Please calculate and set time_bits')

time_mask = 2**time_bits - 1

seq_bits = 16
seq_mask = 2**seq_bits - 1

node_bits = 12
node_mask = 2**node_bits - 1

max_hex_len = len(hex(2**(node_bits+seq_bits+time_bits) - 1)) - 2  # 21

_default_node_number = getrandbits(node_bits)  # or `uuid.getnode() & node_mask`


def sequential_uuid(node_number=None):
    """Return 21-characters long hex string that is sequential and unique for each call in current process.

    Results from different processes may "overlap" but are guaranteed to
    be unique if `node_number` is different in each process.

    """
    global _my_clock_seq
    global _last_timestamp_part
    global _used_clock_seq
    if node_number is None:
        node_number = _default_node_number
    if not 0 <= node_number <= node_mask:
        raise ValueError("Node number out of range")

    timestamp_part = int(time.time() * timestamp_multiplier) & time_mask
    _my_clock_seq = (_my_clock_seq + 1) & seq_mask

    if _last_timestamp_part >= timestamp_part:
        timestamp_part = _last_timestamp_part
        if _used_clock_seq == _my_clock_seq:
            timestamp_part = (timestamp_part + 1) & time_mask
    else:
        _used_clock_seq = _my_clock_seq

    _last_timestamp_part = timestamp_part

    return hex(
        (timestamp_part << (node_bits+seq_bits))
        |
        (_my_clock_seq << (node_bits))
        |
        node_number
    )[2:]

Примечания:

  • Может быть, лучше просто хранить целочисленное значение (не шестнадцатеричное) в базе данных
  • Если вы храните его как text / char, то лучше преобразовать целое число в base64-строку, чем преобразовывать его в шестнадцатеричную строку. Таким образом, он будет короче (21-символьная шестнадцатеричная строка → 16-символьная строка в кодировке b64):
from base64 import b64encode

total_bits = time_bits+seq_bits+node_bits
total_bytes = total_bits // 8 + 1 * bool(total_bits % 8)

def int_to_b64(int_value):
    return b64encode(int_value.to_bytes(total_bytes, 'big'))

Вероятность столкновения

  • Одиночный процесс: столкновения невозможны
  • Несколько процессов с вручную установленными уникальными clock_seq или уникальными node в каждом процессе: коллизии невозможны
  • Несколько процессов со случайно установленными node (48 бит, «фиксировано» во времени):

    • Вероятность столкновения node в нескольких процессах:

      • в 2 процессах из 10000: ~ 0,000018%
      • в 2 процессах из 100000: 0,0018%
    • Вероятность одного столкновения идентификатора в секунду в 2 процессах с "столкновением" node:

      • для интервала "отметка времени" 100 нс (по умолчанию для uuid.uuid1 и в моем коде, когда timestamp_multiplier == 1e7): пропорционально 3.72e-19 * avg_call_frequency²

      • для интервала «отметка времени» в 10 нс (timestamp_multiplier == 1e8): пропорционально 3.72e-21 * avg_call_frequency²

2 голосов
/ 14 мая 2019

В статье, которую вы тоже связали, cassandra.util.uuid_from_time (time_arg, node = None, clock_seq = None) [source] , похоже, именно то, что вы ищете.

def uuid_from_time(time_arg, node=None, clock_seq=None):
    """
    Converts a datetime or timestamp to a type 1 :class:`uuid.UUID`.

    :param time_arg:
      The time to use for the timestamp portion of the UUID.
      This can either be a :class:`datetime` object or a timestamp
      in seconds (as returned from :meth:`time.time()`).
    :type datetime: :class:`datetime` or timestamp

    :param node:
      None integer for the UUID (up to 48 bits). If not specified, this
      field is randomized.
    :type node: long

    :param clock_seq:
      Clock sequence field for the UUID (up to 14 bits). If not specified,
      a random sequence is generated.
    :type clock_seq: int

    :rtype: :class:`uuid.UUID`

    """
    if hasattr(time_arg, 'utctimetuple'):
        seconds = int(calendar.timegm(time_arg.utctimetuple()))
        microseconds = (seconds * 1e6) + time_arg.time().microsecond
    else:
        microseconds = int(time_arg * 1e6)

    # 0x01b21dd213814000 is the number of 100-ns intervals between the
    # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
    intervals = int(microseconds * 10) + 0x01b21dd213814000

    time_low = intervals & 0xffffffff
    time_mid = (intervals >> 32) & 0xffff
    time_hi_version = (intervals >> 48) & 0x0fff

    if clock_seq is None:
        clock_seq = random.getrandbits(14)
    else:
        if clock_seq > 0x3fff:
            raise ValueError('clock_seq is out of range (need a 14-bit value)')

    clock_seq_low = clock_seq & 0xff
    clock_seq_hi_variant = 0x80 | ((clock_seq >> 8) & 0x3f)

    if node is None:
        node = random.getrandbits(48)

    return uuid.UUID(fields=(time_low, time_mid, time_hi_version,
                             clock_seq_hi_variant, clock_seq_low, node), version=1)

Нет ничего особенного для Кассандры UUID типа 1 ...

0 голосов
/ 21 мая 2019

Вы должны иметь возможность кодировать метку времени с точностью до секунды для диапазона времени 135 лет в 32 битах. Это займет всего 8 символов для представления в шестнадцатеричном виде. Добавлено в шестнадцатеричное представление uuid (32 шестнадцатеричных символа), которое будет содержать только 40 шестнадцатеричных символов.

Для кодирования метки времени таким способом необходимо выбрать базовый год (например, 2000) и вычислить количество дней до текущей даты (метка времени). Умножьте это количество дней на 86400, затем добавьте секунды с полуночи. Это даст вам значения, которые меньше 2 ^ 32, пока вы не достигнете 2135 года.

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

Имея несколько битов в отметке времени, вы можете увеличить диапазон времени и / или точность. Еще 8 битов (два шестнадцатеричных символа) позволяют увеличить время до 270 лет с точностью до сотых долей секунды.
Обратите внимание, что вам не нужно моделировать долю секунд в базовом диапазоне 10. Вы получите оптимальное использование битов, разделив его на 128-е вместо 100-х для того же количества символов. С удвоением диапазона года это все еще умещается в пределах 8 бит (2 шестнадцатеричных символа)

Вероятность столкновения в пределах временной точности (то есть в секунду или на 100 или 128-ю секунды) определяется диапазоном uuid, поэтому для выбранной точности она будет равна 1 в 2 ^ 128. Увеличение точности отметки времени оказывает наибольшее влияние на снижение вероятности столкновения. Это также фактор, который оказывает наименьшее влияние на общий размер ключа.

Более эффективное кодирование символов: от 27 до 29 символов

Вы можете значительно уменьшить размер ключа, кодируя его в базе 64 вместо 16, что даст вам от 27 до 29 символов (в зависимости от вашего выбора точности)

Обратите внимание, что для части метки времени вам необходимо использовать функцию кодирования, которая принимает целое число в качестве входных данных и сохраняет последовательность сортировки цифровых символов.

Например:

def encode64(number, size):
    chars  = "+-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    result = list()
    for _ in range(size):
        result.append(chars[number%64])
        number //= 64
    return "".join(reversed(result))

a = encode64(1234567890,6) # '-7ZU9G'
b = encode64(9876543210,6) # '7Ag-Pe'
print(a < b)               # True

u = encode64(int(uuid.uuid4()),22) # '1QA2LtMg30ztnugxaokVMk'

key = a+u  # '-7ZU9G1QA2LtMg30ztnugxaokVMk'  (28 characters)

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

Для функции encode64 () требуется один символ каждые 6 бит.

Итак, в течение 135 лет с точностью до секунды: (32 + 128) / 6 = 26,7 -> 27 символов

вместо (32/6 = 5,3 -> 6) + (128/6 = 21,3 -> 22) ==> 28 символов

uid       = uuid.uuid4()
timeStamp = daysSince2000 * 86400 + int(secondsSinceMidnight)
key       = encode64( timeStamp<<128 | int(uid) ,27)

с 270-летним интервалом и 128-й точностью: (40 + 128) / 6 = 28 символов

uid       = uuid.uuid4()
timeStamp = daysSince2000 * 86400 + int(secondsSinceMidnight)
precision = 128
timeStamp = timeStamp * precision + int(factionOfSecond * precision)
key       = encode64( timeStamp<<128 | int(uid) ,28)

С помощью 29 символов вы можете повысить точность до 1024th секунды и диапазона года до 2160 лет .

Маскировка UUID: клавиши длиной от 17 до 19 символов

Чтобы быть еще более эффективным, вы можете удалить первые 64 бита uuid (который уже является отметкой времени) и объединить его с вашей собственной отметкой времени. Это даст вам ключи длиной от 17 до 19 символов практически без потери предотвращения столкновений (в зависимости от вашего выбора точности).

mask  = (1<<64)-1
key   = encode64( timeStamp<<64 | (int(uid) & mask) ,19)

Целочисленные / цифровые клавиши?

В качестве заключительного замечания: если ваша база данных поддерживает очень большие целые числа или числовые поля (140 бит или более) в качестве ключей, вам не нужно преобразовывать объединенное число в строку. Просто используйте его непосредственно как ключ. Числовая последовательность timeStamp<<128 | int(uid) будет соответствовать хронологии.

...