Предотвратите дубликаты заказов условными записями

AWSBeginner
Практиковаться сейчас

Введение

Повторная отправка заказа не должна перезаписывать существующую сумму. Устаревшее обновление оплаты также не должно перезаписывать более новую версию. Вы выполните фактические условные запросы API, увидите отклонённые дубликаты и устаревшие версии и докажете успешное создание допустимых новых заказов.

Сначала выполните работу «Создайте таблицу DynamoDB для заказов», включая типизированные элементы и выражения обновления. Этот новый раздел не использует прежние таблицы или учётные данные. Используйте /home/labex/project, официальный AWS CLI и AWS View.

Связь с сертификацией

Сертификация Задача экзамена Практика
Developer – Associate (DVA-C02) Задача 1.3, Задача 1.1 Условные записи DynamoDB и оптимистическое управление конкурентным доступом.

Обзор работы

Концептуальная схема с иконками AWS4, экспортированная в draw.io: условные записи атомарно проверяют состояние заказа, допускают корректное создание и обновление и отклоняют повторные или устаревшие запросы без перезаписи принятых данных.

Защитите первое создание заказа

На этом шаге вы создадите заказ, только когда его первичный ключ не используется. Безусловный PutItem заменяет весь элемент по этому ключу. Условие проверяет текущее состояние атомарно вместе с записью, поэтому отдельная проверка чтением перед записью не нужна и допускала бы гонки.

Используйте подготовленное временное подключение и создайте независимую таблицу с идентификатором заказа как строковым ключом раздела.

cd /home/labex/project
aws sts get-caller-identity
aws dynamodb create-table \
  --table-name labex-d04-orders \
  --attribute-definitions AttributeName=order_id,AttributeType=S \
  --key-schema AttributeName=order_id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST
aws dynamodb wait table-exists --table-name labex-d04-orders

Запишите обычный файл запроса API. Heredoc с кавычками записывает JSON буквально; file:// позволяет CLI загрузить его без экранирования большого содержимого в оболочке. attribute_not_exists(order_id) разрешает запись, только если ни один элемент не имеет этого ключа. Поле version: 1 — значение приложения, которое вы используете для защиты обновлений на шаге 3; DynamoDB не увеличивает его автоматически.

cat > create-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Item": {
    "order_id": {
      "S": "O100"
    },
    "amount": {
      "N": "145"
    },
    "status": {
      "S": "NEW"
    },
    "version": {
      "N": "1"
    }
  },
  "ConditionExpression": "attribute_not_exists(order_id)"
}
JSON
aws dynamodb put-item --cli-input-json file://create-order.json
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O100"}}' \
  --consistent-read

O100 содержит сумму 145, NEW и версию 1. Условие отсутствия выполняется для этого нового ключа. Посмотрите на AWS View вверху и раскройте O100, чтобы изучить фактический элемент.

Отклоните дубликат, не блокируя новые заказы

На этом шаге вы повторите идентификатор заказа с другой суммой. Небезопасная перезапись изменила бы O100 на 999; условие должно отклонить её и сохранить 145. Отклонённый дубликат — ответ с ошибкой, а не автоматически успешный идемпотентный ответ приложения. Приложение должно решить, как обработать эту ошибку.

Повторный запрос создания не должен заменять уже принятую сумму заказа. Условие отсутствия защищает это решение в момент записи. Фильтр чтения меняет возвращаемые элементы, а условие записи определяет, допустимо ли изменение вообще; каждому конкурирующему запросу нужно своё условие.

Запишите конкурирующий запрос с тем же ключом и условием отсутствия.

cat > duplicate-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Item": {
    "order_id": {
      "S": "O100"
    },
    "amount": {
      "N": "999"
    },
    "status": {
      "S": "NEW"
    },
    "version": {
      "N": "1"
    }
  },
  "ConditionExpression": "attribute_not_exists(order_id)"
}
JSON

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

aws dynamodb put-item --cli-input-json file://duplicate-order.json || true
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O100"}}' \
  --consistent-read

Ожидайте ConditionalCheckFailedException, затем неизменённую сумму 145 и версию 1. Докажите, что та же защита допускает другой идентификатор заказа, а не запрещает каждую запись.

cat > second-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Item": {
    "order_id": {
      "S": "O101"
    },
    "amount": {
      "N": "200"
    },
    "status": {
      "S": "NEW"
    },
    "version": {
      "N": "1"
    }
  },
  "ConditionExpression": "attribute_not_exists(order_id)"
}
JSON
aws dynamodb put-item --cli-input-json file://second-order.json
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O101"}}' \
  --consistent-read

O101 создан с суммой 200, NEW и версией 1. AWS View показывает оба заказа, а сумма O100 остаётся 145. Эти операции предотвращают непреднамеренную перезапись этой записи по ключу; они не реализуют транзакции нескольких элементов или устранение повторных платежей.

Защитите обновление ожидаемой версией

На этом шаге вы примените переход состояния только к прочитанной версии. Оптимистическое условие версии отклоняет устаревший запрос вместо молчаливой перезаписи более нового решения. Запись атомарно проверяет версию 1, устанавливает PAID и увеличивает версию до 2.

Первое обновление соответствует версии 1; повторение этого ожидания не проходит, когда элемент уже имеет версию 2.

Запишите и выполните этот запрос UpdateItem. Псевдонимы имён выражения безопасно обрабатывают имена атрибутов, а типизированные значения задают ожидаемую и следующую версии.

cat > pay-order.json <<'JSON'
{
  "TableName": "labex-d04-orders",
  "Key": {
    "order_id": {
      "S": "O100"
    }
  },
  "UpdateExpression": "SET #state = :paid, #version = :next",
  "ConditionExpression": "#version = :expected",
  "ExpressionAttributeNames": {
    "#state": "status",
    "#version": "version"
  },
  "ExpressionAttributeValues": {
    ":paid": {
      "S": "PAID"
    },
    ":next": {
      "N": "2"
    },
    ":expected": {
      "N": "1"
    }
  },
  "ReturnValues": "ALL_NEW"
}
JSON
aws dynamodb update-item --cli-input-json file://pay-order.json

ALL_NEW показывает PAID и версию 2 с суммой 145. Повторите неизменённый запрос, всё ещё ожидающий версию 1.

aws dynamodb update-item --cli-input-json file://pay-order.json || true
aws dynamodb get-item \
  --table-name labex-d04-orders \
  --key '{"order_id":{"S":"O100"}}' \
  --consistent-read

Ожидайте ConditionalCheckFailedException. Итоговое чтение сохраняет PAID, версию 2 и сумму 145; O101 остаётся NEW/версия 1. Вызывающая сторона в рабочей среде перечитала бы текущее состояние и решила, уместно ли другое обновление, вместо бесконечного повторения устаревшего ожидания.

Фактический элемент сохраняет сумму после защищённого создания и записывает переход оплаты с версией.

Удалите только собственную таблицу заказов

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

aws dynamodb scan --table-name labex-d04-orders --consistent-read

O100 имеет PAID/версию 2/сумму 145; O101 — NEW/версию 1/сумму 200. Удалите только эту таблицу и дождитесь отсутствия.

aws dynamodb delete-table --table-name labex-d04-orders
aws dynamodb wait table-not-exists --table-name labex-d04-orders
aws dynamodb list-tables
aws dynamodb get-item \
  --table-name labex-d04-reference \
  --key '{"id":{"S":"platform"}}' \
  --consistent-read

Остаётся только эталонная таблица с заметкой keep unchanged. Эти успешные запросы отличают удаление от неудачного подключения. AWS View убирает таблицу заказов. Временное подключение заканчивается с этой VM.

Резюме

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

Подробнее см. в документации AWS: Условные выражения для защищённых записей.