В настоящее время я вижу, что когда я использую управление версиями в DynamoDB, он меняет номер версии, но новая запись заменяет старую запись; то есть:
Ответ 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
. Кроме того, мы должны убедиться, что версия, которую мы обновляем, является верной версией к тому времени, когда запрос достигает сервера базы данных. Мы достигаем этого либо одной из двух проверок, либо даже обеими:
- В команде
Update
в TransactItems
ConditionExpression
должен проверить, что revision
в обновляемой строке соответствует revision
в объекте, над которым мы выполняли Get
ранее.
- В команде
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.
Если у кого-то есть какие-либо другие решения или критические замечания, пожалуйста, прокомментируйте. Спасибо!