Amazon DynamoDB — NoSQL Database
Amazon DynamoDB — NoSQL Database
Реляційна парадигма та її межі: навіщо існує DynamoDB
Протягом більш ніж чотирьох десятиліть реляційна модель баз даних домінувала у сфері зберігання структурованих даних. PostgreSQL, MySQL, SQL Server — ці системи побудовані на фундаменті, що залишається незмінним з часів Едгара Кодда: дані організовані у таблиці зі строго визначеною схемою, зв'язки між сутностями встановлюються через зовнішні ключі, а мова запитів SQL надає декларативний інтерфейс для маніпуляції даними. Для абсолютної більшості корпоративних застосунків ця модель є не просто достатньою — вона є оптимальною.
Однак у 2000-х роках провідні інтернет-компанії зіткнулися зі сценарієм, де реляційна модель показала системні обмеження. Йдеться не про невелику деградацію продуктивності — йдеться про принципову неможливість масштабування до потрібних обсягів при збереженні прийнятних затримок. Проблема полягає у фундаментальній властивості реляційних систем: для гарантування ACID-властивостей (передусім Consistency та Isolation) вони використовують механізми блокувань, які за своєю природою є централізованими. Єдиний вузол або невеликий кластер може забезпечити сильну узгодженість, але горизонтальне масштабування на тисячі вузлів цю гарантію руйнує.
Команда Amazon у 2004–2007 роках дослідила власні внутрішні сервіси та виявила закономірність: 70% операцій на їхніх production-системах мали форму "прочитати або записати одну сутність за відомим ідентифікатором", і лише 20% дійсно потребували можливостей JOIN та складних SQL-запитів. Це спостереження лягло в основу фундаментальної роботи Dynamo: Amazon's Highly Available Key-Value Store (2007, Werner Vogels та ін.), яка описала архітектуру системи, що жертвує частиною семантики реляційної моделі заради лінійного горизонтального масштабування та гарантованих затримок.
Amazon DynamoDB — це хмарний керований NoSQL сервіс AWS, що є еволюцією оригінальної Dynamo-системи, публічно запущений у 2012 році. DynamoDB надає три гарантії, які в поєднанні недосяжні для будь-якої реляційної СУБД при масштабуванні:
- Продуктивність з однозначними мілісекундами — читання та запис будь-якого розміру таблиці виконуються з затримкою 1–10 ms (при правильному проектуванні)
- Необмежений горизонтальний масштаб — одна таблиця DynamoDB здатна зберігати петабайти даних і обробляти мільйони запитів на секунду
- Повністю керований сервіс — відсутність необхідності управляти серверами, patching, бекапами, реплікацією
Модель даних DynamoDB: таблиці, елементи та атрибути
Таблиця (Table)
Таблиця (Table) є найвищим рівнем організації даних у DynamoDB. На перший погляд, таблиця DynamoDB схожа на таблицю реляційної бази даних — вона має назву та містить записи. Проте між ними існує принципова різниця у семантиці.
У реляційній моделі таблиця визначає схему: структуру кожного рядка з іменами стовпців та їхніми типами задають при CREATE TABLE, і всі рядки повинні відповідати цій схемі. DynamoDB, навпаки, є schema-less на рівні атрибутів: єдине, що таблиця DynamoDB вимагає від кожного елемента — наявність атрибутів, що складають первинний ключ. Решта атрибутів кожного елемента може бути абсолютно довільною та різною від елемента до елемента.
Таблиця DynamoDB є незалежним ресурсом — вона не "належить" схемі чи базі даних. Якщо в PostgreSQL ви маєте database → schema → table, то у DynamoDB ієрархія скорочується до table. Це означає, що таблиці DynamoDB не можуть бути об'єднані через JOIN навіть синтаксично — такого механізму не існує.
Елемент (Item)
Елемент (Item) — це одиниця даних у таблиці DynamoDB, аналог рядка (row) у реляційній базі даних. Кожен елемент унікально ідентифікується своїм первинним ключем та є колекцією атрибутів. Максимальний розмір одного елемента — 400 KB (разом з іменами атрибутів та значеннями).
Відсутність фіксованої схеми надає DynamoDB надзвичайну гнучкість при еволюції моделі даних. Уявіть таблицю Products, де різні категорії товарів мають різний набір атрибутів:
// Елемент 1: Книга
{
"ProductId": "book-001",
"Category": "Book",
"Title": "Clean Code",
"Author": "Robert C. Martin",
"ISBN": "978-0132350884",
"Pages": 431,
"Price": 35.00
}
// Елемент 2: Електроніка
{
"ProductId": "elec-002",
"Category": "Electronics",
"Title": "Sony WH-1000XM5",
"Brand": "Sony",
"WarrantyYears": 2,
"BatteryLifeHours": 30,
"Price": 349.99
}
Обидва елементи належать одній таблиці, але мають абсолютно різні набори атрибутів. У PostgreSQL для цього знадобилося б або успадкування таблиць, або поле типу JSONB, або окремі таблиці для кожної категорії.
Атрибут (Attribute) та система типів
Атрибут (Attribute) — це пара "ім'я–значення", що належить конкретному елементу. DynamoDB підтримує систему типів даних, яка суттєво відрізняється від SQL-типів:
Скалярні типи (Scalar)
String (S) — рядок у кодуванні UTF-8. Порожній рядок ("") дозволений.
Number (N) — числове значення (ціле або дробове). Зберігається як рядок для збереження точності. Діапазон: до 38 значущих цифр.
Binary (B) — бінарні дані у форматі Base64. Використовується для зберігання зображень, стиснутих даних, криптографічних хешів.
Boolean (BOOL) — true або false.
Null (NULL) — відсутність значення. Атрибут типу Null займає місце, але не має значення.
Документні типи (Document)
Map (M) — довільна структура вигляду "ключ–значення", аналог JSON-об'єкта. Значення кожного ключа може бути будь-яким типом DynamoDB, включаючи вкладені Map.
List (L) — впорядкована колекція значень довільних типів. Аналог JSON-масиву. Елементи списку можуть бути різних типів.
Множинні типи (Set)
StringSet (SS) — невпорядкована колекція унікальних рядків.
NumberSet (NS) — невпорядкована колекція унікальних чисел.
BinarySet (BS) — невпорядкована колекція унікальних бінарних значень.
Важливо: Set не може бути порожнім. Всі елементи Set мають бути одного типу.
Приклад елемента з вкладеними типами:
{
"UserId": "usr-123",
"Name": "Олена Петренко",
"Email": "elena@example.com",
"Age": 28,
"IsActive": true,
"Tags": ["developer", "aws-certified", "ukraine"],
"Address": {
"Country": "Ukraine",
"City": "Kyiv",
"ZipCode": "01001"
},
"Skills": ["C#", ".NET", "DynamoDB", "PostgreSQL"],
"LastLoginAt": "2025-06-01T10:30:00Z"
}
У цьому прикладі:
UserId,Name,Email,LastLoginAt— тип String (S)Age— тип Number (N)IsActive— тип Boolean (BOOL)Tags— тип List (L) зі String-елементамиAddress— тип Map (M) з вкладеними String-атрибутамиSkills— тип StringSet (SS) (множина, не може містити дублікати)
Первинний ключ (Primary Key): фундамент DynamoDB
Первинний ключ є найважливішим концептуальним елементом DynamoDB. На відміну від реляційних баз даних, де первинний ключ є лише засобом унікальної ідентифікації рядка, у DynamoDB первинний ключ виконує дві нерозривно пов'язані функції одночасно: унікальну ідентифікацію елемента та визначення фізичного розташування даних на серверах. Саме тому вибір первинного ключа є фундаментальним архітектурним рішенням, від якого залежить продуктивність системи при будь-якому масштабі.
DynamoDB підтримує два типи первинного ключа.
Partition Key (Simple Primary Key)
Partition Key (також відомий як Hash Key або PK) — це простий первинний ключ, що складається з одного атрибута. DynamoDB використовує значення Partition Key як вхідні дані для детермінованої хеш-функції, результат якої визначає, на якому фізичному сервері (partition) зберігатиметься елемент.
Візуалізація Simple Primary Key
Приклад 1: Таблиця Users (Профілі користувачів)
У цій таблиці первинним ключем є UserId (тип String). Всі інші атрибути (Name, Age, Email) є довільними та необов'язковими для схеми:
| UserId (PK) тип: String (S) | Name тип: String (S) | Age тип: Number (N) | Email тип: String (S) |
|---|---|---|---|
usr-001 | Олена | 28 | elena@example.com |
usr-002 | Іван | 32 | ivan@example.com |
usr-003 | Марія | 22 | maria@example.com |
Приклад 2: Таблиця Devices (Метадані IoT-пристроїв)
У цьому сценарії первинним ключем є DeviceId (тип String), який ідентифікує конкретний фізичний пристрій. Кожен пристрій має свій набір налаштувань та метаданих:
| DeviceId (PK) тип: String (S) | DeviceType тип: String (S) | FirmwareVersion тип: String (S) | Status тип: String (S) | Metadata тип: Map (M) |
|---|---|---|---|---|
dev-sensor-091 | Temperature | v2.4.1 | ACTIVE | {"Location": "ServerRoom-A", "IntervalSec": 10} |
dev-camera-104 | IP-Camera | v1.0.8 | OFFLINE | {"Location": "MainEntrance", "Resolution": "4K"} |
dev-gate-002 | SmartGate | v3.1.0 | ACTIVE | {"Location": "GateEast"} |
Основні особливості роботи з простим ключем:
- Унікальність: Кожен запис (елемент) у таблиці повинен мати унікальне значення
UserId. Запис елемента з наявнимUserIdперезапише старий елемент. - Швидкий доступ: Доступ до даних здійснюється виключно за точним значенням Partition Key (наприклад,
GetItemзаUserId = "usr-001"). DynamoDB обчислює хеш від ключа та миттєво знаходить потрібну партицію. - Обмеження вибірок: Неможливо зробити запит (
Query) на кшталт "показати всіх користувачів, які старші за 25 років". Подібна операція без додаткових індексів вимагає сканування всієї таблиці (Scan), що є дорогою операцією та створює велике навантаження.
Нижче показано, як DynamoDB розподіляє ці дані на фізичному рівні:
Критична вимога до Partition Key: висока кардинальність. Якщо всі або більшість елементів мають однакове значення Partition Key, всі вони потраплять на одну фізичну партицію. Ця ситуація називається hot partition (гаряча партиція) і є найчастішою причиною деградації продуктивності у DynamoDB. Детально розглянемо це у розділі Best Practices.
Приклади хороших Partition Key:
UserId(UUID) — висока кардинальність, рівномірний розподілOrderId(UUID або timestamp-based) — унікальний для кожного записуDeviceId(для IoT) — кожен пристрій має унікальний ідентифікатор
Приклади поганих Partition Key:
Country— лише ~200 унікальних значень, більшість запитів сконцентровано на одній-двох країнахStatus("active", "inactive") — лише 2 значення, "active" буде hot partitionDate(тільки дата без часу) — всі записи за один день на одній партиції
Composite Primary Key: Partition Key + Sort Key
Composite Primary Key (або Range Key) складається з двох атрибутів: Partition Key та Sort Key. Ця комбінація надає DynamoDB принципово нові можливості для запитів. Partition Key визначає, на якій фізичній партиції зберігатиметься елемент, а Sort Key визначає порядок зберігання елементів всередині однієї партиції.
Ключова відмінність від простого первинного ключа: при Composite Primary Key кілька елементів можуть мати однакове значення Partition Key — це цілком допустимо і є основним патерном проектування. Унікальність елемента визначається парою (Partition Key, Sort Key).
Візуалізація Composite Primary Key
Приклад 1: Таблиця UserOrders (Замовлення користувачів)
Тут UserId є Partition Key, а OrderDate#OrderId — Sort Key. Обидва атрибути мають тип String. Унікальність запису гарантується виключно комбінацією обох значень. Зверніть увагу, що замовлення для usr-001 відсортовані хронологічно:
| UserId (PK) тип: String (S) | OrderDate#OrderId (SK) тип: String (S) | Amount тип: Number (N) | Status тип: String (S) |
|---|---|---|---|
usr-001 | 2025-01-15T10:00:00#ord-A | 150.00 | DELIVERED |
usr-001 | 2025-03-20T14:30:00#ord-B | 89.99 | SHIPPED |
usr-001 | 2025-06-01T09:15:00#ord-C | 320.50 | PENDING |
usr-002 | 2025-02-10T08:00:00#ord-D | 45.00 | DELIVERED |
usr-002 | 2025-05-05T16:45:00#ord-E | 210.00 | DELIVERED |
Приклад 2: Таблиця ChatMessages (Повідомлення в чатах)
Тут ConversationId виступає як Partition Key, групуючи всі повідомлення конкретної розмови на одній партиції. Timestamp#MessageId виступає як Sort Key, забезпечуючи хронологічний порядок та унікальність повідомлень (оскільки кілька повідомлень можуть бути відправлені в одну секунду):
| ConversationId (PK) тип: String (S) | Timestamp#MessageId (SK) тип: String (S) | SenderId тип: String (S) | MessageText тип: String (S) |
|---|---|---|---|
room-402 | 2025-06-03T10:00:15Z#msg-001 | usr-001 | Привіт всім! |
room-402 | 2025-06-03T10:00:45Z#msg-002 | usr-002 | Привіт, Олено! Як справи? |
room-402 | 2025-06-03T10:01:10Z#msg-003 | usr-001 | Все чудово, працюю над DynamoDB. |
room-511 | 2025-06-03T11:30:00Z#msg-004 | usr-003 | Коли почнемо зустріч? |
Приклад 3: Таблиця ProjectTasks (Задачі проектів з ієрархічним SK)
У великих системах Composite PK використовується для організації зв'язків типу "один-до-багатьох" та ієрархій. Розглянемо таблицю проектних задач, де Partition Key — це ProjectId, а Sort Key — це Priority#TaskId. Такий SK дозволяє не просто ідентифікувати задачу, але й сортувати задачі за пріоритетом у межах проекту:
| ProjectId (PK) тип: String (S) | Priority#TaskId (SK) тип: String (S) | TaskTitle тип: String (S) | AssigneeId тип: String (S) | DueDate тип: String (S) |
|---|---|---|---|---|
proj-alpha | 1-CRITICAL#task-102 | Налаштувати AWS Credentials | usr-001 | 2025-06-05 |
proj-alpha | 2-HIGH#task-105 | Створити схему DynamoDB | usr-001 | 2025-06-08 |
proj-alpha | 3-NORMAL#task-101 | Написати README.md | usr-002 | 2025-06-12 |
proj-beta | 1-CRITICAL#task-201 | Виправити баг з авторизацією | usr-003 | 2025-06-04 |
Основні особливості роботи з композитним ключем:
- Групування та сортування: Записи з однаковим Partition Key (наприклад,
usr-001) зберігаються на одній фізичній партиції та відсортовані за значенням Sort Key. - Ефективний діапазонний пошук: Завдяки сортуванню ви можете використовувати операцію
Queryдля вибірки всіх замовлень користувачаusr-001за певний період (наприклад, за допомогою оператораBETWEENдля SK). - Складений Sort Key: Використання патерну
Date#IdабоCategory#Statusу Sort Key дозволяє будувати гнучкі та складні умови для пошуку в межах однієї партиції.
Схема нижче демонструє, як це виглядає на фізичному та логічному рівнях:
Можливості запитів із Sort Key. Наявність Sort Key відкриває можливість ефективних Query-операцій із умовою на значення Sort Key. DynamoDB підтримує такі оператори для Sort Key у межах однієї партиції:
| Оператор | Значення | Приклад |
|---|---|---|
= | Точне значення | OrderDate = "2025-06-01T10:00:00" |
<, <= | Менше або рівне | Score <= 100 |
>, >= | Більше або рівне | CreatedAt >= "2025-01-01" |
BETWEEN | Включний діапазон | Score BETWEEN 50 AND 100 |
begins_with | Починається з префіксу | OrderId begins_with "2025-06" |
begins_with та всі порівняльні оператори для Sort Key працюють тільки всередині однієї партиції. Неможливо знайти всі елементи з Sort Key, що "починається з X" по всій таблиці без сканування. Для таких запитів необхідні Global Secondary Index (розглянемо у наступному розділі).Паттерни проектування з Composite Primary Key. Розуміння цього патерну є ключовим для ефективного використання DynamoDB. Partition Key групує пов'язані елементи в одну партицію, Sort Key впорядковує їх для ефективного пошуку:
Порівняльна таблиця первинних ключів
Для швидкого вибору типу первинного ключа скористайтеся порівняльною таблицею:
| Характеристика | Simple Primary Key (Partition Key) | Composite Primary Key (PK + Sort Key) |
|---|---|---|
| Склад ключа | Лише один атрибут: Partition Key (PK) | Два атрибути: Partition Key (PK) + Sort Key (SK) |
| Унікальність | Гарантується на рівні Partition Key | Гарантується комбінацією (Partition Key, Sort Key) |
| Зберігання даних | Кожен запис може мати свій унікальний PK | Кілька записів можуть мати однаковий PK (групуються разом) |
| Сортування | Дані не впорядковані | Дані відсортовані за значенням SK в межах одного PK |
| Доступні запити | Лише за точним значенням PK (GetItem) | За точним PK + фільтрація по SK (Query, BETWEEN, begins_with) |
| Типові сценарії | Таблиці сесій, кеш, конфігурації користувача | Історія замовлень, чат-повідомлення, логування IoT-пристроїв |
Операції читання і запису: базовий API
Розуміння базових операцій DynamoDB є необхідною передумовою для обговорення продуктивності та проектування схем. DynamoDB надає чіткий поділ між операціями для одного елемента та операціями для множини елементів.
Операції з одним елементом
GetItem — найшвидша операція в DynamoDB. Отримує рівно один елемент за його повним первинним ключем (Partition Key + Sort Key, якщо є). GetItem завжди звертається до конкретної партиції — жодного сканування, жодних індексів, гарантований час O(1).
# Отримання сесії за композитним ключем
aws dynamodb get-item \
--table-name UserSessions \
--key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
--region eu-central-1
var client = new AmazonDynamoDBClient();
var response = await client.GetItemAsync(new GetItemRequest
{
TableName = "UserSessions",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } }
}
});
# Отримання сесії за композитним ключем
$key = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
}
Get-DDBItem -TableName UserSessions -Key $key -Region eu-central-1
PutItem — записує або повністю замінює елемент за вказаним первинним ключем. Якщо елемент із таким ключем вже існує — він буде повністю замінений. Підтримує умовні записи (Condition Expression).
# Запис нової сесії (якщо вже існує — буде повністю перезаписано)
aws dynamodb put-item \
--table-name UserSessions \
--item '{
"UserId": {"S": "usr-001"},
"SessionId": {"S": "sess-a1b2c3d4"},
"CreatedAt": {"S": "2025-06-01T10:00:00Z"},
"IsActive": {"BOOL": true}
}' \
--region eu-central-1
var response = await client.PutItemAsync(new PutItemRequest
{
TableName = "UserSessions",
Item = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } },
{ "CreatedAt", new AttributeValue { S = "2025-06-01T10:00:00Z" } },
{ "IsActive", new AttributeValue { BOOL = true } }
}
});
# Запис нової сесії (або повний перезапис наявної)
$item = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
CreatedAt = New-DDBEntry -S '2025-06-01T10:00:00Z'
IsActive = New-DDBEntry -BOOL $true
}
Set-DDBItem -TableName UserSessions -Item $item -Region eu-central-1
UpdateItem — оновлює конкретні атрибути існуючого елемента, не торкаючись інших. Є атомарним: або всі зміни застосовуються, або жодна. Підтримує атомарні лічильники (Atomic Counter).
# Атомарне оновлення статусу та часу активності сесії
aws dynamodb update-item \
--table-name UserSessions \
--key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
--update-expression "SET IsActive = :false, LastActivity = :now" \
--expression-attribute-values '{
":false": {"BOOL": false},
":now": {"S": "2025-06-01T12:00:00Z"}
}' \
--region eu-central-1
var response = await client.UpdateItemAsync(new UpdateItemRequest
{
TableName = "UserSessions",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } }
},
UpdateExpression = "SET IsActive = :false, LastActivity = :now",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":false", new AttributeValue { BOOL = false } },
{ ":now", new AttributeValue { S = "2025-06-01T12:00:00Z" } }
}
});
# Атомарне оновлення статусу та часу активності
$updateRequest = [Amazon.DynamoDBv2.Model.UpdateItemRequest]@{
TableName = 'UserSessions'
Key = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
}
UpdateExpression = 'SET IsActive = :false, LastActivity = :now'
ExpressionAttributeValues = @{
':false' = New-DDBEntry -BOOL $false
':now' = New-DDBEntry -S '2025-06-01T12:00:00Z'
}
}
Update-DDBItem -UpdateItemRequest $updateRequest -Region eu-central-1
DeleteItem — видаляє елемент за первинним ключем. Підтримує умовне видалення.
# Видалення сесії за її первинним ключем
aws dynamodb delete-item \
--table-name UserSessions \
--key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
--region eu-central-1
var response = await client.DeleteItemAsync(new DeleteItemRequest
{
TableName = "UserSessions",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } }
}
});
# Видалення сесії за первинним ключем
$key = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
}
Remove-DDBItem -TableName UserSessions -Key $key -Region eu-central-1
Операції з множиною елементів
Query — отримує один або кілька елементів, що відповідають заданому Partition Key та (необов'язково) умові на Sort Key. Query виконується виключно всередині однієї партиції — це забезпечує ефективність. Query — це основна операція запиту у правильно спроектованій таблиці DynamoDB.
# Пошук сесій користувача, які починаються з певного префіксу
aws dynamodb query \
--table-name UserSessions \
--key-condition-expression "UserId = :uid AND SessionId begins_with(:sessPrefix)" \
--expression-attribute-values '{
":uid": {"S": "usr-001"},
":sessPrefix": {"S": "sess-a"}
}' \
--region eu-central-1
var response = await client.QueryAsync(new QueryRequest
{
TableName = "UserSessions",
KeyConditionExpression = "UserId = :uid AND SessionId begins_with(:sessPrefix)",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":uid", new AttributeValue { S = "usr-001" } },
{ ":sessPrefix", new AttributeValue { S = "sess-a" } }
}
});
# Пошук сесій за Partition Key та префіксом Sort Key
$queryRequest = [Amazon.DynamoDBv2.Model.QueryRequest]@{
TableName = 'UserSessions'
KeyConditionExpression = 'UserId = :uid AND SessionId begins_with(:sessPrefix)'
ExpressionAttributeValues = @{
':uid' = New-DDBEntry -S 'usr-001'
':sessPrefix' = New-DDBEntry -S 'sess-a'
}
}
Invoke-DDBQuery -QueryRequest $queryRequest -Region eu-central-1
Scan — проходить по всіх елементах таблиці або індексу. Scan є дорогою операцією: він читає кожен елемент, споживає одиниці пропускної здатності для кожного прочитаного елемента, навіть якщо результат фільтрується. Scan використовується виключно для адміністративних задач або однократного аналізу даних.
# Сканування всієї таблиці для пошуку активних сесій (неефективно!)
aws dynamodb scan \
--table-name UserSessions \
--filter-expression "IsActive = :true" \
--expression-attribute-values '{":true": {"BOOL": true}}' \
--region eu-central-1
var response = await client.ScanAsync(new ScanRequest
{
TableName = "UserSessions",
FilterExpression = "IsActive = :true",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":true", new AttributeValue { BOOL = true } }
}
});
# Сканування всієї таблиці з фільтрацією
$scanRequest = [Amazon.DynamoDBv2.Model.ScanRequest]@{
TableName = 'UserSessions'
FilterExpression = 'IsActive = :true'
ExpressionAttributeValues = @{
':true' = New-DDBEntry -BOOL $true
}
}
Invoke-DDBScan -ScanRequest $scanRequest -Region eu-central-1
Batch-операції та транзакції
BatchGetItem — дозволяє отримати до 100 елементів з однієї або кількох таблиць за один API-виклик. Внутрішньо DynamoDB паралелізує читання, тому час виконання близький до часу читання одного елемента.
# Пакетне отримання кількох сесій за один запит
aws dynamodb batch-get-item \
--request-items '{
"UserSessions": {
"Keys": [
{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2"}},
{"UserId": {"S": "usr-002"}, "SessionId": {"S": "sess-c3d4"}}
]
}
}' \
--region eu-central-1
var response = await client.BatchGetItemAsync(new BatchGetItemRequest
{
RequestItems = new Dictionary<string, KeysAndAttributes>
{
{
"UserSessions", new KeysAndAttributes
{
Keys = new List<Dictionary<string, AttributeValue>>
{
new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2" } }
},
new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-002" } },
{ "SessionId", new AttributeValue { S = "sess-c3d4" } }
}
}
}
}
}
});
# Пакетне отримання кількох елементів за один запит
$requestItems = @{
'UserSessions' = [Amazon.DynamoDBv2.Model.KeysAndAttributes]@{
Keys = @(
@{ UserId = New-DDBEntry -S 'usr-001'; SessionId = New-DDBEntry -S 'sess-a1b2' },
@{ UserId = New-DDBEntry -S 'usr-002'; SessionId = New-DDBEntry -S 'sess-c3d4' }
)
}
}
Get-DDBItemBatch -RequestItems $requestItems -Region eu-central-1
BatchWriteItem — дозволяє записати або видалити до 25 елементів за один виклик. Операції у BatchWriteItem не є атомарними — можливий частковий успіх (деякі елементи записані, деякі — ні). Елементи, що не вдалося обробити, повертаються у полі UnprocessedItems.
# Пакетний запис та видалення сесій (до 25 операцій)
aws dynamodb batch-write-item \
--request-items '{
"UserSessions": [
{
"PutRequest": {
"Item": {
"UserId": {"S": "usr-001"},
"SessionId": {"S": "sess-e5f6"},
"IsActive": {"BOOL": true}
}
}
},
{
"DeleteRequest": {
"Key": {
"UserId": {"S": "usr-002"},
"SessionId": {"S": "sess-old"}
}
}
}
]
}' \
--region eu-central-1
var response = await client.BatchWriteItemAsync(new BatchWriteItemRequest
{
RequestItems = new Dictionary<string, List<WriteRequest>>
{
{
"UserSessions", new List<WriteRequest>
{
new WriteRequest
{
PutRequest = new PutRequest
{
Item = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-e5f6" } },
{ "IsActive", new AttributeValue { BOOL = true } }
}
}
},
new WriteRequest
{
DeleteRequest = new DeleteRequest
{
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-002" } },
{ "SessionId", new AttributeValue { S = "sess-old" } }
}
}
}
}
}
}
});
# Пакетний запис та видалення кількох елементів
$writeRequests = @(
[Amazon.DynamoDBv2.Model.WriteRequest]@{
PutRequest = @{
Item = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-e5f6'
IsActive = New-DDBEntry -BOOL $true
}
}
},
[Amazon.DynamoDBv2.Model.WriteRequest]@{
DeleteRequest = @{
Key = @{
UserId = New-DDBEntry -S 'usr-002'
SessionId = New-DDBEntry -S 'sess-old'
}
}
}
)
$requestItems = @{ 'UserSessions' = $writeRequests }
Write-DDBItemBatch -RequestItems $requestItems -Region eu-central-1
TransactWriteItems / TransactGetItems — атомарні транзакції через кілька елементів і таблиць (детально розглянемо у відповідному розділі).
# Атомарна транзакція: запис нової сесії та збільшення лічильника сесій користувача
aws dynamodb transact-write-items \
--transact-items '[
{
"Put": {
"TableName": "UserSessions",
"Item": {
"UserId": {"S": "usr-001"},
"SessionId": {"S": "sess-new"},
"IsActive": {"BOOL": true}
}
}
},
{
"Update": {
"TableName": "Users",
"Key": {"UserId": {"S": "usr-001"}},
"UpdateExpression": "ADD ActiveSessionsCount :one",
"ExpressionAttributeValues": {":one": {"N": "1"}}
}
}
]' \
--region eu-central-1
var response = await client.TransactWriteItemsAsync(new TransactWriteItemsRequest
{
TransactItems = new List<TransactWriteItem>
{
new TransactWriteItem
{
Put = new Put
{
TableName = "UserSessions",
Item = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-new" } },
{ "IsActive", new AttributeValue { BOOL = true } }
}
}
},
new TransactWriteItem
{
Update = new Update
{
TableName = "Users",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } }
},
UpdateExpression = "ADD ActiveSessionsCount :one",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":one", new AttributeValue { N = "1" } }
}
}
}
}
});
# Атомарна транзакція з кількох операцій запису/оновлення
$transactItems = @(
[Amazon.DynamoDBv2.Model.TransactWriteItem]@{
Put = @{
TableName = 'UserSessions'
Item = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-new'
IsActive = New-DDBEntry -BOOL $true
}
}
},
[Amazon.DynamoDBv2.Model.TransactWriteItem]@{
Update = @{
TableName = 'Users'
Key = @{ UserId = New-DDBEntry -S 'usr-001' }
UpdateExpression = 'ADD ActiveSessionsCount :one'
ExpressionAttributeValues = @{ ':one' = New-DDBEntry -N '1' }
}
}
)
Submit-DDBTransactWriteItems -TransactItem $transactItems -Region eu-central-1
Створення таблиці DynamoDB: консоль та AWS CLI
Консоль AWS Management Console
Найпростіший спосіб ознайомитися з DynamoDB — створити таблицю через web-консоль. Перейдіть до сервісу DynamoDB → Tables → Create table.
Кроки створення таблиці UserSessions:
- Table name:
UserSessions - Partition key:
UserId(тип String) - Sort key:
SessionId(тип String) (необов'язково, але рекомендовано) - Table settings: для початку — Default settings (On-Demand capacity mode)
- Create table
Створення через AWS CLI та PowerShell
AWS CLI надає повний контроль над параметрами таблиці та є основою для автоматизації та Infrastructure-as-Code:
# Створення таблиці UserSessions з Composite Primary Key
aws dynamodb create-table \
--table-name UserSessions \
--attribute-definitions \
AttributeName=UserId,AttributeType=S \
AttributeName=SessionId,AttributeType=S \
--key-schema \
AttributeName=UserId,KeyType=HASH \
AttributeName=SessionId,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--region eu-central-1
var client = new AmazonDynamoDBClient();
var response = await client.CreateTableAsync(new CreateTableRequest
{
TableName = "UserSessions",
AttributeDefinitions = new List<AttributeDefinition>
{
new AttributeDefinition { AttributeName = "UserId", AttributeType = ScalarAttributeType.S },
new AttributeDefinition { AttributeName = "SessionId", AttributeType = ScalarAttributeType.S }
},
KeySchema = new List<KeySchemaElement>
{
new KeySchemaElement { AttributeName = "UserId", KeyType = KeyType.HASH },
new KeySchemaElement { AttributeName = "SessionId", KeyType = KeyType.RANGE }
},
BillingMode = BillingMode.PAY_PER_REQUEST
});
# Встановити AWS Tools for PowerShell (якщо ще не встановлено)
# Install-Module -Name AWS.Tools.DynamoDBv2 -Force
# Створення таблиці UserSessions з Composite Primary Key
$attrUserId = New-DDBAttributeDefinition -AttributeName UserId -AttributeType S
$attrSessionId = New-DDBAttributeDefinition -AttributeName SessionId -AttributeType S
$keyUserId = New-DDBKeySchemaElement -AttributeName UserId -KeyType HASH
$keySessionId = New-DDBKeySchemaElement -AttributeName SessionId -KeyType RANGE
New-DDBTable `
-TableName UserSessions `
-AttributeDefinition @($attrUserId, $attrSessionId) `
-KeySchema @($keyUserId, $keySessionId) `
-BillingMode PAY_PER_REQUEST `
-Region eu-central-1
# Дочекатися поки таблиця перейде у стан ACTIVE
aws dynamodb wait table-exists \
--table-name UserSessions \
--region eu-central-1
echo "Таблиця UserSessions готова"
# Перевірити статус таблиці
aws dynamodb describe-table \
--table-name UserSessions \
--region eu-central-1 \
--query "Table.{Status: TableStatus, ItemCount: ItemCount, SizeBytes: TableSizeBytes}"
var client = new AmazonDynamoDBClient();
while (true)
{
var res = await client.DescribeTableAsync("UserSessions");
if (res.Table.TableStatus == TableStatus.ACTIVE)
{
Console.WriteLine("Таблиця UserSessions готова");
Console.WriteLine($"Status: {res.Table.TableStatus}, Items: {res.Table.ItemCount}, Size: {res.Table.TableSizeBytes} bytes");
break;
}
await Task.Delay(5000);
}
# Дочекатися стану ACTIVE (з timeout 5 хвилин)
$timeout = [DateTime]::UtcNow.AddMinutes(5)
do {
$table = Get-DDBTable -TableName UserSessions -Region eu-central-1
if ($table.TableStatus -eq 'ACTIVE') { break }
Write-Host "Статус: $($table.TableStatus) — очікуємо..."
Start-Sleep -Seconds 5
} until ([DateTime]::UtcNow -ge $timeout)
Write-Host "Таблиця UserSessions готова"
# Перевірити статус таблиці
$t = Get-DDBTable -TableName UserSessions -Region eu-central-1
[PSCustomObject]@{
Status = $t.TableStatus
ItemCount = $t.ItemCount
SizeBytes = $t.TableSizeBytes
}
CRUD-операції
# ── PutItem: записати новий елемент ───────────────────────────────────────
aws dynamodb put-item \
--table-name UserSessions \
--item '{
"UserId": {"S": "usr-001"},
"SessionId": {"S": "sess-a1b2c3d4"},
"CreatedAt": {"S": "2025-06-01T10:00:00Z"},
"ExpiresAt": {"S": "2025-06-01T22:00:00Z"},
"IpAddress": {"S": "203.0.113.42"},
"UserAgent": {"S": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},
"IsActive": {"BOOL": true}
}' \
--region eu-central-1
# ── GetItem: отримати один елемент ────────────────────────────────────────
aws dynamodb get-item \
--table-name UserSessions \
--key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
--region eu-central-1
# ── Query: знайти всі сесії користувача ──────────────────────────────────
aws dynamodb query \
--table-name UserSessions \
--key-condition-expression "UserId = :uid" \
--expression-attribute-values '{":uid": {"S": "usr-001"}}' \
--region eu-central-1
# ── UpdateItem: деактивувати сесію ────────────────────────────────────────
aws dynamodb update-item \
--table-name UserSessions \
--key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
--update-expression "SET IsActive = :false" \
--expression-attribute-values '{":false": {"BOOL": false}}' \
--region eu-central-1
# ── DeleteItem: видалити сесію ───────────────────────────────────────────
aws dynamodb delete-item \
--table-name UserSessions \
--key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
--region eu-central-1
var client = new AmazonDynamoDBClient();
// ── PutItem: записати новий елемент ───────────────────────────────────────
await client.PutItemAsync(new PutItemRequest
{
TableName = "UserSessions",
Item = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } },
{ "CreatedAt", new AttributeValue { S = "2025-06-01T10:00:00Z" } },
{ "ExpiresAt", new AttributeValue { S = "2025-06-01T22:00:00Z" } },
{ "IpAddress", new AttributeValue { S = "203.0.113.42" } },
{ "UserAgent", new AttributeValue { S = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } },
{ "IsActive", new AttributeValue { BOOL = true } }
}
});
// ── GetItem: отримати один елемент ────────────────────────────────────────
var getResponse = await client.GetItemAsync(new GetItemRequest
{
TableName = "UserSessions",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } }
}
});
// ── Query: знайти всі сесії користувача ──────────────────────────────────
var queryResponse = await client.QueryAsync(new QueryRequest
{
TableName = "UserSessions",
KeyConditionExpression = "UserId = :uid",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":uid", new AttributeValue { S = "usr-001" } }
}
});
// ── UpdateItem: деактивувати сесію ────────────────────────────────────────
await client.UpdateItemAsync(new UpdateItemRequest
{
TableName = "UserSessions",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } }
},
UpdateExpression = "SET IsActive = :false",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":false", new AttributeValue { BOOL = false } }
}
});
// ── DeleteItem: видалити сесію ───────────────────────────────────────────
await client.DeleteItemAsync(new DeleteItemRequest
{
TableName = "UserSessions",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-a1b2c3d4" } }
}
});
Import-Module AWS.Tools.DynamoDBv2
# ── PutItem: записати новий елемент ───────────────────────────────────────
$item = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
CreatedAt = New-DDBEntry -S '2025-06-01T10:00:00Z'
ExpiresAt = New-DDBEntry -S '2025-06-01T22:00:00Z'
IpAddress = New-DDBEntry -S '203.0.113.42'
UserAgent = New-DDBEntry -S 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
IsActive = New-DDBEntry -BOOL $true
}
Set-DDBItem -TableName UserSessions -Item $item -Region eu-central-1
# ── GetItem: отримати один елемент ────────────────────────────────────────
$key = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
}
Get-DDBItem -TableName UserSessions -Key $key -Region eu-central-1
# ── Query: знайти всі сесії користувача ──────────────────────────────────
$queryRequest = [Amazon.DynamoDBv2.Model.QueryRequest]@{
TableName = 'UserSessions'
KeyConditionExpression = 'UserId = :uid'
ExpressionAttributeValues = @{
':uid' = New-DDBEntry -S 'usr-001'
}
}
Invoke-DDBQuery -QueryRequest $queryRequest -Region eu-central-1
# ── UpdateItem: деактивувати сесію ────────────────────────────────────────
$updateRequest = [Amazon.DynamoDBv2.Model.UpdateItemRequest]@{
TableName = 'UserSessions'
Key = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
}
UpdateExpression = 'SET IsActive = :false'
ExpressionAttributeValues = @{
':false' = New-DDBEntry -BOOL $false
}
}
Update-DDBItem -UpdateItemRequest $updateRequest -Region eu-central-1
# ── DeleteItem: видалити сесію ───────────────────────────────────────────
$delKey = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-a1b2c3d4'
}
Remove-DDBItem -TableName UserSessions -Key $delKey -Region eu-central-1
Одиниці пропускної здатності: RCU та WCU
Для розробки ефективних архітектур на базі Amazon DynamoDB та прогнозування витрат критично важливо розуміти її внутрішню модель тарифікації та розподілу ресурсів. На відміну від традиційних реляційних СУБД, де продуктивність лімітується апаратними характеристиками сервера (кількістю ядер CPU, обсягом RAM та IOPS накопичувачів), DynamoDB абстрагує фізичні ресурси за допомогою одиниць пропускної здатності (Capacity Units).
Цей підхід дозволяє гарантувати передбачувану пропускну здатність незалежно від загального обсягу даних у таблиці. Вся ємність вимірюється двома метриками: Read Capacity Units (RCU) для операцій читання та Write Capacity Units (WCU) для операцій запису.
Read Capacity Units (RCU)
1 RCU (Read Capacity Unit) визначається як потужність системи, необхідна для виконання:
- 1 Strongly Consistent Read (сильно узгодженого читання) одного елемента розміром до 4 KB за секунду.
- 2 Eventually Consistent Reads (кінцево узгоджених читань) елементів розміром до 4 KB за секунду.
- 0.5 Transactional Read (транзакційного читання) одного елемента розміром до 4 KB за секунду (тобто для транзакційного читання 1 елемента до 4 KB потрібно 2 RCU).
Моделі узгодженості читання (Read Consistency Models)
- Eventually Consistent Reads (Кінцева узгодженість): При записі даних DynamoDB асинхронно реплікує зміни на три географічно розподілені вузли зберігання (storage nodes) в межах однієї Availability Zone. Eventually Consistent читання повертає результат з будь-якої випадкової репліки. Існує мінімальна ймовірність (зазвичай < 1 секунди), що запит поверне застарілі дані, якщо реплікація ще не завершилась. Цей режим споживає вдвічі менше ресурсів — 0.5 RCU за кожні 4 KB.
- Strongly Consistent Reads (Сильна узгодженість): Запит направляється до реплік та очікує підтвердження від більшості вузлів (quorum), гарантуючи повернення найактуальнішого стану даних. Цей режим є дорожчим та споживає 1 RCU за кожні 4 KB.
- Transactional Reads (Транзакційне читання):
Використовується в межах ACID-транзакцій через API
TransactGetItems. Забезпечує ізольовано та атомарно читання групи елементів. Споживає 2 RCU за кожні 4 KB.
Математичне округлення розміру елементів
При обчисленні RCU розмір кожного зчитуваного елемента спочатку округляється в більшу сторону до найближчого кратного 4 KB. Наприклад:
- Елемент розміром 2.5 KB округляється до 4 KB (1 блок).
- Елемент розміром 5.0 KB округляється до 8 KB (2 блоки).
Формула розрахунку RCU
Математична модель розрахунку необхідної кількості RCU виглядає наступним чином:
Де Коефіцієнт узгодженості дорівнює:
- 0.5 — для Eventually Consistent читання.
- 1.0 — для Strongly Consistent читання.
- 2.0 — для Transactional читання (
TransactGetItems).
Покрокові приклади розрахунку RCU
- Приклад 1: Масове читання профілів користувачів
- Умова: Потрібно забезпечити 150 Eventually Consistent читань на секунду. Розмір одного профілю (елемента) становить 3.5 KB.
- Крок 1 (Округлення розміру): ⌈3.5 KB / 4 KB⌉ = 1 блок.
- Крок 2 (Розрахунок за формулою): 1 × 150 × 0.5 = 75 RCU.
- Приклад 2: Читання фінансових транзакцій з високою узгодженістю
- Умова: Необхідно виконувати 80 Strongly Consistent читань на секунду. Розмір елемента транзакції — 9.2 KB.
- Крок 1 (Округлення розміру): ⌈9.2 KB / 4 KB⌉ = ⌈2.3 блоки⌉ = 3 блоки.
- Крок 2 (Розрахунок за формулою): 3 × 80 × 1.0 = 240 RCU.
- Приклад 3: Транзакційна перевірка балансу
- Умова: Потрібно зчитувати 50 балансів на секунду через
TransactGetItems. Розмір запису балансу — 1.5 KB. - Крок 1 (Округлення розміру): ⌈1.5 KB / 4 KB⌉ = 1 блок.
- Крок 2 (Розрахунок за формулою): 1 × 50 × 2.0 = 100 RCU.
- Умова: Потрібно зчитувати 50 балансів на секунду через
Write Capacity Units (WCU)
1 WCU (Write Capacity Unit) визначається як потужність системи, необхідна для виконання:
- 1 стандартного запису (PutItem, UpdateItem, DeleteItem) одного елемента розміром до 1 KB за секунду.
- 0.5 транзакційного запису (TransactWriteItems) одного елемента розміром до 1 KB за секунду (тобто для транзакційного запису 1 елемента до 1 KB потрібно 2 WCU).
Особливості операцій запису
- Співвідношення з читанням: Записи в DynamoDB є в 4 рази "дорожчими" з точки зору розміру даних: якщо 1 RCU покриває 4 KB, то 1 WCU покриває лише 1 KB. Це зумовлено необхідністю синхронної реплікації змін на кілька фізичних вузлів для запобігання втрати даних.
- Умовні записи (Conditional Writes):
Використання
ConditionExpression(наприклад, перевірка чи існує email перед створенням користувача) не змінює вартість успішного запису. Однак, якщо умова не виконується і запис скасовується, DynamoDB все одно стягує WCU за перевірку, якщо запит намагався перезаписати існуючий елемент. - Транзакційні записи (Transactional Writes):
Виконуються через
TransactWriteItemsдля забезпечення атомарності групи записів (до 100 елементів). Кожен запис у транзакції коштує вдвічі дорожче — 2 WCU за 1 KB.
Математичне округшення розміру елементів
При обчисленні WCU розмір кожного записуваного або оновлюваного елемента округляється в більшу сторону до найближчого кратного 1 KB. Наприклад:
- Запис розміром 450 байт округляється до 1 KB (1 блок).
- Запис розміром 2.1 KB округляється до 3 KB (3 блоки).
Формула розрахунку WCU
Математична модель розрахунку необхідної кількості WCU:
Де Коефіцієнт запису дорівнює:
- 1.0 — для стандартних записів (Put, Update, Delete, включно з умовними).
- 2.0 — для транзакційних записів (
TransactWriteItems).
Покрокові приклади розрахунку WCU
- Приклад 1: Логування IoT-метрик
- Умова: Пристрій відправляє 200 метрик на секунду. Розмір одного повідомлення — 600 байт.
- Крок 1 (Округлення розміру): ⌈0.6 KB / 1 KB⌉ = 1 блок.
- Крок 2 (Розрахунок за формулою): 1 × 200 × 1.0 = 200 WCU.
- Приклад 2: Оновлення профілів користувачів
- Умова: Користувачі роблять 45 оновлень профілю на секунду. Розмір оновленого запису — 2.4 KB.
- Крок 1 (Округлення розміру): ⌈2.4 KB / 1 KB⌉ = 3 блоки.
- Крок 2 (Розрахунок за формулою): 3 × 45 × 1.0 = 135 WCU.
- Приклад 3: Оформлення замовлення транзакцією
- Умова: Система обробляє 10 замовлень на секунду. Транзакція записує 1 замовлення (розмір 1.2 KB) та оновлює складські запаси (розмір 0.8 KB).
- Крок 1 (Замовлення): Розмір 1.2 KB оновлюється до 2 KB. Вартість: 2 × 10 × 2.0 = 40 WCU.
- Крок 2 (Склад): Розмір 0.8 KB оновлюється до 1 KB. Вартість: 1 × 10 × 2.0 = 20 WCU.
- Загальна вартість: 40 + 20 = 60 WCU.
Зведена порівняльна таблиця вартості ємностей
| Операція | Одиниці базового виміру | Коефіцієнт (Multiplier) | Вартість 1 операції до базового розміру |
|---|---|---|---|
| Eventually Consistent Read | 4 KB | 0.5 | 0.5 RCU |
| Strongly Consistent Read | 4 KB | 1.0 | 1.0 RCU |
| Transactional Read | 4 KB | 2.0 | 2.0 RCU |
| Standard Write (Put/Update/Delete) | 1 KB | 1.0 | 1.0 WCU |
| Transactional Write | 1 KB | 2.0 | 2.0 WCU |
::
Secondary Indexes: запити поза первинним ключем
Ми з'ясували фундаментальне обмеження DynamoDB: операція Query може шукати елементи лише за Partition Key основної таблиці. Це означає, що таблиця UserSessions з PK=UserId / SK=SessionId ефективно відповідає на запитання «які сесії має користувач X?», але абсолютно не здатна відповісти на «які сесії закінчились до певної дати?» або «які сесії з конкретної IP-адреси?» — без повного Scan всієї таблиці.
DynamoDB вирішує цю проблему через Secondary Indexes — механізм, що дозволяє визначити альтернативну схему ключів для таблиці та виконувати Query за цією альтернативною схемою. Існує два типи Secondary Indexes з принципово різними характеристиками.
Для наочності розглянемо поточний стан основної таблиці UserSessions (з первинним ключем UserId + SessionId), на прикладі якої ми будемо вивчати обидва типи індексів:
Вихідний набір даних таблиці UserSessions:
| UserId (Partition Key) | SessionId (Sort Key) | ExpiresAt | IpAddress | IsActive | CreatedAt |
|---|---|---|---|---|---|
usr-001 | sess-A | 2025-06-01T22:00:00Z | 203.0.113.42 | true | 2025-06-01T10:00:00Z |
usr-001 | sess-B | 2025-06-02T10:00:00Z | 198.51.100.7 | true | 2025-06-01T11:00:00Z |
usr-001 | sess-C | 2025-05-31T08:00:00Z | 203.0.113.42 | false | 2025-05-30T16:00:00Z |
usr-002 | sess-D | 2025-06-03T14:00:00Z | 10.0.0.5 | true | 2025-06-01T13:00:00Z |
Local Secondary Index (LSI)
Local Secondary Index (LSI) представляє собою альтернативний індекс доступу, який створюється в межах однієї логічної партиції. Ключовою архітектурною особливістю LSI є те, що він зобов'язаний використовувати той самий Partition Key (HASH), що й основна таблиця, але дозволяє визначити інший атрибут як Sort Key (RANGE).
Термін «локальний» вказує на фізичну локалізацію даних: всі проектовані елементи індексу, що відповідають певному Partition Key, зберігаються на тому самому фізичному вузлі (storage node) і в межах тієї ж групи партицій, що й відповідні оригінальні елементи основної таблиці. Це забезпечує сувору гарантію транзакційної узгодженості та мінімальних затримок при зверненні.
Архітектурна доцільність та патерни застосування LSI
Необхідність використання LSI виникає у сценаріях, коли для однієї сутності (ідентифікованої за Partition Key) потрібно виконувати складні запити з різними критеріями сортування або фільтрації.
Розглянемо практичний сценарій проектування таблиці сесій користувачів UserSessions, де первинний ключ побудований як composite: Partition Key = UserId, Sort Key = SessionId. Ця схема дозволяє виконувати ефективні операції точкового читання (GetItem) конкретної сесії або отримувати перелік усіх сесій користувача (Query).
Проте, якщо виникає патерн доступу виду «отримати всі сесії конкретного користувача, термін дії яких закінчується до заданого моменту» (наприклад, для інвалідації сесій або виведення попереджень користувачу), стандартна схема вимагатиме виконання запиту Query за UserId з подальшою фільтрацією за полем ExpiresAt на стороні додатка або через FilterExpression. Це призведе до зайвого споживання RCU, оскільки DynamoDB зчитує всі сесії користувача, перш ніж застосувати фільтр.
Створення LSI з назвою SessionsByExpiry, де Sort Key визначено як ExpiresAt, дозволяє виконувати прямі сортувальні запити (Query) в межах одного UserId з накладанням умов на ExpiresAt безпосередньо на рівні бази даних.
Системні обмеження та архітектурні компроміси LSI
Хоча LSI надає перевагу у вигляді сильної узгодженості та атомарності, його використання супроводжується жорсткими обмеженнями, які необхідно враховувати на етапі проектування.
1. Обмеження розміру колекції елементів (10 GB Item Collection Limit)
Найбільш критичним обмеженням LSI є ліміт на розмір колекції елементів (Item Collection). Колекція елементів — це сукупність усіх елементів у таблиці та в усіх її локальних вторинних індексах, які мають однакове значення Partition Key.
Фізично, для забезпечення низьких затримок, DynamoDB зберігає всю колекцію елементів на одному фізичному вузлі партиції. Через це сумарний розмір колекції елементів для одного значення Partition Key не може перевищувати 10 GB. Якщо обсяг даних одного ключа (наприклад, сесій одного активного користувача) перевищить цей ліміт, будь-які подальші операції запису або оновлення для цього Partition Key завершаться помилкою ValidationException.
Практичний висновок: LSI абсолютно не підходить для сутностей з необмеженим зростанням даних (наприклад, логування подій IoT-пристроїв за ідентифікатором пристрою, де обсяг логів на один пристрій гарантовано перевищить 10 GB).
2. Неможливість динамічного керування
Локальний вторинний індекс може бути створений виключно в момент ініціалізації таблиці (CreateTable). Додати LSI до існуючої таблиці або видалити його пізніше неможливо. Якщо в процесі експлуатації виявиться потреба в новому LSI, єдиним виходом буде створення нової таблиці, налаштування нової схеми індексів та міграція всіх історичних даних, що є тривалим та дорогим процесом.
Стратегії проектування індексів: Типи проекцій (Projection Types)
При створенні вторинного індексу розробник повинен визначити, які саме атрибути з основної таблиці будуть копіюватися (проектувалися) в індекс. Це визначається параметром Projection. Вибір типу проекції є компромісом між обсягом збережених даних (витрати на Storage та WCU) та продуктивністю запитів (витрати на RCU).
KEYS_ONLY
Індекс зберігає лише ключові атрибути: PK та SK основної таблиці + PK та SK індексу. Мінімальний розмір → мінімальні витрати. Якщо потрібні інші атрибути — DynamoDB автоматично виконує додаткове читання основної таблиці.
Коли: часто використовується як «існування» (перевірити чи є такий запис), або якщо майже завжди потрібно також читати основну таблицю для деталей.
INCLUDE
Зберігає ключові атрибути + явно вказаний список атрибутів (NonKeyAttributes). Дозволяє точно контролювати, які поля дублюються в індексі.
Коли: коли відомий фіксований набір атрибутів, що потрібні при Query через індекс. Наприклад, ExpiresAt + IsActive — достатньо для перевірки статусу сесії без читання повного елемента.
ALL
Зберігає всі атрибути елемента. Максимальний розмір індексу — фактично дублювання всієї таблиці. Але Query через індекс повертає повний елемент без додаткових читань.
Коли: якщо при Query через індекс завжди потрібні всі атрибути, і витрати на зберігання прийнятні.
KEYS_ONLY або INCLUDE (мінімально необхідний набір атрибутів для вашого Query). ALL зручний, але подвоює або потроює витрати на зберігання. DynamoDB не дозволяє змінити Projection після створення індексу — треба перестворювати.Вплив LSI на споживання RCU та WCU
Використання локальних індексів суттєво оновлює можливості запитів, проте створює додаткове навантаження на пропускну здатність таблиці:
- Витрати на запис (Write Cost Dynamics):
При додаванні (
PutItem), оновленні (UpdateItem) або видаленні (DeleteItem) елемента в основній таблиці DynamoDB автоматично оновлює дані у всіх LSI.- Якщо записується новий елемент, споживається 1 WCU (або 2 WCU для транзакцій) за кожен 1 KB розміру елемента як для основної таблиці, так і для кожного LSI, куди проектується цей елемент.
- Якщо оновлюється атрибут елемента, який не входить у проекцію LSI, додаткові WCU для цього індексу не споживаються.
- Якщо оновлюється атрибут, що входить до проекції індексу (або змінюється сам ключ індексу Sort Key), DynamoDB виконує дві операції в індексі (видалення старого запису та запис нового), що споживає додаткові WCU.
Усі витрати WCU на обслуговування LSI списуються з пулу пропускної здатності основної таблиці. - Витрати на читання та явище Main Table Fetch (Read Cost Dynamics):
При виконанні запиту
Queryчерез LSI вартість у RCU залежить від вибраного типу проекції:- Покритий запит (Covered Query): Якщо запит запитує лише ті атрибути, які були спроектовані в індекс (
KEYS_ONLYабоINCLUDE), DynamoDB зчитує дані виключно з LSI. Розрахунок RCU відбувається стандартно: 1 RCU на 4 KB спроектованих даних (Strongly Consistent). - Непокритий запит та Main Table Fetch: Якщо запит звертається до атрибутів, які відсутні в проекції індексу (наприклад, у проекції
KEYS_ONLYзапитується полеIpAddress), DynamoDB змушена виконати внутрішню операцію вибірки з основної таблиці (Main Table Fetch / Lookup) для кожного знайденого елемента.
Штраф за Fetch: Кожна така вибірка з основної таблиці виконується як окреме Strongly Consistent читання і споживає щонайменше 1 RCU на кожен елемент (навіть якщо розмір елемента менший за 4 KB) та додає затримку (latency) на мережеве коло всередині бази даних.
Формула розрахунку RCU для непокритого запиту:Завантаження... - Покритий запит (Covered Query): Якщо запит запитує лише ті атрибути, які були спроектовані в індекс (
Створення таблиці з LSI
# Таблиця UserSessions з LSI SessionsByExpiry
aws dynamodb create-table \
--table-name UserSessions \
--attribute-definitions \
AttributeName=UserId,AttributeType=S \
AttributeName=SessionId,AttributeType=S \
AttributeName=ExpiresAt,AttributeType=S \
--key-schema \
AttributeName=UserId,KeyType=HASH \
AttributeName=SessionId,KeyType=RANGE \
--local-secondary-indexes '[
{
"IndexName": "SessionsByExpiry",
"KeySchema": [
{"AttributeName": "UserId", "KeyType": "HASH"},
{"AttributeName": "ExpiresAt", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "INCLUDE",
"NonKeyAttributes": ["IsActive", "IpAddress"]
}
}
]' \
--billing-mode PAY_PER_REQUEST \
--region eu-central-1
var client = new AmazonDynamoDBClient();
var response = await client.CreateTableAsync(new CreateTableRequest
{
TableName = "UserSessions",
AttributeDefinitions = new List<AttributeDefinition>
{
new AttributeDefinition { AttributeName = "UserId", AttributeType = ScalarAttributeType.S },
new AttributeDefinition { AttributeName = "SessionId", AttributeType = ScalarAttributeType.S },
new AttributeDefinition { AttributeName = "ExpiresAt", AttributeType = ScalarAttributeType.S }
},
KeySchema = new List<KeySchemaElement>
{
new KeySchemaElement { AttributeName = "UserId", KeyType = KeyType.HASH },
new KeySchemaElement { AttributeName = "SessionId", KeyType = KeyType.RANGE }
},
LocalSecondaryIndexes = new List<LocalSecondaryIndex>
{
new LocalSecondaryIndex
{
IndexName = "SessionsByExpiry",
KeySchema = new List<KeySchemaElement>
{
new KeySchemaElement { AttributeName = "UserId", KeyType = KeyType.HASH },
new KeySchemaElement { AttributeName = "ExpiresAt", KeyType = KeyType.RANGE }
},
Projection = new Projection
{
ProjectionType = ProjectionType.INCLUDE,
NonKeyAttributes = new List<string> { "IsActive", "IpAddress" }
}
}
},
BillingMode = BillingMode.PAY_PER_REQUEST
});
$attrUserId = New-DDBAttributeDefinition -AttributeName UserId -AttributeType S
$attrSessionId = New-DDBAttributeDefinition -AttributeName SessionId -AttributeType S
$attrExpiresAt = New-DDBAttributeDefinition -AttributeName ExpiresAt -AttributeType S
$keyUserId = New-DDBKeySchemaElement -AttributeName UserId -KeyType HASH
$keySessionId = New-DDBKeySchemaElement -AttributeName SessionId -KeyType RANGE
# LSI: той самий PK (UserId), новий SK (ExpiresAt)
$lsiKey1 = New-DDBKeySchemaElement -AttributeName UserId -KeyType HASH
$lsiKey2 = New-DDBKeySchemaElement -AttributeName ExpiresAt -KeyType RANGE
$lsiProjection = [Amazon.DynamoDBv2.Model.Projection]@{
ProjectionType = 'INCLUDE'
NonKeyAttributes = @('IsActive', 'IpAddress')
}
$lsi = [Amazon.DynamoDBv2.Model.LocalSecondaryIndex]@{
IndexName = 'SessionsByExpiry'
KeySchema = @($lsiKey1, $lsiKey2)
Projection = $lsiProjection
}
New-DDBTable `
-TableName UserSessions `
-AttributeDefinition @($attrUserId, $attrSessionId, $attrExpiresAt) `
-KeySchema @($keyUserId, $keySessionId) `
-LocalSecondaryIndex @($lsi) `
-BillingMode PAY_PER_REQUEST `
-Region eu-central-1
Query через LSI
# Знайти сесії usr-001, що закінчаться до 2025-06-02T00:00:00Z
aws dynamodb query \
--table-name UserSessions \
--index-name SessionsByExpiry \
--key-condition-expression "UserId = :uid AND ExpiresAt < :exp" \
--expression-attribute-values '{
":uid": {"S": "usr-001"},
":exp": {"S": "2025-06-02T00:00:00Z"}
}' \
--region eu-central-1
var client = new AmazonDynamoDBClient();
var response = await client.QueryAsync(new QueryRequest
{
TableName = "UserSessions",
IndexName = "SessionsByExpiry",
KeyConditionExpression = "UserId = :uid AND ExpiresAt < :exp",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":uid", new AttributeValue { S = "usr-001" } },
{ ":exp", new AttributeValue { S = "2025-06-02T00:00:00Z" } }
}
});
$queryRequest = [Amazon.DynamoDBv2.Model.QueryRequest]@{
TableName = 'UserSessions'
IndexName = 'SessionsByExpiry'
KeyConditionExpression = 'UserId = :uid AND ExpiresAt < :exp'
ExpressionAttributeValues = @{
':uid' = New-DDBEntry -S 'usr-001'
':exp' = New-DDBEntry -S '2025-06-02T00:00:00Z'
}
}
Invoke-DDBQuery -QueryRequest $queryRequest -Region eu-central-1
Global Secondary Index (GSI)
Global Secondary Index (GSI) представляє собою найбільш гнучкий та потужний інструмент побудови вторинних ключів доступу в Amazon DynamoDB. На відміну від LSI, GSI дозволяє обрати абсолютно довільні атрибути таблиці як в якості Partition Key (HASH), так і Sort Key (RANGE), повністю ігноруючи схему первинного ключа основної таблиці. Слово «Global» у назві вказує на те, що запити через цей індекс можуть охоплювати дані на всіх фізичних партиціях основної таблиці.
Архітектурні особливості та фізична реалізація GSI
Під капом Amazon DynamoDB реалізує GSI як повністю автономну внутрішню таблицю, яка автоматично синхронізується з основною таблицею. Ця внутрішня таблиця має власну схему розділення на фізичні партиції, власні вузли зберігання (storage nodes) і, що найважливіше, — свій власний незалежний пул пропускної здатності (RCU та WCU) у випадку використання режиму Provisioned.
Така фізична автономність зумовлює три фундаментальні архітектурні наслідки:
- Гарантована кінцева узгодженість (Eventually Consistent Reads):
Синхронізація даних між основною таблицею та GSI виконується асинхронно за допомогою внутрішнього механізму реплікації. Через це запити (
Query) через GSI підтримують виключно Eventually Consistent reads. Зчитування з Strongly Consistent або в межах транзакцій безпосередньо через GSI є неможливим, оскільки реплікація в індекс відбувається з мінімальною затримкою (зазвичай до кількох десятків мілісекунд, але за умови перевантаження індексу затримка може зростати). - Автономне керування ємністю (Independent Throughput): У режимі Provisioned для GSI необхідно окремо налаштовувати ліміти RCU та WCU. Вони ніяк не пов'язані з лімітами основної таблиці та оплачуються окремо.
- Ефект "зворотного тиску" при перевантаженні індексу (GSI Throttling Backpressure):
Оскільки реплікація в GSI відбувається асинхронно, при високій інтенсивності записів на основну таблицю індекс може не встигати оновлюватися, якщо його ліміт WCU занижений. Щоб запобігти безконтрольному відставанню реплікації та переповненню черг оновлень, DynamoDB застосовує зворотний тиск: помилки недостатньої ємності (throttling) на GSI будуть каскадно блокувати операції запису в основній таблиці.
Критичне правило: навіть якщо основна таблиця має надлишок WCU, запис до неї завершиться помилкоюProvisionedThroughputExceededException, якщо асоційований GSI вичерпає власні WCU.
Sparse Index (Розріджені індекси) — паттерн оптимізації обсягу та витрат
Sparse Index (розріджений індекс) — це фундаментальний патерн проектування вторинних індексів у DynamoDB, який використовує вибіркову поведінку оновлення індексів.
За замовчуванням вторинний індекс у DynamoDB вважається розрідженим, якщо будь-який елемент основної таблиці не містить атрибутів, визначених як Partition Key або Sort Key цього індексу. DynamoDB не створює запис в індексі, якщо у вихідному елементі відсутній хоча б один із ключів індексу.
Переваги та архітектурна цінність розріджених індексів:
- Скорочення витрат на зберігання (Storage Savings): Замість копіювання мільярдів записів, індекс містить лише ту вибіркову підмножину даних, яка відповідає певній умові бізнес-логіки.
- Мінімізація WCU (Write Savings): DynamoDB оновлює GSI лише тоді, коли створюється або змінюється елемент, що містить ключові атрибути індексу. Зміна будь-яких інших елементів таблиці не витрачає пропускну здатність GSI.
- Висока швидкість виконання запитів (Query Efficiency):
Оскільки індекс компактний, операції
Queryвиконуються за мінімальну кількість звернень, без потреби сканувати зайві дані.
Сценарій застосування: Обробка незавершених замовлень (Pending Orders)
Уявімо велику e-commerce систему з мільярдами замовлень, де 99% замовлень знаходяться у кінцевих статусах (DELIVERED, CANCELLED), і лише 1% замовлень перебуває в процесі обробки (PENDING).
Операторам потрібно постійно отримувати список замовлень для обробки. Сканування всієї таблиці є неприпустимо дорогим. Якщо ми створимо GSI з ключем сортування, який заповнюється значенням дати виключно для замовлень зі статусом PENDING (наприклад, атрибут PendingStatus заповнюється лише тоді, коли статус дорівнює PENDING), індекс міститиме лише цей 1% активних замовлень. Як тільки замовлення доставляється, атрибут PendingStatus видаляється з елемента, і DynamoDB автоматично прибирає цей запис із розрідженого GSI.
Додавання GSI до існуючої таблиці та фонове заповнення (Backfilling)
На відміну від локальних індексів (LSI), глобальні індекси (GSI) є динамічними сутностями. Їх можна створювати, оновлювати або видаляти на будь-му етапі життєвого циклу таблиці, навіть якщо вона містить мільярди записів.
Процес онлайн-індексації та фонового заповнення (Backfilling)
Створення GSI на існуючій таблиці є складною розподіленою операцією, яка виконується повністю у фоновому режимі:
- Створення метаданих (State: CREATING): Після виклику
UpdateTableстатус індексу переходить уCREATING. DynamoDB виділяє фізичні ресурси (storage nodes) для нової партиційної структури індексу. - Фаза фонового сканування та заповнення (Backfilling phase): DynamoDB автоматично запускає фоновий процес сканування основної таблиці. Кожен знайдений елемент, що містить ключові атрибути GSI, проектується та записується в індекс.
- Синхронізація дельти (Catch-up phase): Одночасно з заповненням історичними даними, всі нові операції запису, які виконуються клієнтами в цей момент до основної таблиці, автоматично буферизуються та реплікуються в новий індекс.
- Активація (State: ACTIVE): Після завершення реплікації дельти статус індексу змінюється на
ACTIVE, і він стає доступним для виконання запитів читання.
Розрахунок RCU/WCU під час Backfilling
Процес створення індексу створює додаткове навантаження на пропускну здатність:
- Фонове сканування основної таблиці споживає її власні RCU (AWS намагається виконувати це з мінімальним пріоритетом, щоб не заважати основному бізнес-трафіку).
- Запис даних у новий індекс споживає WCU новоствореного GSI. Якщо для GSI встановлено занижене значення WCU, процес заповнення триватиме значно довше або взагалі тимчасово призупиниться.
# ── Додавання GSI SessionsByIp до існуючої таблиці UserSessions ──────────────
aws dynamodb update-table \
--table-name UserSessions \
--attribute-definitions \
AttributeName=IpAddress,AttributeType=S \
AttributeName=CreatedAt,AttributeType=S \
--global-secondary-index-updates '[{
"Create": {
"IndexName": "SessionsByIp",
"KeySchema": [
{"AttributeName": "IpAddress", "KeyType": "HASH"},
{"AttributeName": "CreatedAt", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "INCLUDE",
"NonKeyAttributes": ["UserId", "IsActive"]
}
}
}]' \
--region eu-central-1
# ── Моніторинг статусу створення індексу ─────────────────────────────────────
aws dynamodb describe-table \
--table-name UserSessions \
--region eu-central-1 \
--query "Table.GlobalSecondaryIndexes[*].{Name:IndexName, Status:IndexStatus}"
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
var client = new AmazonDynamoDBClient();
// ── Додавання GSI (SessionsByIp) до існуючої таблиці (UserSessions) ──────────
await client.UpdateTableAsync(new UpdateTableRequest
{
TableName = "UserSessions",
AttributeDefinitions = new List<AttributeDefinition>
{
new() { AttributeName = "IpAddress", AttributeType = ScalarAttributeType.S },
new() { AttributeName = "CreatedAt", AttributeType = ScalarAttributeType.S }
},
GlobalSecondaryIndexUpdates = new List<GlobalSecondaryIndexUpdate>
{
new()
{
Create = new CreateGlobalSecondaryIndexAction
{
IndexName = "SessionsByIp",
KeySchema = new List<KeySchemaElement>
{
new() { AttributeName = "IpAddress", KeyType = KeyType.HASH },
new() { AttributeName = "CreatedAt", KeyType = KeyType.RANGE }
},
Projection = new Projection
{
ProjectionType = ProjectionType.INCLUDE,
NonKeyAttributes = new List<string> { "UserId", "IsActive" }
},
ProvisionedThroughput = new ProvisionedThroughput
{
ReadCapacityUnits = 50,
WriteCapacityUnits = 50
}
}
}
}
});
Import-Module AWS.Tools.DynamoDBv2
$attrIp = New-DDBAttributeDefinition -AttributeName IpAddress -AttributeType S
$attrCreatedAt = New-DDBAttributeDefinition -AttributeName CreatedAt -AttributeType S
$gsiKeyIp = New-DDBKeySchemaElement -AttributeName IpAddress -KeyType HASH
$gsiKeyCreatedAt = New-DDBKeySchemaElement -AttributeName CreatedAt -KeyType RANGE
$gsiProjection = [Amazon.DynamoDBv2.Model.Projection]@{
ProjectionType = 'INCLUDE'
NonKeyAttributes = @('UserId', 'IsActive')
}
$gsiCreate = [Amazon.DynamoDBv2.Model.GlobalSecondaryIndexUpdate]@{
Create = [Amazon.DynamoDBv2.Model.CreateGlobalSecondaryIndexAction]@{
IndexName = 'SessionsByIp'
KeySchema = @($gsiKeyIp, $gsiKeyCreatedAt)
Projection = $gsiProjection
}
}
Update-DDBTable `
-TableName UserSessions `
-AttributeDefinition @($attrIp, $attrCreatedAt) `
-GlobalSecondaryIndexUpdate @($gsiCreate) `
-Region eu-central-1
# ── Перевірка статусу створення індексу ──────────────────────────────────────
(Get-DDBTable -TableName UserSessions -Region eu-central-1).GlobalSecondaryIndexes |
Select-Object IndexName, IndexStatus
[
{
"Name": "SessionsByIp",
"Status": "CREATING"
}
]
Виконання запитів (Query) через GSI
# ── Пошук сесій за IP та часом створення ──────────────────────────────────────
aws dynamodb query \
--table-name UserSessions \
--index-name SessionsByIp \
--key-condition-expression "IpAddress = :ip AND CreatedAt >= :since" \
--expression-attribute-values '{
":ip": {"S": "203.0.113.42"},
":since": {"S": "2025-05-25T00:00:00Z"}
}' \
--region eu-central-1
# ── Видалення глобального індексу GSI ─────────────────────────────────────────
aws dynamodb update-table \
--table-name UserSessions \
--global-secondary-index-updates '[{
"Delete": {"IndexName": "SessionsByIp"}
}]' \
--region eu-central-1
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
var client = new AmazonDynamoDBClient();
// ── Пошук сесій за IP та датою через GSI ─────────────────────────────────────
var queryRequest = new QueryRequest
{
TableName = "UserSessions",
IndexName = "SessionsByIp",
KeyConditionExpression = "IpAddress = :ip AND CreatedAt >= :since",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":ip", new AttributeValue { S = "203.0.113.42" } },
{ ":since", new AttributeValue { S = "2025-05-25T00:00:00Z" } }
}
};
var response = await client.QueryAsync(queryRequest);
Import-Module AWS.Tools.DynamoDBv2
# ── Пошук сесій за IP та часом створення ──────────────────────────────────────
$queryRequest = [Amazon.DynamoDBv2.Model.QueryRequest]@{
TableName = 'UserSessions'
IndexName = 'SessionsByIp'
KeyConditionExpression = 'IpAddress = :ip AND CreatedAt >= :since'
ExpressionAttributeValues = @{
':ip' = New-DDBEntry -S '203.0.113.42'
':since' = New-DDBEntry -S '2025-05-25T00:00:00Z'
}
}
$result = Invoke-DDBQuery -QueryRequest $queryRequest -Region eu-central-1
$result.Items
# ── Видалення глобального індексу GSI ─────────────────────────────────────────
$gsiDelete = [Amazon.DynamoDBv2.Model.GlobalSecondaryIndexUpdate]@{
Delete = [Amazon.DynamoDBv2.Model.DeleteGlobalSecondaryIndexAction]@{
IndexName = 'SessionsByIp'
}
}
Update-DDBTable -TableName UserSessions -GlobalSecondaryIndexUpdate @($gsiDelete) -Region eu-central-1
Порівняльний аналіз Local Secondary Index та Global Secondary Index
Для оптимального вибору індексу під конкретний сценарій використання зверніться до порівняльної таблиці:
| Характеристика | Local Secondary Index (LSI) | Global Secondary Index (GSI) |
|---|---|---|
| Partition Key (HASH) | Має бути ідентичним Partition Key основної таблиці | Може бути будь-яким атрибутом таблиці |
| Sort Key (RANGE) | Будь-який атрибут основної таблиці | Будь-який атрибут основної таблиці |
| Момент створення | Виключно під час створення таблиці | Будь-коли протягом життєвого циклу таблиці |
| Обмеження розміру | Ліміт у 10 GB на колекцію елементів (Item Collection) | Без лімітів (горизонтальне масштабування) |
| Пропускна здатність | Використовує RCU/WCU основної таблиці | Вимагає окремого виділення RCU/WCU для індексу |
| Модель консистентності | Strongly Consistent або Eventually Consistent читання | Виключно Eventually Consistent читання |
| Штраф вибірки (Fetch Penalty) | Дозволяє запитувати непроектовані атрибути (+1 RCU з основної таблиці) | Не підтримує автоматичне довантаження (запит провалюється) |
Вибір типу індексу (Decision Tree)
Практичний приклад: проектування індексів для E-commerce
У системах електронної комерції Single-Table Design є промисловим стандартом. Розглянемо проектування таблиці Orders, яка повинна ефективно обслуговувати чотири типи запитів.
Вимоги додатку до доступу до даних (Access Patterns):
- Запит 1: Отримання всіх замовлень конкретного клієнта, відсортованих за часом оформлення.
- Запит 2: Отримання всіх замовлень у статусі
PENDING(очікують на обробку), відсортованих за часом оформлення. - Запит 3: Отримання всіх замовлень, що містять конкретний продукт.
- Запит 4: Пошук замовлення клієнта у визначеному часовому діапазоні.
Схема проектування ключів та індексів:
- Основний ключ (Composite PK): Partition Key —
CustomerId, Sort Key —OrderDate#OrderId. Це повністю покриває Запит 1 та Запит 4 за допомогою оператораbegins_withтаbetween. - Індекс 1 (PendingOrdersByDate - Sparse GSI): Для Запиту 2 створюється розріджений індекс.
PendingStatusвикористовується як HASH (заповнюється значеннямPENDINGлише для незавершених замовлень), аOrderDateId— як RANGE. При зміні статусу наPAIDатрибутPendingStatusвидаляється, і замовлення зникає з індексу. - Індекс 2 (OrdersByProduct - GSI): Для Запиту 3 створюється індекс, де Partition Key —
ProductId_0(ідентифікатор першого продукту у кошику), а Sort Key —OrderDateIdз проекцієюKEYS_ONLY.
Практична реалізація E-commerce таблиці
# ── Створення таблиці Orders з двома GSI ──────────────────────────────────────
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=CustomerId,AttributeType=S \
AttributeName=OrderDateId,AttributeType=S \
AttributeName=PendingStatus,AttributeType=S \
AttributeName=ProductId_0,AttributeType=S \
--key-schema \
AttributeName=CustomerId,KeyType=HASH \
AttributeName=OrderDateId,KeyType=RANGE \
--global-secondary-indexes '[
{
"IndexName": "PendingOrdersByDate",
"KeySchema": [
{"AttributeName": "PendingStatus", "KeyType": "HASH"},
{"AttributeName": "OrderDateId", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "INCLUDE",
"NonKeyAttributes": ["CustomerId", "TotalAmount"]
}
},
{
"IndexName": "OrdersByProduct",
"KeySchema": [
{"AttributeName": "ProductId_0", "KeyType": "HASH"},
{"AttributeName": "OrderDateId", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "KEYS_ONLY"}
}
]' \
--billing-mode PAY_PER_REQUEST \
--region eu-central-1
# ── Запис нового PENDING замовлення (потрапляє в Sparse GSI) ──────────────────
aws dynamodb put-item \
--table-name Orders \
--item '{
"CustomerId": {"S": "cust-001"},
"OrderDateId": {"S": "2025-06-01T10:00#ord-XYZ"},
"OrderId": {"S": "ord-XYZ"},
"Status": {"S": "PENDING"},
"PendingStatus": {"S": "PENDING"},
"TotalAmount": {"N": "149.99"},
"ProductId_0": {"S": "prod-laptop"}
}' \
--region eu-central-1
# ── Оплата замовлення (видалення PendingStatus -> автоматично зникає з GSI) ────
aws dynamodb update-item \
--table-name Orders \
--key '{"CustomerId": {"S": "cust-001"}, "OrderDateId": {"S": "2025-06-01T10:00#ord-XYZ"}}' \
--update-expression "SET #s = :paid REMOVE PendingStatus" \
--expression-attribute-names '{"#s": "Status"}' \
--expression-attribute-values '{":paid": {"S": "PAID"}}' \
--region eu-central-1
# ── Отримання активних PENDING замовлень через Sparse GSI ───────────────────────
aws dynamodb query \
--table-name Orders \
--index-name PendingOrdersByDate \
--key-condition-expression "PendingStatus = :p" \
--expression-attribute-values '{":p": {"S": "PENDING"}}' \
--region eu-central-1
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
var client = new AmazonDynamoDBClient();
// ── Створення таблиці Orders з двома глобальними індексами (GSI) ──────────────
await client.CreateTableAsync(new CreateTableRequest
{
TableName = "Orders",
AttributeDefinitions = new List<AttributeDefinition>
{
new() { AttributeName = "CustomerId", AttributeType = ScalarAttributeType.S },
new() { AttributeName = "OrderDateId", AttributeType = ScalarAttributeType.S },
new() { AttributeName = "PendingStatus", AttributeType = ScalarAttributeType.S },
new() { AttributeName = "ProductId_0", AttributeType = ScalarAttributeType.S }
},
KeySchema = new List<KeySchemaElement>
{
new() { AttributeName = "CustomerId", KeyType = KeyType.HASH },
new() { AttributeName = "OrderDateId", KeyType = KeyType.RANGE }
},
GlobalSecondaryIndexes = new List<GlobalSecondaryIndex>
{
new()
{
IndexName = "PendingOrdersByDate",
KeySchema = new List<KeySchemaElement>
{
new() { AttributeName = "PendingStatus", KeyType = KeyType.HASH },
new() { AttributeName = "OrderDateId", KeyType = KeyType.RANGE }
},
Projection = new Projection
{
ProjectionType = ProjectionType.INCLUDE,
NonKeyAttributes = new List<string> { "CustomerId", "TotalAmount" }
},
ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 10, WriteCapacityUnits = 10 }
},
new()
{
IndexName = "OrdersByProduct",
KeySchema = new List<KeySchemaElement>
{
new() { AttributeName = "ProductId_0", KeyType = KeyType.HASH },
new() { AttributeName = "OrderDateId", KeyType = KeyType.RANGE }
},
Projection = new Projection { ProjectionType = ProjectionType.KEYS_ONLY },
ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 10, WriteCapacityUnits = 10 }
}
},
BillingMode = BillingMode.PAY_PER_REQUEST
});
// ── Запис нового замовлення зі статусом PENDING ──────────────────────────────
await client.PutItemAsync(new PutItemRequest
{
TableName = "Orders",
Item = new Dictionary<string, AttributeValue>
{
{ "CustomerId", new AttributeValue { S = "cust-001" } },
{ "OrderDateId", new AttributeValue { S = "2025-06-01T10:00#ord-XYZ" } },
{ "OrderId", new AttributeValue { S = "ord-XYZ" } },
{ "Status", new AttributeValue { S = "PENDING" } },
{ "PendingStatus", new AttributeValue { S = "PENDING" } },
{ "TotalAmount", new AttributeValue { N = "149.99" } },
{ "ProductId_0", new AttributeValue { S = "prod-laptop" } }
}
});
// ── Оплата замовлення: оновлення статусу та видалення PendingStatus ────────────
await client.UpdateItemAsync(new UpdateItemRequest
{
TableName = "Orders",
Key = new Dictionary<string, AttributeValue>
{
{ "CustomerId", new AttributeValue { S = "cust-001" } },
{ "OrderDateId", new AttributeValue { S = "2025-06-01T10:00#ord-XYZ" } }
},
UpdateExpression = "SET #s = :paid REMOVE PendingStatus",
ExpressionAttributeNames = new Dictionary<string, string> { { "#s", "Status" } },
ExpressionAttributeValues = new Dictionary<string, AttributeValue> { { ":paid", new AttributeValue { S = "PAID" } } }
});
// ── Запит активних PENDING замовлень через Sparse GSI ──────────────────────────
var pendingQuery = new QueryRequest
{
TableName = "Orders",
IndexName = "PendingOrdersByDate",
KeyConditionExpression = "PendingStatus = :p",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":p", new AttributeValue { S = "PENDING" } }
}
};
var pendingResult = await client.QueryAsync(pendingQuery);
Import-Module AWS.Tools.DynamoDBv2
# ── Запис нового PENDING замовлення ──────────────────────────────────────────
$item = @{
CustomerId = New-DDBEntry -S 'cust-001'
OrderDateId = New-DDBEntry -S '2025-06-01T10:00#ord-XYZ'
OrderId = New-DDBEntry -S 'ord-XYZ'
Status = New-DDBEntry -S 'PENDING'
PendingStatus = New-DDBEntry -S 'PENDING'
TotalAmount = New-DDBEntry -N '149.99'
ProductId_0 = New-DDBEntry -S 'prod-laptop'
}
Set-DDBItem -TableName Orders -Item $item -Region eu-central-1
# ── Оновлення статусу замовлення та видалення PendingStatus ──────────────────
Update-DDBItem `
-TableName Orders `
-Key @{ CustomerId = New-DDBEntry -S 'cust-001'; OrderDateId = New-DDBEntry -S '2025-06-01T10:00#ord-XYZ' } `
-UpdateExpression "SET #s = :paid REMOVE PendingStatus" `
-ExpressionAttributeName @{ '#s' = 'Status' } `
-ExpressionAttributeValue @{ ':paid' = New-DDBEntry -S 'PAID' } `
-Region eu-central-1
# ── Запит active PENDING замовлень через Sparse GSI ──────────────────────────
$queryReq = [Amazon.DynamoDBv2.Model.QueryRequest]@{
TableName = 'Orders'
IndexName = 'PendingOrdersByDate'
KeyConditionExpression = 'PendingStatus = :p'
ExpressionAttributeValues = @{ ':p' = New-DDBEntry -S 'PENDING' }
}
(Invoke-DDBQuery -QueryRequest $queryReq -Region eu-central-1).Items
{
"Items": [
{
"PendingStatus": { "S": "PENDING" },
"OrderDateId": { "S": "2025-06-01T10:00#ord-XYZ" },
"CustomerId": { "S": "cust-001" },
"TotalAmount": { "S": "149.99" }
}
],
"Count": 1
}
Частина 3: Режими керування пропускною здатністю (Capacity Modes)
Одним із найбільш критичних архітектурних рішень при проектуванні систем на базі Amazon DynamoDB є вибір режиму керування обчислювальною ємністю. Цей параметр визначає не лише фінансову модель використання СУБД, а й механізми виділення фізичних ресурсів на рівні партицій, поведінку системи при непередбачуваних піках навантаження та стратегії автоматичного масштабування. DynamoDB пропонує два фундаментально різні підходи: Provisioned Mode (режим резервування пропускної здатності) та On-Demand Mode (режим оплати за фактичні запити).
Provisioned Mode — резервування обчислювальних ресурсів
У режимі Provisioned архітектор заздалегідь визначає обсяг обчислювальної потужності таблиці, виражений в одиницях пропускної здатності читання (RCU) та запису (WCU). AWS резервує ці ресурси на фізичному рівні (виділяючи відповідну кількість партицій та обчислювальних потужностей на вузлах збереження даних) та підтримує їх у стані постійної готовності. Оплата нараховується за кожну годину резервування, незалежно від того, чи виконувалися запити до таблиці.
Математична модель тарифікації (на прикладі регіону eu-central-1):
Вартість резервування розраховується за наступними базовими ставками:
- 1 RCU: ≈ 0.00013 USD за годину (що становить приблизно 0.095 USD на місяць).
- 1 WCU: ≈ 0.00065 USD за годину (що становить приблизно 0.474 USD на місяць).
При резервуванні 100 WCU та 200 RCU щомісячна вартість утримання таблиці (без врахування вартості збереження даних) становитиме:
Механізм обмеження пропускної здатності (Throttling)
Якщо обсяг вхідних запитів за секунду перевищує сумарно зарезервовану ємність, DynamoDB ініціює механізм захисту ресурсів, повертаючи клієнту виняток ProvisionedThroughputExceededException (HTTP статус 400).
Під капотом цей процес працює так:
- Генерація винятку: Запит відхиляється на рівні Request Router ще до моменту виконання операції на вузлі партиції, що запобігає перевантаженню сховища.
- Клієнтська обробка (Retry & Backoff): Офіційні клієнтські бібліотеки AWS SDK містять вбудовані обробники цього винятку. Вони автоматично повторюють запит, використовуючи алгоритм експоненціальної затримки з додаванням випадкового шуму (Exponential Backoff з Full Jitter). Це запобігає ефекту "лавини ретраїв" (retry storm), коли клієнти синхронно перевантажують базу даних повторними запитами.
- Вплив на затримку (Latency): Незважаючи на автоматичне відновлення, повторні спроби збільшують загальний час відповіді системи (Round Trip Time, RTT) для кінцевого користувача.
Механізм Burst Capacity (Імпульсна ємність)
Для згладжування короткочасних стрибків трафіку DynamoDB використовує алгоритм маркерного кошика (Token Bucket) під назвою Burst Capacity.
- Накопичення ресурсів: Якщо реальне споживання пропускної здатності таблиці є нижчим за ліміт (u(t) < C, де C — зарезервована ємність), невикористані одиниці ємності акумулюються в спеціальному пулі (кошику).
- Обмеження об'єму кошика: Максимальний об'єм накопичених токенів обмежений часовим інтервалом у 5 хвилин (300 секунд). Формально, максимальний запас токенів T_max дорівнює:
- Використання імпульсу: При різкому стрибку навантаження, що перевищує C, Request Router починає списувати токени з пулу Burst Capacity, дозволяючи додатку виконувати запити без помилок throttling.
- Обмеження гарантій: Burst Capacity є сервісом з рівнем доступності Best Effort. AWS не гарантує надання накопиченої ємності, якщо запити концентруються на одній фізичній партиції (викликаючи Hot Partition) або якщо фізичний вузол, на якому розміщено партицію, перевантажений іншими клієнтами (noisy neighbor effect).
Динамічне масштабування (Auto Scaling) у Provisioned Mode
Для мінімізації ручного керування та запобігання надлишковим витратам режим Provisioned інтегрується з сервісом AWS Application Auto Scaling. Цей механізм автоматично коригує зарезервовану ємність таблиці у відповідь на динаміку реального трафіку.
Архітектура взаємодії компонентів:
- Моніторинг метрик: DynamoDB кожну хвилину надсилає метрики використання ємності (
ConsumedReadCapacityUnits,ConsumedWriteCapacityUnits) до Amazon CloudWatch. - CloudWatch Alarms: На основі конфігурації Auto Scaling створюються два триггери (Alarms): один для масштабування вгору (Scale-out), інший — для масштабування вниз (Scale-in). Вони активуються, коли середнє споживання ємності за певний проміжок часу відхиляється від встановленого цільового показника (Target Utilization, зазвичай 70%).
- Application Auto Scaling: При спрацьовуванні CloudWatch Alarm сервіс Auto Scaling викликає API-метод DynamoDB
UpdateTableдля зміни параметрівProvisionedThroughput.
Ковзні інтервали та запобігання осциляції (Flapping)
- Scale-out (Масштабування вгору): Виконується максимально оперативно. Коли споживання перевищує цільове значення протягом кількох хвилин, ліміти збільшуються. Параметр Scale-out cooldown зазвичай встановлюється в 0 секунд, дозволяючи миттєву реакцію на зростання трафіку.
- Scale-in (Масштабування вниз): Здійснюється консервативно. Зменшення лімітів ініціюється лише тоді, коли навантаження стабільно тримається нижче цільового рівня протягом тривалого часу. Параметр Scale-in cooldown за замовчуванням дорівнює 900 секундам (15 хвилинам). Це запобігає ефекту "флаппінгу" (flapping) — постійного перемикання ємності туди-сюди при короткочасних спадах трафіку, що могло б призвести до штучного дефіциту ресурсів при повторному стрибку.
On-Demand Mode — оплата за фактично оброблені запити
Режим On-Demand повністю абстрагує поняття планування ємності. Замість резервування віртуальних каналів зв'язку, користувач оплачує кожен успішно виконаний запит читання чи запису, виражений в Request Read Units (RRU) та Request Write Units (RWU). Розмірність та правила округлення для RRU/RWU є повністю аналогічними до RCU/WCU (читання блоками по 4 KB, запис блоками по 1 KB).
Вартість використання On-Demand (на прикладі регіону eu-central-1):
- 1 мільйон RRU: ≈ 0.284 USD
- 1 мільйон RWU: ≈ 1.42 USD
Алгоритм автоматичного масштабування під капом
On-Demand не потребує налаштування тригерів масштабування. Проте, важливо розуміти фізичні обмеження масштабованості:
- Історичний пік (Peak Capacity): DynamoDB гарантує миттєве обслуговування трафіку, який не перевищує подвоєне значення максимального навантаження за останні 30 хвилин. Наприклад, якщо таблиця успішно обробила пік у 10 000 RPS, вона може безпосередньо з цього моменту обробити до 20 000 RPS.
- Поділ партицій при перевищенні піку: Якщо навантаження перевищує 2 × Peak, DynamoDB запускає фоновий процес поділу партицій (Partition Splitting) для горизонтального розширення сховища та обчислювальних ресурсів. Під час цього процесу (який може тривати до кількох хвилин) запити, що виходять за межі подвоєного піку, можуть зазнавати тимчасового throttling.
- Стартові ліміти: Для абсолютно нової таблиці в режимі On-Demand встановлено ліміти у 4000 Write Requests/sec та 12 000 Read Requests/sec. Ці обмеження автоматично піднімаються в міру зростання реального трафіку.
Порівняльна характеристика режимів ємності
| Критерій порівняння | Provisioned Mode (+ Auto Scaling) | On-Demand Mode |
|---|---|---|
| Характер тарифікації | Погодинна плата за зарезервовану ємність | Плата за фактичну кількість виконаних запитів |
| Реакція на різкі стрибки | З затримкою 5–15 хв (поки відпрацює Auto Scaling) | Миттєва (у межах подвоєного історичного піку) |
| Фінансова ефективність | Максимальна при стабільному, прогнозованому трафіку | Оптимальна при нерівномірному, непередбачуваному трафіку |
| Ризик виникнення Throttling | Присутній під час різких переходів навантаження | Майже відсутній (окрім екстремальних стрибків) |
| Адміністративні витрати | Потребує аналізу профілю навантаження та налаштування лімітів | Повний "Zero Ops" — не потребує втручання |
| Рекомендовано для | Production-середовищ зі стабільними добовими циклами | MVP, розробки, тестування, систем з імпульсним трафіком |
Стратегія перемикання між режимами
!WARNINGОбмеження на перемикання режимів Зміна режиму з Provisioned на On-Demand (і навпаки) дозволяється не частіше ніж один раз на 24 години.
Стратегія проведення навантажувального тестування (Load Testing):
Якщо планується масштабне тестування системи або очікується маркетинговий реліз (наприклад, Black Friday), використання On-Demand для нової таблиці може викликати throttling через низький стартовий ліміт. Рекомендована стратегія:
- Тимчасово перевести таблицю в режим Provisioned.
- Встановити вручну високі показники ємності (наприклад, 10 000 WCU та 20 000 RCU), що змусить DynamoDB миттєво виділити необхідну кількість фізичних партицій ("прогріти таблицю").
- Провести тестування.
- Повернути таблицю в режим On-Demand (або знизити Provisioned ліміти) для повсякденної експлуатації.
Налаштування режимів ємності через інтерфейси керування
# ── Створення нової таблиці в режимі On-Demand (PAY_PER_REQUEST) ─────────────
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=CustomerId,AttributeType=S \
AttributeName=OrderId,AttributeType=S \
--key-schema \
AttributeName=CustomerId,KeyType=HASH \
AttributeName=OrderId,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--region eu-central-1
# ── Переведення існуючої таблиці в режим Provisioned з базовими лімітами ──────
aws dynamodb update-table \
--table-name Orders \
--billing-mode PROVISIONED \
--provisioned-throughput ReadCapacityUnits=100,WriteCapacityUnits=50 \
--region eu-central-1
# ── Реєстрація таблиці як об'єкта масштабування для WCU в Auto Scaling ───────
aws application-autoscaling register-scalable-target \
--service-namespace dynamodb \
--resource-id "table/Orders" \
--scalable-dimension "dynamodb:table:WriteCapacityUnits" \
--min-capacity 10 \
--max-capacity 500 \
--region eu-central-1
# ── Створення політики відстеження цільового використання (Target Tracking) ──
aws application-autoscaling put-scaling-policy \
--service-namespace dynamodb \
--resource-id "table/Orders" \
--scalable-dimension "dynamodb:table:WriteCapacityUnits" \
--policy-name "Orders-WCU-Scaling-Policy" \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "DynamoDBWriteCapacityUtilization"
}
}' \
--region eu-central-1
# ── Повернення таблиці в режим оплати за фактичні запити (On-Demand) ─────────
aws dynamodb update-table \
--table-name Orders \
--billing-mode PAY_PER_REQUEST \
--region eu-central-1
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.ApplicationAutoScaling;
using Amazon.ApplicationAutoScaling.Model;
var ddbClient = new AmazonDynamoDBClient(Amazon.RegionEndpoint.EUCentral1);
var scalingClient = new AmazonApplicationAutoScalingClient(Amazon.RegionEndpoint.EUCentral1);
// ── Ініціалізація нової таблиці в режимі On-Demand (PAY_PER_REQUEST) ────────
await ddbClient.CreateTableAsync(new CreateTableRequest
{
TableName = "Orders",
AttributeDefinitions = new List<AttributeDefinition>
{
new() { AttributeName = "CustomerId", AttributeType = ScalarAttributeType.S },
new() { AttributeName = "OrderId", AttributeType = ScalarAttributeType.S }
},
KeySchema = new List<KeySchemaElement>
{
new() { AttributeName = "CustomerId", KeyType = KeyType.HASH },
new() { AttributeName = "OrderId", KeyType = KeyType.RANGE }
},
BillingMode = BillingMode.PAY_PER_REQUEST
});
// ── Переведення таблиці на зарезервовану ємність (Provisioned Mode) ───────────
await ddbClient.UpdateTableAsync(new UpdateTableRequest
{
TableName = "Orders",
BillingMode = BillingMode.PROVISIONED,
ProvisionedThroughput = new ProvisionedThroughput
{
ReadCapacityUnits = 100,
WriteCapacityUnits = 50
}
});
// ── Налаштування масштабованого об'єкта в Application Auto Scaling ───────────
await scalingClient.RegisterScalableTargetAsync(new RegisterScalableTargetRequest
{
ServiceNamespace = ServiceNamespace.Dynamodb,
ResourceId = "table/Orders",
ScalableDimension = ScalableDimension.DynamodbTableWriteCapacityUnits,
MinCapacity = 10,
MaxCapacity = 500
});
// ── Створення політики динамічного масштабування для WCU на рівні 70% ─────────
await scalingClient.PutScalingPolicyAsync(new PutScalingPolicyRequest
{
ServiceNamespace = ServiceNamespace.Dynamodb,
ResourceId = "table/Orders",
ScalableDimension = ScalableDimension.DynamodbTableWriteCapacityUnits,
PolicyName = "Orders-WCU-AutoScaling-Policy",
PolicyType = PolicyType.TargetTrackingScaling,
TargetTrackingScalingPolicyConfiguration = new TargetTrackingScalingPolicyConfiguration
{
TargetValue = 70.0,
PredefinedMetricSpecification = new PredefinedMetricSpecification
{
PredefinedMetricType = MetricType.DynamoDBWriteCapacityUtilization
}
}
});
// ── Повернення таблиці до конфігурації On-Demand ──────────────────────────────
await ddbClient.UpdateTableAsync(new UpdateTableRequest
{
TableName = "Orders",
BillingMode = BillingMode.PAY_PER_REQUEST
});
Import-Module AWS.Tools.DynamoDBv2
Import-Module AWS.Tools.ApplicationAutoScaling
# ── Створення таблиці в режимі On-Demand ──────────────────────────────────────
$attrDefs = @(
New-DDBAttributeDefinition -AttributeName CustomerId -AttributeType S
New-DDBAttributeDefinition -AttributeName OrderId -AttributeType S
)
$keySchema = @(
New-DDBKeySchemaElement -AttributeName CustomerId -KeyType HASH
New-DDBKeySchemaElement -AttributeName OrderId -KeyType RANGE
)
New-DDBTable `
-TableName Orders `
-AttributeDefinition $attrDefs `
-KeySchema $keySchema `
-BillingMode PAY_PER_REQUEST `
-Region eu-central-1
# ── Переведення таблиці на зарезервовану ємність (Provisioned) ────────────────
$throughput = [Amazon.DynamoDBv2.Model.ProvisionedThroughput]@{
ReadCapacityUnits = 100
WriteCapacityUnits = 50
}
Update-DDBTable `
-TableName Orders `
-BillingMode PROVISIONED `
-ProvisionedThroughput $throughput `
-Region eu-central-1
# ── Реєстрація цілі масштабування для WCU через Auto Scaling ─────────────────
Add-AASScalableTarget `
-ServiceNamespace dynamodb `
-ResourceId 'table/Orders' `
-ScalableDimension 'dynamodb:table:WriteCapacityUnits' `
-MinCapacity 10 `
-MaxCapacity 500 `
-Region eu-central-1
# ── Встановлення цільової політики масштабування (70% утилізації) ────────────
$metricSpec = [Amazon.ApplicationAutoScaling.Model.PredefinedMetricSpecification]@{
PredefinedMetricType = 'DynamoDBWriteCapacityUtilization'
}
$policyConfig = [Amazon.ApplicationAutoScaling.Model.TargetTrackingScalingPolicyConfiguration]@{
TargetValue = 70.0
PredefinedMetricSpecification = $metricSpec
}
Set-AASScalingPolicy `
-ServiceNamespace dynamodb `
-ResourceId 'table/Orders' `
-ScalableDimension 'dynamodb:table:WriteCapacityUnits' `
-PolicyName 'Orders-WCU-AutoScaling-Policy' `
-PolicyType TargetTrackingScaling `
-TargetTrackingScalingPolicyConfiguration $policyConfig `
-Region eu-central-1
# ── Повернення таблиці на On-Demand режим ─────────────────────────────────────
Update-DDBTable `
-TableName Orders `
-BillingMode PAY_PER_REQUEST `
-Region eu-central-1
{
"TableDescription": {
"TableName": "Orders",
"TableStatus": "UPDATING",
"BillingModeSummary": {
"BillingMode": "PROVISIONED",
"LastUpdateToPayPerRequestDateTime": "2026-06-03T10:00:00Z"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 100,
"WriteCapacityUnits": 50
}
}
}
Частина 4: DynamoDB Streams та Транзакції
DynamoDB Streams — архітектура потоків змін
DynamoDB Streams є високодоступним, розподіленим журналом транзакційних логів (Change Data Capture, CDC), який фіксує всі модифікації даних у таблиці в хронологічному порядку. Кожна операція створення (PutItem), оновлення (UpdateItem) чи видалення (DeleteItem) генерує відповідний запис зміни (Stream Record).
Архітектурні принципи та гарантії:
- Шардування (Shards): Потік даних автоматично розділяється на логічні фрагменти — шарди. Кожен шард відповідає певному діапазону ключів партицій і обслуговується власною обчислювальною інфраструктурою. Шарди є ефемерними: при збільшенні навантаження або обсягу даних вони автоматично розгалужуються (split), а при зменшенні — закриваються.
- Впорядкованість записів: DynamoDB гарантує строгу послідовність записів виключно в межах одного шарду. Якщо два записи належать до різних партицій (і відповідно, потрапляють у різні шарди), їх взаємний порядок у потоці не гарантується.
- Життєвий цикл (Data Retention): Всі записи в потоці зберігаються строго протягом 24 годин із моменту створення. Після цього дані видаляються без можливості відновлення.
- Гарантія доставки (Delivery Guarantees): Забезпечується доставка типу At-least-once (щонайменше один раз). Споживачі повинні бути готовими до обробки дублікатів записів (дедуплікація на рівні бізнес-логіки).
View Types — класифікація та оцінка накладних витрат
При активації DynamoDB Streams архітектор зобов'язаний обрати тип представлення даних (View Type), який визначає обсяг інформації, що проектується у кожен запис потоку. Вибір типу безпосередньо впливає на обсяг мережевого трафіку та вартість обробки подій:
KEYS_ONLY
Мережеве навантаження: Мінімальне.
Використання: Оптимально, коли споживачу достатньо знати факт зміни об'єкта, а повний стан за потреби може бути завантажений з основної таблиці через
GetItem (хоча це створює додаткове навантаження RCU).NEW_IMAGE
Мережеве навантаження: Середнє.
Використання: Найбільш поширений варіант для побудови Read-моделей (CQRS), реплікації в Elasticsearch/Redis або відправки сповіщень про створення нових об'єктів.
OLD_IMAGE
Мережеве навантаження: Середнє.
Використання: Використовується в аудиторських системах для логування попереднього стану даних, а також у сценаріях компенсаційних транзакцій (Saga Pattern) для відкату змін.
NEW_AND_OLD_IMAGES
Мережеве навантаження: Максимальне (додаткова серіалізація).
Використання: Необхідний для аналізу дельти (diff) змін конкретних атрибутів (наприклад, визначення факту зміни ціни або статусу замовлення).
Механізм інтеграції AWS Lambda з DynamoDB Streams
Інтеграція реалізується за допомогою сервісу AWS Lambda Event Source Mapping (ESM). Це внутрішній опитувач (Poller), який працює на стороні інфраструктури Lambda, виконуючи періодичні запити до шардів потоку через внутрішній API.
LATEST орієнтується виключно на події, що з'явилися після створення тригера. TRIM_HORIZON починає зчитування з найстаріших доступних у 24-годинному журналі записів.eventName == INSERT), економлячи обчислювальні ресурси.Увімкнення Streams та Lambda тригера
# ── Увімкнути Streams на таблиці ─────────────────────────────────────────
aws dynamodb update-table \
--table-name Orders \
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \
--region eu-central-1
# ── Отримати ARN потоку ───────────────────────────────────────────────────
STREAM_ARN=$(aws dynamodb describe-table \
--table-name Orders \
--query "Table.LatestStreamArn" \
--output text \
--region eu-central-1)
echo "Stream ARN: $STREAM_ARN"
# ── Підключити Lambda до потоку ───────────────────────────────────────────
aws lambda create-event-source-mapping \
--function-name OrderStreamProcessor \
--event-source-arn "$STREAM_ARN" \
--batch-size 100 \
--starting-position LATEST \
--bisect-batch-on-function-error \
--maximum-retry-attempts 3 \
--filter-criteria '{"Filters": [{"Pattern": "{\"eventName\": [\"INSERT\", \"MODIFY\"]}"}]}' \
--region eu-central-1
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda;
using Amazon.Lambda.Model;
var ddbClient = new AmazonDynamoDBClient(Amazon.RegionEndpoint.EUCentral1);
var lambdaClient = new AmazonLambdaClient(Amazon.RegionEndpoint.EUCentral1);
// ── Увімкнути Streams на таблиці ─────────────────────────────────────────
await ddbClient.UpdateTableAsync(new UpdateTableRequest
{
TableName = "Orders",
StreamSpecification = new StreamSpecification
{
StreamEnabled = true,
StreamViewType = StreamViewType.NEW_AND_OLD_IMAGES
}
});
// ── Отримати ARN потоку ───────────────────────────────────────────────────
var tableDescription = await ddbClient.DescribeTableAsync("Orders");
string streamArn = tableDescription.Table.LatestStreamArn;
Console.WriteLine($"Stream ARN: {streamArn}");
// ── Підключити Lambda до потоку ───────────────────────────────────────────
await lambdaClient.CreateEventSourceMappingAsync(new CreateEventSourceMappingRequest
{
FunctionName = "OrderStreamProcessor",
EventSourceArn = streamArn,
BatchSize = 100,
StartingPosition = EventSourcePosition.LATEST,
BisectBatchOnFunctionError = true,
MaximumRetryAttempts = 3,
FilterCriteria = new FilterCriteria
{
Filters = new List<Filter>
{
new() { Pattern = "{\"eventName\": [\"INSERT\", \"MODIFY\"]}" }
}
}
});
Import-Module AWS.Tools.DynamoDBv2
Import-Module AWS.Tools.Lambda
# ── Увімкнути Streams на таблиці ─────────────────────────────────────────
$streamSpec = [Amazon.DynamoDBv2.Model.StreamSpecification]@{
StreamEnabled = $true
StreamViewType = 'NEW_AND_OLD_IMAGES'
}
Update-DDBTable `
-TableName Orders `
-StreamSpecification $streamSpec `
-Region eu-central-1
# ── Отримати ARN потоку ───────────────────────────────────────────────────
$tableDesc = Get-DDBTable -TableName Orders -Region eu-central-1
$streamArn = $tableDesc.LatestStreamArn
Write-Host "Stream ARN: $streamArn"
# ── Підключити Lambda до потоку ───────────────────────────────────────────
$filterPattern = '{"Filters": [{"Pattern": "{\"eventName\": [\"INSERT\", \"MODIFY\"]}"}]}'
$filterCriteria = [Amazon.Lambda.Model.FilterCriteria]@{
Filters = @([Amazon.Lambda.Model.Filter]@{ Pattern = $filterPattern })
}
New-LMEventSourceMapping `
-FunctionName OrderStreamProcessor `
-EventSourceArn $streamArn `
-BatchSize 100 `
-StartingPosition LATEST `
-BisectBatchOnFunctionError $true `
-MaximumRetryAttempt 3 `
-FilterCriteria $filterCriteria `
-Region eu-central-1
Транзакції в DynamoDB — внутрішня механіка та ACID-гарантії
Починаючи з 2018 року, Amazon DynamoDB підтримує виконання транзакційних запитів за допомогою інтерфейсів TransactWriteItems та TransactGetItems. Це дозволяє виконувати атомарні операції над групою до 100 елементів (або сумарним обсягом до 4 MB) в межах однієї транзакції, навіть якщо елементи розташовані в різних фізичних таблицях одного AWS-аккаунта та регіону.
Життєвий цикл транзакції та протокол Two-Phase Commit (2PC)
Під капом DynamoDB використовує адаптований протокол двофазного коміту (Two-Phase Commit, 2PC), який координується внутрішньою інфраструктурою бази даних:
- Фаза підготовки (Prepare Phase):
- Координатор транзакції перевіряє ліміти та права доступу.
- Надсилаються запити на вузли партицій, де зберігаються відповідні елементи.
- На кожному вузлі перевіряються умови
ConditionExpression. Якщо хоча б одна умова не виконується (або елемент заблоковано іншою транзакцією), транзакція негайно скасовується. - Елементи тимчасово блокуються для модифікації іншими конкурентними транзакціями.
- Фаза фіксації (Commit Phase):
- Якщо перша фаза завершилися успішно на всіх вузлах, координатор приймає рішення про фіксацію.
- Зміни застосовуються на фізичному рівні, створюються нові версії елементів і знімаються блокування.
- У разі виникнення помилки на фазі підготовки ініціюється процес відкату (Rollback), і всі проміжні блокування знімаються без внесення змін до бази даних.
Специфікація операцій у TransactWriteItems:
Put: Запис або повна заміна елемента.Update: Модифікація окремих атрибутів існуючого елемента.Delete: Видалення елемента.ConditionCheck: Перевірка стану елемента без його фактичної модифікації (критично для валідації зв'язаних сутностей).
!CAUTIONПодвійна вартість транзакцій (Transaction Cost Penalty) Транзакційні операції споживають вдвічі більше RCU та WCU порівняно зі стандартними запитами. Наприклад, транзакційний запис об'єкта розміром 1 KB споживає 2 WCU (замість 1 WCU), а strongly consistent читання блоку 4 KB споживає 2 RCU (замість 1 RCU).
Практичний сценарій: Транзакційне оформлення замовлення (E-commerce)
При оформленні замовлення додаток повинен гарантувати атомарність трьох дій: перевірити наявність задовільної кількості товару на складі (ConditionCheck), зменшити цю кількість (Update) та безпосередньо створити картку замовлення (Put).
# ── Виконання транзакційного запису TransactWriteItems ──────────────────────
aws dynamodb transact-write-items \
--transact-items '[
{
"ConditionCheck": {
"TableName": "Inventory",
"Key": {"ProductId": {"S": "prod-42"}},
"ConditionExpression": "quantity >= :qty",
"ExpressionAttributeValues": {":qty": {"N": "2"}}
}
},
{
"Update": {
"TableName": "Inventory",
"Key": {"ProductId": {"S": "prod-42"}},
"UpdateExpression": "SET quantity = quantity - :qty",
"ExpressionAttributeValues": {":qty": {"N": "2"}}
}
},
{
"Put": {
"TableName": "Orders",
"Item": {
"CustomerId": {"S": "usr-001"},
"OrderId": {"S": "ord-2026-001"},
"ProductId": {"S": "prod-42"},
"Quantity": {"N": "2"},
"Status": {"S": "PENDING"},
"CreatedAt": {"S": "2026-06-03T12:00:00Z"}
},
"ConditionExpression": "attribute_not_exists(OrderId)"
}
}
]' \
--region eu-central-1
# ── Отримання кількох елементів у транзакційному режимі TransactGetItems ──────
aws dynamodb transact-get-items \
--transact-items '[
{"Get": {"TableName": "Orders", "Key": {"CustomerId": {"S": "usr-001"}, "OrderId": {"S": "ord-2026-001"}}}},
{"Get": {"TableName": "Inventory", "Key": {"ProductId": {"S": "prod-42"}}}}
]' \
--region eu-central-1
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
var ddbClient = new AmazonDynamoDBClient(Amazon.RegionEndpoint.EUCentral1);
// ── Транзакційний запис: Зняття залишків та створення замовлення ─────────────
await ddbClient.TransactWriteItemsAsync(new TransactWriteItemsRequest
{
TransactItems = new List<TransactWriteItem>
{
new()
{
ConditionCheck = new ConditionCheck
{
TableName = "Inventory",
Key = new Dictionary<string, AttributeValue> { { "ProductId", new AttributeValue { S = "prod-42" } } },
ConditionExpression = "quantity >= :qty",
ExpressionAttributeValues = new Dictionary<string, AttributeValue> { { ":qty", new AttributeValue { N = "2" } } }
}
},
new()
{
Update = new Update
{
TableName = "Inventory",
Key = new Dictionary<string, AttributeValue> { { "ProductId", new AttributeValue { S = "prod-42" } } },
UpdateExpression = "SET quantity = quantity - :qty",
ExpressionAttributeValues = new Dictionary<string, AttributeValue> { { ":qty", new AttributeValue { N = "2" } } }
}
},
new()
{
Put = new Put
{
TableName = "Orders",
Item = new Dictionary<string, AttributeValue>
{
{ "CustomerId", new AttributeValue { S = "usr-001" } },
{ "OrderId", new AttributeValue { S = "ord-2026-001" } },
{ "ProductId", new AttributeValue { S = "prod-42" } },
{ "Quantity", new AttributeValue { N = "2" } },
{ "Status", new AttributeValue { S = "PENDING" } },
{ "CreatedAt", new AttributeValue { S = "2026-06-03T12:00:00Z" } }
},
ConditionExpression = "attribute_not_exists(OrderId)"
}
}
}
});
// ── Транзакційне зчитування для забезпечення узгодженості даних ──────────────
var transactGetResult = await ddbClient.TransactGetItemsAsync(new TransactGetItemsRequest
{
TransactItems = new List<TransactGetItem>
{
new()
{
Get = new Get
{
TableName = "Orders",
Key = new Dictionary<string, AttributeValue>
{
{ "CustomerId", new AttributeValue { S = "usr-001" } },
{ "OrderId", new AttributeValue { S = "ord-2026-001" } }
}
}
},
new()
{
Get = new Get
{
TableName = "Inventory",
Key = new Dictionary<string, AttributeValue> { { "ProductId", new AttributeValue { S = "prod-42" } } }
}
}
}
});
Import-Module AWS.Tools.DynamoDBv2
# ── Опис перевірки залишків товару на складі ─────────────────────────────
$condCheck = [Amazon.DynamoDBv2.Model.ConditionCheck]@{
TableName = 'Inventory'
Key = @{ ProductId = New-DDBEntry -S 'prod-42' }
ConditionExpression = 'quantity >= :qty'
ExpressionAttributeValues = @{ ':qty' = New-DDBEntry -N '2' }
}
# ── Опис операції оновлення складу (зменшення кількості) ─────────────────
$updateInventory = [Amazon.DynamoDBv2.Model.Update]@{
TableName = 'Inventory'
Key = @{ ProductId = New-DDBEntry -S 'prod-42' }
UpdateExpression = 'SET quantity = quantity - :qty'
ExpressionAttributeValues = @{ ':qty' = New-DDBEntry -N '2' }
}
# ── Опис створення запису нового замовлення ──────────────────────────────
$putOrder = [Amazon.DynamoDBv2.Model.Put]@{
TableName = 'Orders'
ConditionExpression = 'attribute_not_exists(OrderId)'
Item = @{
CustomerId = New-DDBEntry -S 'usr-001'
OrderId = New-DDBEntry -S 'ord-2026-001'
ProductId = New-DDBEntry -S 'prod-42'
Quantity = New-DDBEntry -N '2'
Status = New-DDBEntry -S 'PENDING'
CreatedAt = New-DDBEntry -S '2026-06-03T12:00:00Z'
}
}
# ── Об'єднання в єдину транзакційну структуру та виклик API ─────────────────
$txItems = @(
[Amazon.DynamoDBv2.Model.TransactWriteItem]@{ ConditionCheck = $condCheck },
[Amazon.DynamoDBv2.Model.TransactWriteItem]@{ Update = $updateInventory },
[Amazon.DynamoDBv2.Model.TransactWriteItem]@{ Put = $putOrder }
)
Invoke-DDBTransactWrite -TransactItems $txItems -Region eu-central-1
# ── Отримання стану замовлення та складу через TransactGet ────────────────────
$getOrder = [Amazon.DynamoDBv2.Model.Get]@{
TableName = 'Orders'
Key = @{ CustomerId = New-DDBEntry -S 'usr-001'; OrderId = New-DDBEntry -S 'ord-2026-001' }
}
$getInventory = [Amazon.DynamoDBv2.Model.Get]@{
TableName = 'Inventory'
Key = @{ ProductId = New-DDBEntry -S 'prod-42' }
}
$getItems = @(
[Amazon.DynamoDBv2.Model.TransactGetItem]@{ Get = $getOrder },
[Amazon.DynamoDBv2.Model.TransactGetItem]@{ Get = $getInventory }
)
Invoke-DDBTransactGet -TransactItems $getItems -Region eu-central-1
Частина 5: TTL, Global Tables та Best Practices
Time to Live (TTL) — автоматичне очищення застарілих даних
Time to Live (TTL) — це вбудований безкоштовний механізм автоматичного видалення застарілих елементів з таблиці DynamoDB. Для роботи TTL розробник визначає спеціальний атрибут таблиці (наприклад, ExpiresAt), що зберігає часову мітку Unix Timestamp у секундах. Фоновий сервіс DynamoDB постійно сканує таблиці та асинхронно видаляє елементи, у яких термін придатності минув.
Особливості функціонування та архітектурний вплив:
- Економічність: Процес видалення елементів за допомогою TTL є абсолютно безкоштовним — він не споживає WCU (Write Capacity Units) основної таблиці, що дозволяє суттєво економити бюджет на операціях очищення.
- Тимчасовий лаг видалення: Видалення за допомогою TTL відбувається асинхронно. AWS гарантує очищення елемента протягом 48 годин після настання вказаного часу. У цей проміжок часу прострочений елемент все ще може відображатися в таблиці.
!IMPORTANT Оскільки видалення не є миттєвим, клієнтський додаток повинен самостійно фільтрувати expired елементи в бізнес-сценаріях. Завжди додавайте у вирази фільтрації (FilterExpression) або бізнес-логіку умову:
ExpiresAt > :currentTimestamp - Інтеграція з DynamoDB Streams: Коли TTL видаляє елемент, ця подія реєструється в потоці змін Streams як подія
REMOVE. Для ідентифікації того, що запис видалено саме сервісом TTL, а не користувачем, метадані події в Streams містять прапорецьuserIdentity.type = 'Service'таuserIdentity.principalId = 'dynamodb.amazonaws.com'.
Практичне налаштування та робота з TTL
# ── Активація TTL на таблиці UserSessions за атрибутом ExpiresAt ─────────────
aws dynamodb update-time-to-live \
--table-name UserSessions \
--time-to-live-specification Enabled=true,AttributeName=ExpiresAt \
--region eu-central-1
# ── Запис сесії користувача з терміном дії 8 годин від поточного часу ──────────
EXPIRES=$(date -d "+8 hours" +%s 2>/dev/null || date -v+8H +%s)
aws dynamodb put-item \
--table-name UserSessions \
--item '{
"UserId": {"S": "usr-001"},
"SessionId": {"S": "sess-new-001"},
"CreatedAt": {"S": "2026-06-03T12:00:00Z"},
"ExpiresAt": {"N": "'"$EXPIRES"'"},
"IsActive": {"BOOL": true}
}' \
--region eu-central-1
# ── Перевірка конфігурації TTL таблиці ──────────────────────────────────────
aws dynamodb describe-time-to-live \
--table-name UserSessions \
--region eu-central-1
# ── Сканування таблиці з виключенням застарілих елементів ─────────────────────
aws dynamodb scan \
--table-name UserSessions \
--filter-expression "ExpiresAt > :now" \
--expression-attribute-values "{\":now\": {\"N\": \"$(date +%s)\"}}" \
--region eu-central-1
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
var ddbClient = new AmazonDynamoDBClient(Amazon.RegionEndpoint.EUCentral1);
// ── Активація TTL на таблиці UserSessions за атрибутом ExpiresAt ─────────────
await ddbClient.UpdateTimeToLiveAsync(new UpdateTimeToLiveRequest
{
TableName = "UserSessions",
TimeToLiveSpecification = new TimeToLiveSpecification
{
Enabled = true,
AttributeName = "ExpiresAt"
}
});
// ── Запис елемента з часом життя 8 годин ──────────────────────────────────
long expiresAt = DateTimeOffset.UtcNow.AddHours(8).ToUnixTimeSeconds();
await ddbClient.PutItemAsync(new PutItemRequest
{
TableName = "UserSessions",
Item = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = "usr-001" } },
{ "SessionId", new AttributeValue { S = "sess-new-001" } },
{ "CreatedAt", new AttributeValue { S = "2026-06-03T12:00:00Z" } },
{ "ExpiresAt", new AttributeValue { N = expiresAt.ToString() } },
{ "IsActive", new AttributeValue { BOOL = true } }
}
});
// ── Перевірка конфігурації TTL ──────────────────────────────────────────────
var ttlDescription = await ddbClient.DescribeTimeToLiveAsync(new DescribeTimeToLiveRequest
{
TableName = "UserSessions"
});
// ── Зчитування даних із фільтрацією прострочених сесій ─────────────────────────
long nowTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var scanResult = await ddbClient.ScanAsync(new ScanRequest
{
TableName = "UserSessions",
FilterExpression = "ExpiresAt > :now",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":now", new AttributeValue { N = nowTs.ToString() } }
}
});
Import-Module AWS.Tools.DynamoDBv2
# ── Активація TTL на таблиці ──────────────────────────────────────────────────
$ttlSpec = [Amazon.DynamoDBv2.Model.TimeToLiveSpecification]@{
Enabled = $true
AttributeName = 'ExpiresAt'
}
Update-DDBTimeToLive -TableName UserSessions -TimeToLiveSpecification $ttlSpec -Region eu-central-1
# ── Запис сесії з часом життя 8 годин ──────────────────────────────────────────
$expiresAt = [int][double]::Parse(
(Get-Date).AddHours(8).ToUniversalTime().Subtract([datetime]'1970-01-01').TotalSeconds
)
$item = @{
UserId = New-DDBEntry -S 'usr-001'
SessionId = New-DDBEntry -S 'sess-new-001'
CreatedAt = New-DDBEntry -S '2026-06-03T12:00:00Z'
ExpiresAt = New-DDBEntry -N "$expiresAt"
IsActive = New-DDBEntry -BOOL $true
}
Set-DDBItem -TableName UserSessions -Item $item -Region eu-central-1
# ── Запит активних сесій з фільтрацією за часом ──────────────────────────────
$nowTs = [int][double]::Parse(
(Get-Date).ToUniversalTime().Subtract([datetime]'1970-01-01').TotalSeconds
)
$scanRequest = [Amazon.DynamoDBv2.Model.ScanRequest]@{
TableName = 'UserSessions'
FilterExpression = 'ExpiresAt > :now'
ExpressionAttributeValues = @{ ':now' = New-DDBEntry -N "$nowTs" }
}
Invoke-DDBScan -ScanRequest $scanRequest -Region eu-central-1
Global Tables — мультирегіональна реплікація Active-Active
DynamoDB Global Tables — це повністю кероване рішення для забезпечення географічного розподілу та стійкості систем до відмови на рівні цілих регіонів AWS. Воно реалізує двонаправлену мультиактивну (Active-Active) реплікацію даних між обраними регіонами.
Архітектурні засади та гарантії:
- Асинхронна реплікація: Запис у будь-яку копію таблиці (replica) в одному регіоні автоматично реплікується в усі інші підключені регіони. Час реплікації зазвичай не перевищує однієї секунди.
- Конфлікти та Last Writer Wins (LWW): Оскільки запис може відбуватися одночасно в різних регіонах, при конфлікті оновлення одного елемента DynamoDB застосовує стратегію вирішення конфліктів Last Writer Wins на основі внутрішніх таймстемпів операцій.
- Технічні передумови:
- Для створення Global Tables на таблиці обов'язково повинен бути увімкнений потік змін DynamoDB Streams із представленням
NEW_AND_OLD_IMAGES. - Режими ємності RCU/WCU повинні бути ідентично налаштовані в усіх регіонах реплікації.
!WARNING Реплікація даних споживає WCU у кожному цільовому регіоні. Якщо ви виконуєте 10 записів за секунду в регіоні eu-central-1, і у вас налаштована реплікація в us-east-1, ці записи спишуть відповідну кількість WCU в обох регіонах.
- Для створення Global Tables на таблиці обов'язково повинен бути увімкнений потік змін DynamoDB Streams із представленням
Налаштування Global Tables
# ── Крок 1: Створення базової таблиці з увімкненим потоком Streams ───────────
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=CustomerId,AttributeType=S \
AttributeName=OrderId,AttributeType=S \
--key-schema \
AttributeName=CustomerId,KeyType=HASH \
AttributeName=OrderId,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \
--region eu-central-1
# ── Крок 2: Додавання реплік у регіони us-east-1 та ap-southeast-1 ───────────
aws dynamodb update-table \
--table-name Orders \
--replica-updates '[
{"Create": {"RegionName": "us-east-1"}},
{"Create": {"RegionName": "ap-southeast-1"}}
]' \
--region eu-central-1
# ── Отримання статусу реплікації ─────────────────────────────────────────────
aws dynamodb describe-table \
--table-name Orders \
--query "Table.Replicas" \
--region eu-central-1
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
var ddbClient = new AmazonDynamoDBClient(Amazon.RegionEndpoint.EUCentral1);
// ── Крок 1: Створення таблиці в eu-central-1 з активацією Streams ───────────
await ddbClient.CreateTableAsync(new CreateTableRequest
{
TableName = "Orders",
AttributeDefinitions = new List<AttributeDefinition>
{
new() { AttributeName = "CustomerId", AttributeType = ScalarAttributeType.S },
new() { AttributeName = "OrderId", AttributeType = ScalarAttributeType.S }
},
KeySchema = new List<KeySchemaElement>
{
new() { AttributeName = "CustomerId", KeyType = KeyType.HASH },
new() { AttributeName = "OrderId", KeyType = KeyType.RANGE }
},
BillingMode = BillingMode.PAY_PER_REQUEST,
StreamSpecification = new StreamSpecification
{
StreamEnabled = true,
StreamViewType = StreamViewType.NEW_AND_OLD_IMAGES
}
});
// ── Крок 2: Додавання реплік у регіони us-east-1 та ap-southeast-1 ───────────
await ddbClient.UpdateTableAsync(new UpdateTableRequest
{
TableName = "Orders",
ReplicaUpdates = new List<ReplicationGroupUpdate>
{
new()
{
Create = new CreateReplicationGroupMemberAction { RegionName = "us-east-1" }
},
new()
{
Create = new CreateReplicationGroupMemberAction { RegionName = "ap-southeast-1" }
}
}
});
// ── Отримання статусу реплікації таблиці ────────────────────────────────────
var tableDesc = await ddbClient.DescribeTableAsync("Orders");
var replicas = tableDesc.Table.Replicas;
Import-Module AWS.Tools.DynamoDBv2
# ── Крок 1: Створення базової таблиці Orders ──────────────────────────────────
$attrDefs = @(
New-DDBAttributeDefinition -AttributeName CustomerId -AttributeType S
New-DDBAttributeDefinition -AttributeName OrderId -AttributeType S
)
$keySchema = @(
New-DDBKeySchemaElement -AttributeName CustomerId -KeyType HASH
New-DDBKeySchemaElement -AttributeName OrderId -KeyType RANGE
)
$streamSpec = [Amazon.DynamoDBv2.Model.StreamSpecification]@{
StreamEnabled = $true
StreamViewType = 'NEW_AND_OLD_IMAGES'
}
New-DDBTable `
-TableName Orders `
-AttributeDefinition $attrDefs `
-KeySchema $keySchema `
-BillingMode PAY_PER_REQUEST `
-StreamSpecification $streamSpec `
-Region eu-central-1
# ── Крок 2: Налаштування реплік у регіони США та Азії ──────────────────────────
$replicaUpdates = @(
[Amazon.DynamoDBv2.Model.ReplicationGroupUpdate]@{
Create = [Amazon.DynamoDBv2.Model.CreateReplicationGroupMemberAction]@{
RegionName = 'us-east-1'
}
},
[Amazon.DynamoDBv2.Model.ReplicationGroupUpdate]@{
Create = [Amazon.DynamoDBv2.Model.CreateReplicationGroupMemberAction]@{
RegionName = 'ap-southeast-1'
}
}
)
Update-DDBTable -TableName Orders -ReplicaUpdates $replicaUpdates -Region eu-central-1
Best Practices: Ефективне проектування схем та запитів
Для забезпечення масштабованості, передбачуваності затримок (Latency) та мінімізації фінансових витрат при роботі з Amazon DynamoDB розробник повинен дотримуватися наступних кращих практик:
1. Уникнення проблеми гарячих партицій (Hot Partitions)
Якщо значення Partition Key розподілені нерівномірно (наприклад, більшість запитів надсилається до одного ключа Status = PENDING), вся ємність RCU/WCU для конкретної фізичної партиції може бути вичерпана, що викличе Throttling для інших клієнтів.
Рішення — Розподіл записів за допомогою солі (Write Sharding / Salting): До Partition Key при записі додається випадковий суфікс (сіль) із фіксованого діапазону:
# Запис: PK = "STATUS#PENDING#" + Random(1, N)
# Приклад: "STATUS#PENDING#3", "STATUS#PENDING#12"
При читанні додаток виконує N паралельних запитів (Query) для кожного можливого суфікса та об'єднує результати в пам'яті.
2. Query проти Scan
- Scan: Повністю зчитує всю таблицю, проходячи по всіх фізичних партиціях. Це операція зі складністю O(N). Вона споживає величезну кількість RCU, блокує інші запити і повинна використовуватися виключно у фонових аналітичних задачах.
- Query: Виконує локальний пошук у межах конкретної партиції за Partition Key (складність O(1) або O(log N)). Це найбільш ефективний спосіб читання даних, який мінімізує витрати RCU.
3. Оптимістичне блокування за допомогою Conditional Writes
Оскільки DynamoDB не підтримує класичні блокування рядків на час транзакції, для запобігання перезапису конкурентними процесами використовується оптимістичне блокування (Optimistic Locking) через атрибут версійності та ConditionExpression.
# Оновлення запису продукту лише якщо його версія збігається з очікуваною
aws dynamodb update-item \
--table-name Products \
--key '{"ProductId": {"S": "prod-42"}}' \
--update-expression "SET price = :newPrice, version = :newVer" \
--condition-expression "version = :expectedVer" \
--expression-attribute-values '{
":newPrice": {"N": "89.99"},
":newVer": {"N": "3"},
":expectedVer": {"N": "2"}
}' \
--region eu-central-1
Якщо інший потік змінив запис першим (версія стала 3), операція поверне ConditionalCheckFailedException, сигналізуючи про необхідність повторного зчитування та спроби оновлення.
Частина 6: Інтеграція з .NET (AWS SDK)
Офіційне AWS SDK для .NET (AWSSDK.DynamoDBv2) є основним інструментом для взаємодії з Amazon DynamoDB у середовищі C# та .NET. Воно розроблене з урахуванням високої продуктивності, асинхронності та підтримує різноманітні рівні абстракції, дозволяючи розробникам балансувати між швидкістю розробки та низькорівневим контролем над мережевим трафіком і структурами даних.
У цій частині ми детально вивчимо всі складові частини SDK, починаючи від інсталяції та життєвого циклу клієнтів у контейнері DI, завершуючи складними транзакціями та обробкою потоків змін у безсерверних функціях AWS Lambda.
1. Архітектурні рівні AWS SDK для .NET
AWS SDK надає три взаємодоповнюючі моделі програмування. Кожна з них орієнтована на конкретні сценарії використання:
Low-Level API
AmazonDynamoDBClientСуть: Найближчий до HTTP-протоколу рівень. Усі типу даних та структури передаються у вигляді сирих словників
Dictionary<string, AttributeValue>.Переваги: Повний контроль, нульовий оверхед, доступ до 100% можливостей API (транзакції, batch-операції, тюнінг запитів).
Document Model
Table + DocumentСуть: Проміжний безсхемний (schemaless) рівень. Замість явного використання
AttributeValue робота відбувається з динамічними об'єктами Document, що спрощує зчитування та запис.Переваги: Швидка JSON-серіалізація, легка робота з динамічними даними без опису C# класів.
Object Persistence Model
DynamoDBContextСуть: Декларативний ORM-подібний рівень. SDK автоматично перетворює POCO (Plain Old CLR Objects) на записи таблиць на основі атрибутів класу.
Переваги: Декларативне проектування, мінімум коду, висока типізація та безпека типів на рівні компіляції.
Порівняльна матриця рівнів абстракції
| Характеристика | Low-Level API | Document Model | Object Persistence Model (OPM) |
|---|---|---|---|
| Швидкість розробки | Низька (багато бойлерплейту) | Середня | Висока (декларативний підхід) |
| Продуктивність | Максимальна (без додаткового мапінгу) | Висока | Злегка знижена через рефлексію |
| Типізація | Словники та AttributeValue | Динамічні типи (Document, Primitive) | Строга типізація (POCO класи) |
| Контроль запитів | Повний контроль над схемами та фільтрами | Частковий контроль | Обмежений абстракціями контексту |
| Транзакції | Підтримуються повністю | Не підтримуються напряму | Підтримуються обмежено |
| Ідеально для... | Системних утиліт, складних запитів | JSON API, динамічних схем | Бізнес-сутностей та CRUD-сервісів |
2. Встановлення, конфігурація та Dependency Injection
Перед початком роботи необхідно підключити відповідні інструменти з екосистеми NuGet.
Крок 1: Встановлення NuGet-пакетів
Для роботи з клієнтом та DI у сучасних .NET 8+ додатках потрібні такі пакети:
# Основна бібліотека клієнта DynamoDB
dotnet add package AWSSDK.DynamoDBv2
# Розширення для інтеграції з .NET Core Dependency Injection
dotnet add package AWSSDK.Extensions.NETCore.Setup
Крок 2: Налаштування файлу конфігурації appsettings.json
Параметри доступу до хмари AWS зазвичай зберігаються у конфігураційному файлі. Розділ "AWS" зчитується автоматично розширенням SDK:
{
"AWS": {
"Region": "eu-central-1",
"Profile": "default"
}
}
!NOTE У хмарному середовищі AWS (наприклад, у контейнерах ECS/EKS або Lambda-функціях) вказувати
Profileне потрібно. SDK автоматично отримає тимчасові Credentials через механізм IAM Roles for Tasks або Execution Role.
Крок 3: Реєстрація у контейнері Dependency Injection (DI)
Для правильного керування життєвим циклом підключень клієнтські сервіси реєструються у Program.cs:
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
var builder = WebApplication.CreateBuilder(args);
// 1. Зчитування та реєстрація опцій AWS
var awsOptions = builder.Configuration.GetAWSOptions();
builder.Services.AddDefaultAWSOptions(awsOptions);
// 2. Реєстрація IAmazonDynamoDB (Singleton за замовчуванням)
builder.Services.AddAWSService<IAmazonDynamoDB>();
// 3. Реєстрація IDynamoDBContext (ORM контекст - зазвичай Scoped)
builder.Services.AddScoped<IDynamoDBContext, DynamoDBContext>();
!IMPORTANT Клієнт
AmazonDynamoDBClientреалізуєIDisposableта утримує внутрішній пул з'єднань (HttpClient). Його обов'язково потрібно реєструвати як Singleton, оскільки створення нового клієнта на кожен запит призведе до вичерпання TCP-сокетів під навантаженням (Socket Exhaustion). КонтекстDynamoDBContextє легкою обгорткою над клієнтом і реєструється як Scoped або Transient.
Крок 4: Локальна розробка та перевизначення ServiceURL
При використанні локального емулятора (наприклад, LocalStack або DynamoDB Local) необхідно обійти стандартну маршрутизацію AWS:
if (builder.Environment.IsDevelopment())
{
builder.Services.AddSingleton<IAmazonDynamoDB>(sp =>
{
var config = new AmazonDynamoDBConfig
{
ServiceURL = "http://localhost:8000", // Стандартний порт DynamoDB Local
UseHttp = true
};
return new AmazonDynamoDBClient(config);
});
}
Конфігурація клієнта (AmazonDynamoDBConfig)
Клас AmazonDynamoDBConfig (успадкований від AmazonServiceClientConfig) дозволяє гнучко налаштувати поведінку HTTP-клієнта та правила взаємодії з AWS.
RegionEndpoint.EUCentral1), до якого надсилатимуться запити, якщо не вказано ServiceURL.true, SDK використовуватиме протокол HTTP замість HTTPS (корисно для локального тестування).3. Low-Level API: Робота на рівні байтів та протоколу
Low-Level API надає прямий доступ до викликів JSON-інтерфейсу DynamoDB через клас AmazonDynamoDBClient. Основною структурою даних тут є AttributeValue, яка реалізує паттерн Union Type для опису типів колонок БД.
Ключовий клас: AmazonDynamoDBClient
Це центральний клас низькорівневого доступу, який безпосередньо надсилає HTTP-запити до AWS API. Усі методи є асинхронними та приймають відповідний об'єкт *Request.
GetItemRequest та повертає GetItemResponse.PutItemRequest.UpdateItemRequest.DeleteItemRequest.QueryRequest.ScanRequest.BatchGetItemRequest.BatchWriteItemRequest.TransactGetItemsRequest.TransactWriteItemsRequest.Опис властивостей класу AttributeValue
Клас AttributeValue містить властивості для кожного підтримуваного типу даних DynamoDB. На рівні SDK це виглядає так:
true або false.MemoryStream (наприклад, для серіалізованих protobuf-файлів).CRUD-операції через Low-Level API
Розглянемо приклад реалізації записів, зчитування та оновлення через прямі HTTP-запити:
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
public class LowLevelRepository
{
private readonly IAmazonDynamoDB _client;
private const string TableName = "Products";
public LowLevelRepository(IAmazonDynamoDB client) => _client = client;
// ── PutItem: Запис нового елемента ──────────────────────────────────────────
public async Task CreateProductAsync(string categoryId, string sku, string name, decimal price)
{
var request = new PutItemRequest
{
TableName = TableName,
Item = new Dictionary<string, AttributeValue>
{
{ "CategoryId", new AttributeValue { S = categoryId } },
{ "Sku", new AttributeValue { S = sku } },
{ "Name", new AttributeValue { S = name } },
{ "Price", new AttributeValue { N = price.ToString("F2") } },
{ "IsAvailable", new AttributeValue { BOOL = true } }
}
};
await _client.PutItemAsync(request);
}
// ── GetItem: Точкове зчитування елемента за HASH та RANGE ─────────────────────
public async Task<Dictionary<string, AttributeValue>?> GetProductAsync(string categoryId, string sku)
{
var request = new GetItemRequest
{
TableName = TableName,
Key = new Dictionary<string, AttributeValue>
{
{ "CategoryId", new AttributeValue { S = categoryId } },
{ "Sku", new AttributeValue { S = sku } }
},
ConsistentRead = true // Strongly Consistent Read
};
var response = await _client.GetItemAsync(request);
return response.IsItemSet ? response.Item : null;
}
// ── UpdateItem: Оновлення окремого атрибута за виразом (Update Expression) ───
public async Task UpdatePriceAsync(string categoryId, string sku, decimal newPrice)
{
var request = new UpdateItemRequest
{
TableName = TableName,
Key = new Dictionary<string, AttributeValue>
{
{ "CategoryId", new AttributeValue { S = categoryId } },
{ "Sku", new AttributeValue { S = sku } }
},
// Вираз оновлення: встановлюємо нову ціну та дату
UpdateExpression = "SET Price = :newPrice, UpdatedAt = :ts",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":newPrice", new AttributeValue { N = newPrice.ToString("F2") } },
{ ":ts", new AttributeValue { S = DateTime.UtcNow.ToString("O") } }
}
};
await _client.UpdateItemAsync(request);
}
}
Використання виразів (Expressions) у запитах
При читанні великих обсягів даних за допомогою QueryRequest або ScanRequest важливо використовувати вирази для мінімізації споживання RCU та фільтрації на боці сервера:
- KeyConditionExpression: Визначає критерії відбору за первинним ключем (обов'язково вказувати HASH-ключ).
- FilterExpression: Додатковий фільтр, який застосовується до знайдених даних після зчитування з диска, але перед поверненням клієнту (не економить RCU, але зменшує мережевий трафік).
- ProjectionExpression: Перелік атрибутів, які необхідно повернути (аналог
SELECT column1, column2). Економить пропускну здатність мережі. - ExpressionAttributeNames (словник з
#): Використовується для екранування зарезервованих слів DynamoDB (таких якStatus,Name,User,Timestamp). - ExpressionAttributeValues (словник з
:): Передає значення параметрів у вирази.
Приклад побудови запиту із зарезервованими іменами атрибутів:
var queryRequest = new QueryRequest
{
TableName = "Products",
KeyConditionExpression = "CategoryId = :catId AND Sku >= :minSku",
FilterExpression = "#status = :statusVal AND Price < :maxPrice",
ProjectionExpression = "Sku, #nameAttr, Price",
ExpressionAttributeNames = new Dictionary<string, string>
{
{ "#status", "Status" }, // Зарезервоване слово
{ "#nameAttr", "Name" } // Зарезервоване слово
},
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":catId", new AttributeValue { S = "Electronics" } },
{ ":minSku", new AttributeValue { S = "SKU-1000" } },
{ ":statusVal", new AttributeValue { S = "ACTIVE" } },
{ ":maxPrice", new AttributeValue { N = "500.00" } }
}
};
var response = await _client.QueryAsync(queryRequest);
4. Document Model: Робота з неструктурованими документами
Document Model — це проміжний рівень абстракції, який надає класи Table та Document для зручної роботи зі схемами без необхідності опису об'єктів AttributeValue.
Ключові класи моделі документів
Для роботи з даними використовуються три основні класи:
Table.LoadTable(client, tableName).Основні методи класу Table
Document за первинним ключем.Document у таблиці.Search.Search.Допоміжні методи класу Document
Document з валідного JSON-рядка.Document у JSON-рядок.AsString(), AsInt(), AsDecimal(), AsBoolean(), AsDouble().Методи класу Search (Пагінація)
true, якщо в БД більше немає сторінок для завантаження.CRUD-операції в моделі документів
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DocumentModel;
public class DocumentModelRepository
{
private readonly Table _table;
public DocumentModelRepository(IAmazonDynamoDB client)
{
// Завантаження метаданих таблиці при ініціалізації
_table = Table.LoadTable(client, "Products");
}
public async Task PutDocumentAsync(string categoryId, string sku, string name, decimal price, List<string> tags)
{
var productDoc = new Document();
productDoc["CategoryId"] = categoryId; // Автоматична конвертація в AttributeValue (S)
productDoc["Sku"] = sku;
productDoc["Name"] = name;
productDoc["Price"] = price; // Автоматична конвертація в (N)
productDoc["Tags"] = tags; // Автоматична конвертація в String Set (SS)
productDoc["CreatedAt"] = DateTime.UtcNow;
await _table.PutItemAsync(productDoc);
}
public async Task<Document?> GetDocumentAsync(string categoryId, string sku)
{
// Метод LoadItemAsync автоматично будує запит GetItem
return await _table.GetItemAsync(categoryId, sku);
}
}
Інтеграція з JSON-даними
Однією з найсильніших переваг моделі документів є підтримка прямого імпорту та експорту JSON-структур за допомогою методів Document.FromJson та Document.ToJson:
// ── Імпорт: Перетворення JSON-рядка безпосередньо у запис таблиці ─────────────
string rawJson = """
{
"CategoryId": "Books",
"Sku": "BOOK-9921",
"Name": "Clean Architecture",
"Details": {
"Author": "Robert C. Martin",
"Pages": 432
}
}
""";
Document document = Document.FromJson(rawJson);
await _table.PutItemAsync(document);
// ── Експорт: Перетворення зчитаного документа у JSON-рядок ────────────────────
Document fetchedDoc = await _table.GetItemAsync("Books", "BOOK-9921");
string jsonOutput = fetchedDoc.ToJson();
// jsonOutput міститиме стандартний валідний JSON
Виконання запитів через Search Helper
Для ітерації по вибірці даних використовується клас Search, який підтримує ледачу підвантаження результатів сторінками:
var filter = new QueryFilter("CategoryId", QueryOperator.Equal, "Books");
filter.AddCondition("Price", QueryOperator.LessThan, 50.00m);
var search = _table.Query(filter);
while (!search.IsDone)
{
// Отримання наступної сторінки записів (Pagination)
List<Document> page = await search.GetNextSetAsync();
foreach (var doc in page)
{
Console.WriteLine($"{doc["Name"]} - {doc["Price"].AsDecimal()}");
}
}
5. Object Persistence Model: Декларативний ORM
Object Persistence Model представляє найвищий рівень абстракції (ORM). Він використовує об'єкт DynamoDBContext для мапінгу стандартних POCO-класів на таблиці за допомогою системи декларативних C#-атрибутів.
Ключовий клас: DynamoDBContext
Це основний робочий інструмент для ORM-мапінгу. Його інтерфейс IDynamoDBContext надає методи для збереження та завантаження POCO-об'єктів.
T за ключами.T у базі даних (виконує Upsert). Автоматично інкрементує версію сутності, якщо налаштовано [DynamoDBVersion].AsyncSearch<T>.AsyncSearch<T>.T.T за списком первинних ключів.Document у строготипізований POCO-об'єкт T.T у слабкотипізований Document.Локальне перевизначення конфігурації (DynamoDBOperationConfig)
При використанні DynamoDBContext ви можете динамічно змінювати параметри виконання окремих CRUD-операцій за допомогою об'єкта DynamoDBOperationConfig.
Query або Scan.[DynamoDBTable].true, властивості POCO-класу зі значенням null не перезаписуватимуть існуючі значення в БД при використанні SaveAsync.[DynamoDBVersion].Атрибути мапування даних OPM
Для опису сутностей у просторі назв Amazon.DynamoDBv2.DataModel використовуються такі атрибути:
Converter).Проектування POCO-моделі даних
Нижче наведено приклад опису складної моделі даних з підтримкою GSI, оптимістичного блокування та вкладеного об'єкта, серіалізованого через кастомний конвертер:
using Amazon.DynamoDBv2.DataModel;
[DynamoDBTable("Catalog")]
public class CatalogItem
{
[DynamoDBHashKey("CategoryId")] // Первинний HASH
public string CategoryId { get; set; } = string.Empty;
[DynamoDBRangeKey("Sku")] // Первинний RANGE
public string Sku { get; set; } = string.Empty;
[DynamoDBProperty("Title")]
public string Title { get; set; } = string.Empty;
[DynamoDBProperty("Price")]
public decimal Price { get; set; }
// Глобальний індекс: HASH-ключ для індексу "Supplier-index"
[DynamoDBGlobalSecondaryIndexHashKey("Supplier-index", AttributeName = "SupplierId")]
public string SupplierId { get; set; } = string.Empty;
// Автоматичне оптимістичне блокування версії запису
[DynamoDBVersion]
public int? Version { get; set; }
// Вкладений складний об'єкт із кастомним конвертером в JSON String
[DynamoDBProperty("SpecSheet", Converter = typeof(SpecsJsonConverter))]
public TechnicalSpecs Specs { get; set; } = new();
[DynamoDBIgnore]
public string MemoryCache { get; set; } = string.Empty;
}
public class TechnicalSpecs
{
public string Manufacturer { get; set; } = string.Empty;
public string WarrantyMonths { get; set; } = string.Empty;
}
Кастомний конвертер типів (IPropertyConverter)
Для збереження складних об'єктів або списків у вигляді одного атрибута (наприклад, JSON-рядка) необхідно імплементувати інтерфейс IPropertyConverter:
using System.Text.Json;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;
public class SpecsJsonConverter : IPropertyConverter
{
// Серіалізація об'єкта C# у DynamoDBEntry
public DynamoDBEntry ToEntry(object value)
{
if (value is not TechnicalSpecs specs)
return new DynamoDBNull();
string json = JsonSerializer.Serialize(specs);
return new Primitive(json); // Зберігаємо як звичайний рядок
}
// Десеріалізація з DynamoDBEntry в об'єкт C#
public object FromEntry(DynamoDBEntry entry)
{
var primitive = entry as Primitive;
if (primitive == null || primitive.Value is not string json)
return new TechnicalSpecs();
return JsonSerializer.Deserialize<TechnicalSpecs>(json) ?? new TechnicalSpecs();
}
}
Методи інтерфейсу IPropertyConverter
DynamoDBEntry (наприклад, Primitive, Document, DynamoDBNull або DynamoDBList).DynamoDBEntry з бази даних у вихідний тип властивості C#.Реалізація CRUD Репозиторію через OPM
using Amazon.DynamoDBv2.DataModel;
public class DynamoDBCatalogRepository
{
private readonly IDynamoDBContext _context;
public DynamoDBCatalogRepository(IDynamoDBContext context) => _context = context;
// ── Load: Точкове зчитування (GetItem) ──────────────────────────────────────
public async Task<CatalogItem?> GetByIdAsync(string categoryId, string sku)
{
return await _context.LoadAsync<CatalogItem>(categoryId, sku);
}
// ── Save: Збереження або перезапис (Upsert) ─────────────────────────────────
public async Task SaveItemAsync(CatalogItem item)
{
// Метод SaveAsync автоматично аналізує [DynamoDBVersion].
// Якщо запис існує, версія звіриться і збільшиться на 1.
// При конфлікті виникне AmazonDynamoDBException із кодом помилки ConditionalCheckFailedException.
try
{
await _context.SaveAsync(item);
}
catch (AmazonDynamoDBException ex) when (ex.ErrorCode == "ConditionalCheckFailedException")
{
throw new InvalidOperationException("Запис був змінений іншим користувачем. Оновіть дані.", ex);
}
}
// ── Delete: Видалення сутності ──────────────────────────────────────────────
public async Task DeleteItemAsync(string categoryId, string sku)
{
await _context.DeleteAsync<CatalogItem>(categoryId, sku);
}
// ── Query через GSI: Зчитування за альтернативним індексом ───────────────────
public async Task<IEnumerable<CatalogItem>> GetBySupplierAsync(string supplierId)
{
var config = new DynamoDBOperationConfig
{
IndexName = "Supplier-index",
ConsistentRead = false // На GSI допускається лише Eventually Consistent Read
};
// QueryAsync повертає об'єкт AsyncSearch<T>, який виступає ітератором
AsyncSearch<CatalogItem> search = _context.QueryAsync<CatalogItem>(supplierId, config);
// Зчитуємо всі сторінки даних, які залишилися
return await search.GetRemainingAsync();
}
}
6. Атомарні транзакції та пакетна обробка
Для забезпечення максимальної продуктивності та надійності даних SDK надає механізми пакетної роботи та транзакційності на рівні ACID.
Пакетні операції (Batch Operations)
Пакетний запис (BatchWrite) та зчитування (BatchGet) дозволяють об'єднати до 25 операцій запису/видалення та до 100 операцій читання в один HTTP-запит. Це значно знижує RTT (Round Trip Time).
// Пакетний запис сутностей через ORM Context
public async Task SaveBatchAsync(List<CatalogItem> items)
{
var batchWrite = _context.CreateBatchWrite<CatalogItem>();
// Додаємо елементи до пакету
batchWrite.AddPutItems(items);
// Можна комбінувати з видаленням
batchWrite.AddDeleteKey("Books", "BOOK-DELETE-1");
await batchWrite.ExecuteAsync();
}
!WARNING При використанні пакетних запитів DynamoDB може повернути частину елементів як необроблені (
UnprocessedItems/UnprocessedKeys) у випадку вичерпання виділеної WCU/RCU ліміту. Низькорівневий API вимагає ручної обробки та повторних спроб у циклі.IDynamoDBContext.ExecuteAsync()намагається обробити частину ретриів самостійно, але у критичних сценаріях розробник має відслідковувати статус відповіді.
Атомарні ACID Транзакції
Транзакції гарантують повне виконання (All-or-Nothing) до 100 операцій (або 4 МБ об'єму). Вони реалізуються виключно через Low-Level API клієнта AmazonDynamoDBClient.
Розглянемо класичний приклад переказу коштів або замовлення товару з валідацією залишків на складі:
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
public class TransactionService
{
private readonly IAmazonDynamoDB _client;
public TransactionService(IAmazonDynamoDB client) => _client = client;
public async Task ProcessOrderAsync(string customerId, string productId, int qty, decimal price)
{
string orderId = $"ORD-{Guid.NewGuid():N}";
var request = new TransactWriteItemsRequest
{
TransactItems = new List<TransactWriteItem>
{
// 1. Валідація та оновлення залишків на складі (Умова: склад має достатньо товару)
new()
{
Update = new Update
{
TableName = "Warehouse",
Key = new Dictionary<string, AttributeValue>
{
{ "ProductId", new AttributeValue { S = productId } }
},
UpdateExpression = "SET Stock = Stock - :qty",
ConditionExpression = "Stock >= :qty", // Перевірка умови
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{ ":qty", new AttributeValue { N = qty.ToString() } }
}
}
},
// 2. Створення нового запису замовлення (Умова: таке замовлення ще не існує)
new()
{
Put = new Put
{
TableName = "Orders",
ConditionExpression = "attribute_not_exists(OrderId)",
Item = new Dictionary<string, AttributeValue>
{
{ "CustomerId", new AttributeValue { S = customerId } },
{ "OrderId", new AttributeValue { S = orderId } },
{ "Quantity", new AttributeValue { N = qty.ToString() } },
{ "TotalAmount", new AttributeValue { N = (qty * price).ToString("F2") } },
{ "Status", new AttributeValue { S = "PAID" } }
}
}
}
}
};
try
{
await _client.TransactWriteItemsAsync(request);
}
catch (TransactionCanceledException ex)
{
// Обробка причин скасування транзакції
var reasons = ex.CancellationReasons
.Select((r, idx) => $"Елемент {idx}: {r.Code} (Опис: {r.Message})");
throw new InvalidOperationException($"Транзакцію скасовано. Причини:\\n{string.Join("\\n", reasons)}", ex);
}
}
}
7. Обробка подій DynamoDB Streams у .NET Lambda
При активації DynamoDB Streams будь-яка зміна в таблиці записується у потік подій, який може викликати безсерверні функції AWS Lambda для побудови Event-Driven архітектури.
NuGet-пакети для Lambda-інтеграції
У проекті AWS Lambda серіалізація та обробка здійснюється за допомогою пакетів:
dotnet add package Amazon.Lambda.Core
dotnet add package Amazon.Lambda.Serialization.SystemTextJson
dotnet add package Amazon.Lambda.DynamoDBEvents
Приклад реалізації Lambda-обробника (Function.cs)
using Amazon.Lambda.Core;
using Amazon.Lambda.DynamoDBEvents;
using System.Text.Json;
// Налаштування глобального серіалізатора Lambda
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace LambdaHandler;
public class OrderStreamProcessor
{
public async Task FunctionHandlerAsync(DynamoDBEvent dynamoEvent, ILambdaContext context)
{
context.Logger.LogInformation($"Отримано пакет подій. Кількість записів: {dynamoEvent.Records.Count}");
foreach (var record in dynamoEvent.Records)
{
context.Logger.LogInformation($"EventID: {record.EventID} | Назва події: {record.EventName}");
switch (record.EventName)
{
case "INSERT":
// Зчитування нових даних
var newItemMap = record.Dynamodb.NewImage;
string customerId = newItemMap["CustomerId"].S;
string orderId = newItemMap["OrderId"].S;
context.Logger.LogInformation($"[Створення] Замовлення {orderId} додано для клієнта {customerId}");
break;
case "MODIFY":
// Порівняння змін (Old Image проти New Image)
var oldImage = record.Dynamodb.OldImage;
var newImage = record.Dynamodb.NewImage;
if (oldImage.TryGetValue("Status", out var oldStatus) &&
newImage.TryGetValue("Status", out var newStatus))
{
context.Logger.LogInformation($"[Оновлення] Статус замовлення змінено: {oldStatus.S} -> {newStatus.S}");
}
break;
case "REMOVE":
// Перевірка джерела видалення (Користувач чи TTL-сервіс)
bool isTtlDeletion = record.UserIdentity?.Type == "Service" &&
record.UserIdentity?.PrincipalId == "dynamodb.amazonaws.com";
if (isTtlDeletion)
{
context.Logger.LogInformation($"[TTL] Запис автоматично видалено сервісом TTL: {record.Dynamodb.OldImage["OrderId"].S}");
}
else
{
context.Logger.LogInformation($"[Видалення] Користувач видалив запис: {record.Dynamodb.OldImage["OrderId"].S}");
}
break;
}
}
await Task.CompletedTask;
}
}
!IMPORTANT Клас
AttributeValueу бібліотеціAmazon.Lambda.DynamoDBEvents— це інший тип порівняно зAmazon.DynamoDBv2.Model.AttributeValueу AWS SDK. Вони мають ідентичні за назвою властивості (S,N,BOOL), але розташовані в різних збірках. Якщо вам потрібно перетворити карту атрибутів Lambda-події в об'єкти домену, скористайтеся JSON-трансляцією або ручним перемальовуванням полів.
8. Практичні Best Practices для розробників .NET
Для забезпечення стабільної роботи .NET додатків під високим навантаженням дотримуйтеся наступних правил конфігурації клієнта:
- Керування TCP-підключеннями (Connection pooling):
Реєструйте
IAmazonDynamoDBвиключно як Singleton. Під капотом SDK використовуєHttpClient, який автоматично утилізує з'єднання. Налаштувати ліміти з'єднань можна черезAmazonDynamoDBConfig:var config = new AmazonDynamoDBConfig { // Збільшуємо ліміт підключень (за замовчуванням 50) для систем із великим паралелізмом ConnectionLimit = 500 }; - Обробка таймаутів та мережевих помилок:
SDK за замовчуванням робить повторні спроби (Exponential Backoff) для помилок типу
ProvisionedThroughputExceededExceptionабо тимчасових втрат з'єднання. Налаштувати кількість ретриїв можна вручну:var config = new AmazonDynamoDBConfig { MaxErrorRetry = 5 // Кількість автоматичних повторних спроб }; - Локалізація та парсинг чисел (CultureInfo):
При ручному формуванні
AttributeValue.Nзавжди передавайте числа через інваріантну культуруprice.ToString(CultureInfo.InvariantCulture)or"F2", щоб запобігти генерації коми замість крапки (89,99замість89.99), що призведе до помилки десеріалізації з боку AWS API.
Практичний приклад: Інтерактивна робота з DynamoDB через AWS CLI
Перш ніж переходити до розробки коду на C#, важливо зрозуміти низькорівневу механіку DynamoDB «на дотик». Найкращий спосіб зробити це — скористатися утилітою командного рядка AWS CLI для взаємодії з вашим хмарним акаунтом AWS. Це дозволить наочно побачити структуру JSON-документів з явним зазначенням типів даних (S, N, BOOL), які надсилаються в HTTP-запитах до DynamoDB API.
Налаштування середовища та автентифікації
Для виконання запитів до хмари AWS переконайтеся, що на вашій машині встановлено AWS CLI та налаштовано профіль автентифікації за замовчуванням (Default Profile) або інший профіль з відповідними правами доступу до DynamoDB (наприклад, політика AmazonDynamoDBFullAccess):
# Конфігурація доступу до реального акаунту AWS
aws configure
Утиліта попросить ввести:
- AWS Access Key ID: ваш ідентифікатор ключа доступу IAM.
- AWS Secret Access Key: ваш секретний ключ доступу IAM.
- Default region name: регіон вашої бази (наприклад,
eu-central-1). - Default output format: формат виводу, наприклад
json.
Після цього всі команди будуть за замовчуванням виконуватися у вашому хмарному середовищі AWS.
!TIPАльтернатива (Локальний запуск): Якщо ви хочете запустити ці ж команди локально на Docker-контейнері DynamoDB Local (з Кроку 1 альтернативного шляху C#-прикладу), додавайте до кожної команди параметр
--endpoint-url http://localhost:8000та вказуйте будь-який фейковий профіль за допомогою--profile local.
Крок 1: Створення таблиці (Create Table)
Створимо таблицю SandboxTasks із композитним ключем (Partition Key UserId типу String та Sort Key TaskId типу String), а також додамо локальний вторинний індекс (LSI) для фільтрації за пріоритетом.
aws dynamodb create-table \
--table-name SandboxTasks \
--attribute-definitions \
AttributeName=UserId,AttributeType=S \
AttributeName=TaskId,AttributeType=S \
AttributeName=Priority,AttributeType=S \
--key-schema \
AttributeName=UserId,KeyType=HASH \
AttributeName=TaskId,KeyType=RANGE \
--local-secondary-indexes \
"[
{
\"IndexName\": \"Priority-index\",
\"KeySchema\": [
{\"AttributeName\": \"UserId\", \"KeyType\": \"HASH\"},
{\"AttributeName\": \"Priority\", \"KeyType\": \"RANGE\"}
],
\"Projection\": {
\"ProjectionType\": \"ALL\"
}
}
]" \
--billing-mode PAY_PER_REQUEST
Що тут відбувається:
AttributeDefinitions: описує схему типів даних лише для полів, які використовуються в ключах (UserId,TaskId,Priority).KeySchema: визначає роль полів у первинному ключі.LocalSecondaryIndexes: створює індексPriority-index, який дозволяє сортувати та шукати задачі конкретного користувача за пріоритетом.
Крок 2: Додавання запису (PutItem)
Запишемо перше завдання до бази. Оскільки DynamoDB є безсхемною (schemaless) базою даних, ми можемо передавати будь-яку кількість додаткових атрибутів (наприклад, Title, Done, StepsCount), не оголошуючи їх у схемі таблиці:
aws dynamodb put-item \
--table-name SandboxTasks \
--item '{
"UserId": {"S": "user-42"},
"TaskId": {"S": "task-001"},
"Title": {"S": "Написати документацію по DynamoDB"},
"Priority": {"S": "HIGH"},
"Done": {"BOOL": false},
"StepsCount": {"N": "5"},
"Tags": {"L": [{"S": "aws"}, {"S": "nosql"}, {"S": "cli"}]}
}'
Зверніть увагу:
- Кожне значення загорнуте в об'єкт із ключем типу даних:
{"S": "..."}для String,{"BOOL": ...}для Boolean,{"N": "..."}для Number (числа передаються як рядки для запобігання втрати точності),{"L": [...]}для List.
Крок 3: Читання запису за ключем (GetItem)
Для точкового зчитування запису (Point Read) ми повинні передати повний первинний ключ (як Partition Key, так і Sort Key):
aws dynamodb get-item \
--table-name SandboxTasks \
--key '{
"UserId": {"S": "user-42"},
"TaskId": {"S": "task-001"}
}'
Очікувана відповідь від бази:
{
"Item": {
"StepsCount": {"N": "5"},
"UserId": {"S": "user-42"},
"TaskId": {"S": "task-001"},
"Done": {"BOOL": false},
"Title": {"S": "Написати документацію по DynamoDB"},
"Priority": {"S": "HIGH"},
"Tags": {"L": [{"S": "aws"}, {"S": "nosql"}, {"S": "cli"}]}
}
}
Крок 4: Умовне оновлення з перевіркою версії (UpdateItem & ConditionExpression)
Виконаємо оновлення статусу виконання задачі (Done = true), але додамо умову: оновлювати лише якщо задача має статус Done = false (Condition Expression). Це демонструє базову механіку уникнення конфліктів:
aws dynamodb update-item \
--table-name SandboxTasks \
--key '{
"UserId": {"S": "user-42"},
"TaskId": {"S": "task-001"}
}' \
--update-expression "SET Done = :new_status" \
--condition-expression "Done = :expected_status" \
--expression-attribute-values '{
":new_status": {"BOOL": true},
":expected_status": {"BOOL": false}
}' \
--return-values ALL_NEW
Що тут відбувається:
UpdateExpression: визначає зміну атрибута за допомогою плейсхолдера:new_status.ConditionExpression: вказує, що операція запису відбудеться лише якщо поточне значенняDoneдорівнює плейсхолдеру:expected_status(тобтоfalse).ReturnValues ALL_NEW: каже базі повернути оновлений об'єкт.
Результат повторного виконання:
Якщо ви виконаєте цю саму команду вдруге, операція завершиться помилкою:
An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation:
Оскільки статус у базі вже став true, умова Done = false більше не виконується.
Крок 5: Пошук за допомогою запитів (Query)
А. Запит за головним ключем
Отримаємо список усіх задач користувача user-42, які починаються на префікс task-:
aws dynamodb query \
--table-name SandboxTasks \
--key-condition-expression "UserId = :uid AND TaskId begins_with(:task_prefix)" \
--expression-attribute-values '{
":uid": {"S": "user-42"},
":task_prefix": {"S": "task-"}
}'
Б. Запит за допомогою вторинного індексу LSI
Тепер скористаємося індексом Priority-index, щоб отримати лише задачі з високим пріоритетом (HIGH), відсортовані за спаданням:
aws dynamodb query \
--table-name SandboxTasks \
--index-name Priority-index \
--key-condition-expression "UserId = :uid AND Priority = :priority" \
--expression-attribute-values '{
":uid": {"S": "user-42"},
":priority": {"S": "HIGH"}
}'
Крок 6: Видалення запису та очищення (DeleteItem)
Видалимо створену задачу:
aws dynamodb delete-item \
--table-name SandboxTasks \
--key '{
"UserId": {"S": "user-42"},
"TaskId": {"S": "task-001"}
}'
Для повного видалення таблиці після експериментів виконайте:
aws dynamodb delete-table \
--table-name SandboxTasks
Практичний приклад: C# Web API + DynamoDB від А до Я
У цьому розділі ми розберемо реалізацію повноцінного production-готового веб-сервісу з управління завданнями (Task Management API) на ASP.NET Core від А до Я. Наш застосунок підтримуватиме повний CRUD-цикл за допомогою OPM ORM (DynamoDBContext) та вибірку за глобальним вторинним індексом (GSI).
Основним середовищем розгортання є реальна хмара AWS (з керуванням ресурсами через веб-консоль та автентифікацією через AWS CLI), а використання локального контейнера з DynamoDB Local охарактеризовано як альтернативний спрощений шлях.
Крок 1: Підготовка інфраструктури (AWS Console, Credentials & Альтернатива)
Для початку роботи нам необхідно створити таблицю в хмарі AWS та налаштувати права доступу на локальній машині розробника.
1. Створення таблиці через AWS Management Console
Для створення таблиці вручну виконайте такі кроки:
- Відкрийте AWS Management Console та авторизуйтесь у вашому акаунті.
- У полі пошуку сервісів введіть DynamoDB та перейдіть на головну сторінку сервісу.
- Натисніть кнопку Create table (Створити таблицю).
- У секції Table details вкажіть такі параметри:
- Table name:
UserTasks - Partition key:
UserId(тип залишіть як String) - Sort key:
TaskId(тип залишіть як String)
- Table name:
- У секції Table settings виберіть пункт Customize settings (Налаштувати параметри):
- Capacity calculator: пропустіть цей пункт.
- Table class: виберіть DynamoDB Standard (за замовчуванням).
- Read/write capacity mode: виберіть On-demand (за запитом). Це найкращий варіант для навчання та тестування, оскільки ви платите лише за фактично виконані запити, а не за зарезервовану ємність (WCU/RCU).
- Прокрутіть вниз до секції Global secondary indexes (Глобальні вторинні індекси) та натисніть Create index:
- Partition key:
Status(тип String) - Index name: автоматично підставиться
Status-index(якщо ні, введіть вручну). - Attribute projections: виберіть All (це дозволить копіювати всі поля запису у вторинний індекс).
- Натисніть Create index у спливаючому вікні.
- Partition key:
- Прокрутіть сторінку до самого кінця та натисніть кнопку Create table. Створення таблиці триває близько 10-30 секунд, статус зміниться на Active.
2. Налаштування локальних AWS Credentials
Веб-додаток на C# під час запуску локально на вашому комп'ютері використовує офіційний AWS SDK. Щоб він міг авторизуватися в хмарі AWS, потрібно налаштувати облікові дані на локальній машині за допомогою AWS CLI:
- Відкрийте термінал та запустіть команду налаштування:
aws configure - Утиліта запитає у вас конфігураційні дані:
- AWS Access Key ID: введіть ваш ключ доступу IAM-коривувача (наприклад,
AKIAIOSFODNN7EXAMPLE). - AWS Secret Access Key: введіть ваш секретний ключ (наприклад,
wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY). - Default region name: введіть регіон, у якому ви створили таблицю (наприклад,
eu-central-1). - Default output format: залишіть порожнім або введіть
json.
- AWS Access Key ID: введіть ваш ключ доступу IAM-коривувача (наприклад,
!IMPORTANT Переконайтеся, що ваш IAM-користувач, ключі якого ви ввели, має права доступу на роботу з DynamoDB (наприклад, політику
AmazonDynamoDBFullAccessабо точкові права на читання/запис до таблиціUserTasks).
Тепер під час запуску додатку AWS SDK автоматично підхопить конфігурацію з файлу ~/.aws/credentials та надішле запити до реальної бази у хмарі.
3. Альтернатива: Локальний запуск через Docker Compose (DynamoDB Local)
Якщо ви бажаєте протестувати код локально без використання реального хмарного акаунту AWS, ви можете запустити базу даних у локальному Docker-контейнері:
- Створіть у корені проєкту файл
docker-compose.yml:docker-compose.ymlversion: '3.8' services: dynamodb-local: image: amazon/dynamodb-local:latest container_name: dynamodb-local ports: - "8000:8000" volumes: - "./docker/dynamodb:/home/dynamodblocal-data" command: "-jar DynamoDBLocal.jar -sharedDb -dbPath /home/dynamodblocal-data" dynamodb-admin: image: aaronshaf/dynamodb-admin:latest container_name: dynamodb-admin ports: - "8001:8001" environment: - DYNAMO_ENDPOINT=http://dynamodb-local:8000 depends_on: - dynamodb-local - Запустіть Docker-контейнер командою:
docker compose up -d - Після старту перейдіть за адресою
http://localhost:8001, щоб користуватися графічним інтерфейсом адміністрування бази. При виборі локального варіанту обов'язково встановіть"LocalMode": trueу файлі конфігураціїappsettings.json(описано у Кроці 3), і додаток автоматично створить потрібну таблицю та індекс при запуску.
Крок 2: Створення та структура C# проєкту (ASP.NET Core Web API)
Створимо новий проєкт веб-інтерфейсу за допомогою .NET CLI та додамо модель даних для завдань. Кожне завдання матиме:
- Partition Key:
UserId(ідентифікатор користувача) - Sort Key:
TaskId(ідентифікатор завдання) - Атрибут
Status(наприклад,TODO,IN_PROGRESS,DONE) - Атрибут
Priority - Мапування GSI
Status-indexдля вибірки завдань за статусом.
1. Ініціалізація проєкту та встановлення NuGet-пакетів
Виконайте наступні команди у терміналі:
# Створюємо Minimal Web API проєкт
dotnet new web -o TaskManager.Api
# Переходимо до папки проєкту
cd TaskManager.Api
# Встановлюємо пакети AWS SDK
dotnet add package AWSSDK.DynamoDBv2
dotnet add package AWSSDK.Extensions.NETCore.Setup
2. Створення моделі сутності TaskItem
Створимо клас TaskItem, розмітивши його атрибутами OPM ORM:
Крок 3: Налаштування клієнта, Dependency Injection та автоматична ініціалізація бази
Оскільки в локальному режимі таблиця UserTasks та глобальний індекс Status-index не існують за замовчуванням, ми налаштуємо спеціальний Database Initializer, який перевірить наявність таблиці під час старту веб-сервісу і створить її у разі відсутності.
1. Налаштування файлу конфігурації
Створіть або оновіть файл appsettings.json, додавши секцію "DynamoDb" з конфігурацією локального підключення:
2. Реалізація DynamoDbInitializer
Створимо інфраструктурний клас, який за допомогою низькорівневого API (IAmazonDynamoDB) автоматично конструює схему таблиці:
3. Реєстрація DI та запуск ініціалізатора при старті
Оновимо файл Program.cs, зареєструвавши клієнт SDK (з урахуванням режиму Local/AWS), ORM контекст та репозиторії:
Крок 4: Реалізація CRUD репозиторію
Створимо рівень доступу до даних (Data Access Layer), який приховує деталі взаємодії з DynamoDBContext за допомогою чистого інтерфейсу.
1. Створення інтерфейсу репозиторію
Створимо інтерфейс ITaskRepository для основних операцій над завданнями:
2. Реалізація репозиторію за допомогою OPM ORM
Створимо клас DynamoDBTaskRepository. Він використовуватиме IDynamoDBContext для CRUD, а також локальне перевизначення конфігурації для пошуку за індексом GSI:
Крок 5: Додавання Minimal API ендпоінтів та верифікація роботи
На завершальному кроці ми додамо REST-ендпоінти для взаємодії з завданнями та протестуємо всі сценарії, включно з валідацією оптимістичного блокування при паралельному оновленні.
1. Додавання ендпоінтів у Program.cs
Оновіть файл Program.cs, додавши групу ендпоінтів для роботи з завданнями. Зверніть увагу на обробку помилок при оновленні записів з невірною версією (Optimistic Locking):
2. Верифікація роботи через HTTP-запити (cURL)
Для тестування розробленого API запустіть проєкт за допомогою команди dotnet run. Протестуємо основні сценарії за допомогою curl.
А. Створення нового завдання
Відправте POST-запит для створення завдання для користувача user-100:
curl -X POST http://localhost:5000/api/tasks \
-H "Content-Type: application/json" \
-d '{
"UserId": "user-100",
"Title": "Вивчити AWS DynamoDB SDK",
"Description": "Розібрати Low-Level, Document та OPM інтерфейси",
"Status": "IN_PROGRESS",
"Priority": "HIGH"
}'
Очікувана відповідь (201 Created):
{
"userId": "user-100",
"taskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
"title": "Вивчити AWS DynamoDB SDK",
"description": "Розібрати Low-Level, Document та OPM інтерфейси",
"status": "IN_PROGRESS",
"priority": "HIGH",
"createdAt": "2026-06-04T12:00:00Z",
"version": 1
}
Примітка: скопіюйте згенерований taskId для наступних запитів.
Б. Отримання завдань користувача (Query за Hash-ключем)
curl -X GET http://localhost:5000/api/tasks/user-100
Очікувана відповідь (200 OK):
[
{
"userId": "user-100",
"taskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
"title": "Вивчити AWS DynamoDB SDK",
"status": "IN_PROGRESS",
"version": 1
}
]
В. Вибірка за статусом (Query через GSI "Status-index")
curl -X GET http://localhost:5000/api/tasks/status/IN_PROGRESS
Поверне всі завдання з бази, у яких поле Status дорівнює IN_PROGRESS (навіть якщо вони належать різним користувачам).
Г. Оновлення та тест Optimistic Locking (Конфлікт версій)
Спробуємо оновити статус завдання на DONE. Спочатку відправимо коректний запит з version: 1:
curl -X PUT http://localhost:5000/api/tasks \
-H "Content-Type: application/json" \
-d '{
"UserId": "user-100",
"TaskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
"Title": "Вивчити AWS DynamoDB SDK",
"Description": "Розібрати Low-Level, Document та OPM інтерфейси",
"Status": "DONE",
"Priority": "HIGH",
"Version": 1
}'
Очікувана відповідь (200 OK):
{
"userId": "user-100",
"taskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
"status": "DONE",
"version": 2
}
Зверніть увагу: поле version автоматично збільшилось на 2.
Тепер змоделюємо конфлікт паралельного доступу. Спробуємо знову виконати оновлення, але передамо застарілу версію version: 1 (хоча у базі вже збережена версія 2):
curl -X PUT http://localhost:5000/api/tasks \
-H "Content-Type: application/json" \
-d '{
"UserId": "user-100",
"TaskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
"Title": "Вивчити AWS DynamoDB SDK",
"Status": "DONE",
"Version": 1
}'
Очікувана відповідь (409 Conflict):
{
"message": "Конфлікт паралельного оновлення. Версія запису у базі була змінена іншим процесом."
}
Це доводить, що механізм оптимістичного блокування успішно спрацював, і база відхилила перезапис застарілими даними!
Д. Видалення завдання
curl -X DELETE http://localhost:5000/api/tasks/user-100/7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d
Очікувана відповідь (204 No Content).
Підсумок модуля
У цьому навчальному модулі ми детально розглянули архітектурні засади, правила проектування та програмну реалізацію систем на базі Amazon DynamoDB:
Частина 1 — Основи: Досліджено реляційну та NoSQL парадигми, типи первинних ключів (Simple HASH проти Composite HASH+RANGE), базові CRUD операції на рівні запитів, розрахунок RCU/WCU для Strongly та Eventually consistent читання та запису.
Частина 2 — Вторинні індекси: Порівняно архітектурні особливості LSI (Local Secondary Index) та GSI (Global Secondary Index), розібрано патерн Sparse Index для оптимізації витрат та Single-Table design.
Частина 3 — Режими керування ємністю: Вивчено специфіку роботи режимів Provisioned (з автоматичним масштабуванням CloudWatch Alarms) та On-Demand (PAY_PER_REQUEST), механізми Burst Capacity та експоненціальної затримки Full Jitter.
Частина 4 — Streams та Транзакції: Деталізовано CDC-рішення DynamoDB Streams з його Shards та інтеграцією через ESM з AWS Lambda, а також реалізацію ACID транзакцій за протоколом 2PC (Two-Phase Commit).
Частина 5 — TTL, Global Tables та Best Practices: Досліджено механізм автоматичного безкоштовного очищення застарілих даних за допомогою TTL, налаштування геореплікації Global Tables (Active-Active) та стратегії уникнення гарячих партицій.
Частина 6 — Інтеграція з .NET: Розглянуто архітектурні рівні інтеграції C# клієнтів (Low-Level, Document, Object Persistence Model), розробку CRUD репозиторіїв та обробників Streams подій у Lambda.
Amazon RDS — Relational Database Service
Повний посібник з Amazon RDS для .NET розробників. PostgreSQL, MySQL, SQL Server, Multi-AZ, Read Replicas, Aurora, RDS Proxy, підключення через Entity Framework Core та Code-First міграції.
AWS Lambda та Serverless Compute
Глибоке дослідження AWS Lambda — від фундаментальних принципів serverless архітектури до продакшн-оптимізації .NET функцій. Runtimes, Layers, Triggers, Cold Start, Provisioned Concurrency, Lambda Destinations та повна інтеграція з екосистемою AWS для .NET 10 розробників.