Как я могу реализовать управление версиями без замены предыдущей записью в DynamoDB?

В настоящее время я вижу, что когда я использую управление версиями в DynamoDB, он меняет номер версии, но новая запись заменяет старую запись; то есть:

old

{ object:one, name:"hey", version:1}

новый

{ object:one, name:"ho", version:2}

Я хочу иметь BOTH записи в db; то есть:

old

{ object:one, name:"hey", version:1 }

новый

{ object:one, name:"hey", version:1}
{ object:one, name:"ho", version:2}

Можно ли это сделать?

Ответ 1

Я не думаю, что служба DynamoDB в настоящее время поддерживает линейное управление версиями. Если вам нужна функция управления версиями, вам нужно будет сделать это на вашей стороне.

В DynamoDB строка уникально идентифицируется по ее первичному ключу. Первичный ключ может быть либо HashKey-only, либо HashKey + RangeKey. Если вы хотите различать одну и ту же строку с разными версиями, вам нужно указать номер версии где-то в вашем первичном ключе.

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

Hash    Attr   Version
hey      a2     2
hey_v1   a1     1

после обновления строки до версии 3 таблица должна выглядеть так:

Hash    Attr   Version
hey      a3      3
hey_v1   a1      1
hey_v2   a2      2

Выполнение версий на стороне клиента всегда не идеально. например, для вышеупомянутого подхода, если вы выполните сканирование, вы также получите hey_V1 и hey_v2. сообщите мне, если это работает для вас или нет. Если у вас есть лучший способ сделать управление версиями на стороне клиента, пожалуйста, отправьте здесь.

Ответ 2

Я экспериментировал и вычислял то, что наиболее эффективно с точки зрения единиц чтения/записи и стоимости, учитывая условия гонки, когда обновления регистрируются во время регистрации версии, и избегая дублирования данных. Я сузил пару возможных решений. Вы должны рассмотреть свой лучший вариант.

Основные концепции вращаются вокруг рассмотрения версии 0 как последней версии. Кроме того, мы будем использовать клавишу revisions, которая будет перечислять, сколько ревизий существует до этого элемента, но также будет использоваться для определения текущей версии элемента (version = revisions + 1). Возможность вычислить, как существуют версии, является требованием, и, по моему мнению, revisions удовлетворяет эту потребность, а также значение, которое может быть представлено пользователю.

Таким образом, первая строка будет создана с помощью version: 0 и revisions: 0. Хотя технически это первая версия (v1), мы не применяем номер версии, пока он не заархивирован. Когда эта строка изменяется, version остается на 0, который по-прежнему обозначает последний, а revisions увеличивается до 1. Новая строка создается со всеми предыдущими значениями, за исключением того, что теперь эта строка обозначает version: 1.

Подводя итог:

При создании предмета:

  • Создать элемент с помощью revisions: 0 и version 0

При обновлении или перезаписи элемента:

  • Увеличение revisions
  • Вставьте старую строку точно так же, как и раньше, но замените version: 0 на новую версию, которую можно легко рассчитать как version: revisions + 1.

Вот как будет выглядеть преобразование в преобразование для таблицы с только первичным ключом:

Первичный ключ: id

  id  color
9501  violet
9502  cyan
9503  magenta

Первичный ключ: идентификатор + версия

id    version  revisions  color
9501        0          6  violet
9501        1          0  red
9501        2          1  orange
9501        3          2  yellow
9501        4          3  green
9501        5          4  blue
9501        6          5  indigo

Вот преобразование таблицы, которая уже использует ключ сортировки:

Первичный ключ: идентификатор + дата

id    date     color
9501  2018-01  violet
9501  2018-02  cyan
9501  2018-03  black

Первичный ключ: id + date_ver

id    date_ver     revisions  color
9501  2018-01__v0          6  violet
9501  2018-01__v1          0  red
9501  2018-01__v2          1  orange
9501  2018-01__v3          2  yellow
9501  2018-01__v4          3  green
9501  2018-01__v5          4  blue
9501  2018-01__v6          5  indigo

Альтернатива № 2:

id    date_ver     revisions  color
9501  2018-01              6  violet
9501  2018-01__v1          0  red
9501  2018-01__v2          1  orange
9501  2018-01__v3          2  yellow
9501  2018-01__v4          3  green
9501  2018-01__v5          4  blue
9501  2018-01__v6          5  indigo

На самом деле у нас есть возможность либо поместить предыдущие версии в одну таблицу, либо разделить их в своей таблице. Оба варианта имеют свои преимущества и недостатки.

Используя ту же таблицу:

  • Первичный ключ состоит из ключа раздела и ключа сортировки
  • Версия должна использоваться в ключе сортировки либо отдельно как number, либо добавляться к существующему ключу сортировки как string

Преимущества:

  • Все данные существуют в одной таблице

Недостатки:

  • Возможно ограничивает использование ключей сортировки таблиц
  • Управление версиями использует те же единицы записи, что и ваша основная таблица
  • Ключи сортировки можно настроить только во время создания таблицы
  • Возможно, нужно перенастроить код для запроса v0
  • На предыдущие версии также влияют индексы

Использование дополнительных таблиц:

  • Добавьте ключ revision в обе таблицы
  • Если ключ сортировки не используется, создайте ключ сортировки для вторичной таблицы с именем version. Первичная таблица всегда будет иметь version: 0. Использование этого ключа в первичной таблице не обязательно.
  • Если вы уже используете ключ сортировки, см. "Альтернатива № 2" выше

Преимущества:

  • Первичная таблица не нуждается в изменении каких-либо ключей или воссоздании. get запросы не меняются.
  • Основная таблица хранит свой ключ сортировки
  • Вторичная таблица может иметь независимые единицы измерения для чтения и записи
  • Вторичная таблица имеет свои индексы

Недостатки:

  • Требуется управление второй таблицей

Независимо от того, как вы решили разделить данные, теперь мы должны решить, как создавать строки ревизий. Вот несколько разных методов:

Синхронная перезапись/обновление элемента по требованию и вставка редакции по требованию

Сводка: Получить текущую версию строки. Выполните обновление текущей строки и вставьте предыдущую версию с одной транзакцией.

Чтобы избежать условий гонки, нам нужно написать и обновление, и вставить в одну и ту же операцию, используя TransactWriteItems. Кроме того, мы должны убедиться, что версия, которую мы обновляем, является верной версией к тому времени, когда запрос достигает сервера базы данных. Мы достигаем этого либо одной из двух проверок, либо даже обеими:

  1. В команде Update в TransactItems ConditionExpression должен проверить, что revision в обновляемой строке соответствует revision в объекте, над которым мы выполняли Get ранее.
  2. В команде Put в TransactItems ConditionExpression проверяет, что строка еще не существует.

Стоимость

  • 1 Емкость считывания на 4K для Get on v0
  • 1 Емкость записи для подготовки TransactWriteItem
  • 1 Емкость записи на 1K для Put/Update на v0
  • 1 Емкость записи на 1КБ для Версии ревизии
  • 1 Емкость записи для фиксации TransactWriteItem

Примечания:

  • Предметы ограничены 400КБ

По запросу, асинхронное получение элементов, перезапись/обновление элементов и вставка версий

Сводка: Получить и сохранить текущую строку. При перезаписи или обновлении строки проверьте текущую ревизию и приращение revisions. Вставьте ранее сохраненную строку с номером версии.

Выполните update с помощью

{
  UpdateExpression: 'SET revisions = :newRevisionCount',
  ExpressionAttributeValues: {
    ':newRevisionCount': previousRow.revisions + 1,
    ':expectedRevisionCount': previousRow.revisions,
  },
  ConditionExpression: 'revisions = :expectedRevisionCount',
}

Мы можем использовать тот же ConditionExpression с put при перезаписи ранее существующей строки.

В ответе мы наблюдаем за ConditionalCheckFailedException. Если это возвращается, это означает, что ревизия уже была изменена другим процессом, и мы должны повторить процесс с самого начала или прервать его полностью. Если исключений нет, то мы можем вставить предыдущую сохраненную строку после обновления значения атрибута вашей версии соответствующим образом (числовое или строковое).

Стоимость

  • 1 единица емкости чтения на 4K для Get on v0
  • 1 единица емкости записи на 1 КБ для Put/UpdateItem в v0
  • 1 единица емкости записи на 1 КБ для версии Put

По требованию, асинхронное скрытое обновление элемента и изменение-вставка

Сводка: Выполните "слепое" обновление строки v0, увеличивая при этом revisions и запрашивая старые атрибуты. Используйте возвращаемое значение, чтобы создать новую строку с номером версии.

Выполните update-item с помощью

{
  UpdateExpression: 'ADD revisions :revisionIncrement',
  ExpressionAttributeValues: {
    ':revisionIncrement': 1,
  },
  ReturnValues: 'ALL_OLD',
}

Действие ADD автоматически создаст revisions, если его не существует, и рассмотрит его 0. Еще одно приятное преимущество ReturnValues:

Никаких дополнительных затрат, связанных с запросом возвращаемого значения, за исключением небольшой сети, и накладных расходов на обработку получения более крупного ответа. Единицы чтения не используются.

В ответе на обновление значением Attributes будут данные из старой записи. Версия этой записи - значение Attributes.revisions + 1. При необходимости измените значение атрибута версии (числовое или строковое).

Теперь вы можете вставить эту запись в вашу целевую таблицу.

Стоимость

  • 1 единица емкости записи на 1 КБ для обновления v0
  • 1 единица емкости записи на 1 КБ для версии Put

Примечания:

  • Длина возвращаемого объекта Attributes ограничена 65535.
  • Нет решения для перезаписи строк.

Автоматическая асинхронная ревизия-вставка

Сводка: Выполните "слепые" обновления и вставки на первичном при увеличении revisions. Используйте лямбда-триггер, отслеживающий изменения в revision, для асинхронной вставки ревизий.

Выполните update с помощью

{
  UpdateExpression: 'ADD revisions :revisionIncrement',
  ExpressionAttributeValues: {
    ':revisionIncrement': 1,
  },
}

Действие ADD автоматически создаст revisions, если его не существует, и рассмотрит его 0.

Для перезаписи записей со значением put с шагом revisions на основе предыдущего запроса get.

Сконфигурируйте тип представления DynamoDB Stream для возврата как новых, так и старых изображений. Настройте лямбда-триггер для таблицы базы данных. Вот пример кода для NodeJS, который сравнил бы старые и новые изображения и вызвал функцию для записи ревизий в пакетном режиме.

/**
 * @param {AWSLambda.DynamoDBStreamEvent} event
 * @return {void}
 */
export function handler(event) {
  const oldRevisions = event.Records
    .filter(record => record.dynamodb.OldImage
      && record.dynamodb.NewImage
      && record.dynamodb.OldImage.revision.N !== record.dynamodb.NewImage.revision.N)
    .map(record => record.dynamodb.OldImage);
  batchWriteRevisions(oldRevisions);
}

Это всего лишь пример, но рабочий код, скорее всего, будет включать больше проверок.

Стоимость

  • 1 единица емкости чтения на 4K для доступа к v0 (только при перезаписи)
  • 1 единица емкости записи на 1 КБ для Put/Update v0
  • 1 блок запроса чтения DynamoDB Stream на команду GetRecords
  • 1 единица емкости записи на 1 КБ для пут редакции

Примечания:

  • Срок действия данных сегмента DynamoDB Stream истекает через 24 часа
  • Блоки запросов чтения DynamoDB Stream не зависят от единиц емкости чтения таблиц
  • Использование лямбда-функций имеет свою цену
  • Изменение типа представления потока требует отключения и повторного включения потока
  • Работает с командами Write, Put, BatchWriteItems, TransactWriteItems

Для моих случаев использования я уже использую DynamoDB Streams и не ожидаю, что пользователи будут так часто запрашивать версионные строки. Я также могу позволить пользователям немного подождать, пока будут готовы ревизии, поскольку они асинхронные. Это делает использование второй таблицы и автоматизированного лямбда-процесса более идеальным решением для меня.

Для асинхронных опций есть несколько точек сбоя. Тем не менее, это то, что вы можете либо сразу повторить на запросах по требованию, либо запланировать на будущее решение DynamoDB Stream.

Если у кого-то есть какие-либо другие решения или критические замечания, пожалуйста, прокомментируйте. Спасибо!

Ответ 3

Вы также можете достичь этого, поддерживая две отдельные таблицы. Один для самых последних предметов, а другой для их версий. Я написал пост в блоге с подробным объяснением https://www.efekarakus.com/2018/05/25/client-side-row-versioning-in-dynamo-db.html

Таблица ресурсов, где хэш является первичным ключом.

      +----------+---------+-------------------+
      |   hash   | version |   attr1..attrN    |
      +----------+---------+-------------------+
      | 1c5815b2 |    2    |  some values      |
      +----------+---------+-------------------+

Таблица истории ресурсов, где хэш - это ключ раздела, а версия - ключ сортировки.

      +----------+---------+-------------------+
      |   hash   | version |   attr1..attrN    |
      +----------+---------+-------------------+
      | 1c5815b2 |    2    |  some values      |
      +----------+---------+-------------------+
      | 1c5815b2 |    1    |  some old values  |
      +----------+---------+-------------------+

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

Когда вы создаете или обновляете ресурс, сначала пишите в таблицу истории ресурсов, а затем в таблицу ресурсов.

Я обнаружил, что это немного чище, потому что вы не столкнетесь с возможными сценариями потери данных, как при работе с неизменяемыми данными в одной таблице.

Ответ 4

Amazon выдвинула рекомендацию о том, как осуществлять контроль версий в DynamoDB: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-sort-keys.html#bp-sort-keys-version-control

Используя ключ сортировки в качестве версии, вы можете убедиться, что последний всегда всегда первый (например, "v0_"), а остальные ключи располагаются последовательно после этого. Они также предлагают клонировать v0_latest в "v00x_", чтобы он мог быть последним ключом для поисков, которые хотят упорядочить историю версий.

Смотрите эту ссылку для получения полной информации.