Amazon DynamoDB - один из самых универсальных и популярных сервисов на AWS. За секунды мы можем развернуть высокодоступное, динамически масштабируемое хранилище ключевых документов с глобальной репликацией, транзакциями и многим другим! Однако, если мы изменяем атрибут списка в документе, нам нужно предпринять дополнительные шаги для достижения корректности и параллелизма. Ниже я опишу проблему и предложу несколько решений.

Полный код, если хотите следовать.

Давайте проясним проблему. Предположим, мы вставляем следующий документ, используя JavaScript AWS-SDK и DynamoDB DocumentClient:

Вот как выглядит документ в консоли DynamoDB:

По умолчанию DocumentClient упорядочивает массив JavaScript как тип списка DynamoDB. Как бы мы удалили значение frylock из атрибута список друзей? Вот документ об операции удаления списка. Поскольку нам нужно указать индекс удаляемого элемента, нам нужно прочитать документ и найти индекс:

Но эта реализация имеет состояние гонки; существует небольшой промежуток времени между чтением документа, поиском индекса и отправкой запроса на обновление, в течение которого документ может быть обновлен другим источником, что приводит к тому, что операция удалить элемент по индексу X дает нежелательный результат. Эта проблема также называется транзакционной памятью. К счастью, есть несколько решений.

Выражение условия в содержимом списка

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

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

Этот метод только гарантирует, что обновления атрибута list безопасны. Как мы можем гарантировать, что будем применять обновления только тогда, когда документ не изменился?

Выражение условия для атрибута версии

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

Давайте обновим выражение условия и добавим обработку ошибок:

Обратите внимание, что выражение обновления также увеличивает атрибут версии. У этого подхода есть два недостатка:

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

Используйте тип данных Set

На практике список друзей будет хранить список уникальных внешних ключей. Если мы знаем, что записи уникальны, мы можем маршалировать поле друзей как тип данных DynamoDB Set вместо списка. По сравнению со списками, наборы имеют несколько отличий:

  1. Все значения должны быть одного типа (строка, логическое значение, число).
  2. Все значения должны быть уникальными
  3. Чтобы удалить элемент (ы) из набора, используйте операцию DELETE, указав набор значений
  4. Набор не может быть пустым

Идеально подходит для хранения списка связанных ключей документов. Однако мы видели, что DocumentClient сериализует массивы JavaScript в виде списков, поэтому нам нужно переопределить это поведение с помощью настраиваемого маршаллера.

Примечание: в примере в документации используется класс DynamoDBSet, но, похоже, он недоступен для импорта из модуля aws-sdk JS npm. Вместо этого мы воспользуемся функцией DynamoDB.createSet, которая выполняет то же самое:

В консоли наш новый документ выглядит почти идентично, за исключением типа StringSet в атрибуте friends.

Теперь, чтобы указать операцию DELETE:

Работа с наборами из JavaScript имеет два подводных камня. Во-первых: атрибут set в документе не десериализуется в массив JavaScript. Посмотрим, что он на самом деле возвращает:

Ага! Набор DynamoDB десериализуется в объект, массив элементов которого хранится в свойстве values. Если мы хотим, чтобы Set десериализовался в массив, нам нужно добавить шаг демаршалинга, на котором мы присваиваем свойство values ​​вместо самого десериализованного объекта набора.

Второе: помните, как наборы не могут быть пустыми? Если мы попытаемся удалить все элементы из набора, консоль нас остановит:

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

Вы все равно получите сообщение об ошибке, если попытаетесь сохранить пустой массив как набор, поэтому вот вспомогательная функция, идущая в обратном направлении:

Глобальная блокировка записи

Давайте не будем забывать освященную веками традицию предотвращать проблемы, а не решать их. Транзакционная память представляет собой проблему только тогда, когда мы разрешаем одновременную запись. Мы можем избежать одновременной записи, потребовав от любого писателя получить распределенную блокировку записи (используя службу распределенной блокировки, такую ​​как etcd или zookeeper).

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

У этого метода есть два существенных недостатка: 1) служба распределенной блокировки добавляет дополнительную сложность и задержку. 2) Глобальная блокировка записи уменьшает объем записи. Если вы уже используете службу распределенной блокировки и вам не нужна высокая пропускная способность записи, стоит подумать об этом решении.

А как насчет транзакций?

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

Предметы не блокируются во время транзакции. Транзакции DynamoDB обеспечивают сериализуемую изоляцию. Если элемент изменяется вне транзакции во время выполнения транзакции, транзакция отменяется и создается исключение с подробностями о том, какой элемент или элементы вызвали исключение.

В этом случае транзакции будут действовать как более медленная версия выражений условий.

Все, ребята

Вот суть всего кода, который мы написали до сих пор. Есть ли у вас лучший способ обновления безопасных списков? Поделитесь ими в комментариях. Надеюсь, это поможет вам максимально использовать DynamoDB и удачного взлома!