DynamoDB: NoSQL Database cho ứng dụng hiệu năng cao

Hiểu về DynamoDB từ partition keys đến GSI/LSI. Khi nào dùng DynamoDB thay vì RDS? Capacity modes và best practices.

Prerequisites: Nên đọc RDS & Aurora trước để hiểu sự khác biệt SQL vs NoSQL.

DynamoDB là gì?

DynamoDB là fully managed NoSQL database của AWS với khả năng scale không giới hạn và latency cực thấp (single-digit milliseconds).

Khi nào dùng DynamoDB?

Use caseDynamoDBRDS/Aurora
Gaming leaderboards✅ Tốt nhất❌ Chậm
Session storage✅ Tốt nhất⚠️ Được
IoT data (millions writes/sec)✅ Tốt nhất❌ Không scale
Complex SQL queries❌ Không hỗ trợ✅ Tốt nhất
Transactions across tables⚠️ Hạn chế✅ Tốt nhất
Unknown query patterns❌ Cần biết trước✅ Linh hoạt

Quy tắc đơn giản:

  • DynamoDB: Biết trước access patterns, cần scale lớn, cần low latency
  • RDS/Aurora: Query phức tạp, joins, transactions, không biết trước access patterns

Các khái niệm cốt lõi

1. Tables, Items, Attributes

DynamoDB Table: Users
┌──────────────────────────────────────────────────────────┐
│ Partition Key │ Sort Key  │  Attributes...               │
├───────────────┼───────────┼──────────────────────────────┤
│ user_123      │ profile   │ {"name": "An", "age": 25}    │  ← Item
│ user_123      │ settings  │ {"theme": "dark", ...}       │  ← Item
│ user_456      │ profile   │ {"name": "Binh", "age": 30}  │  ← Item
└──────────────────────────────────────────────────────────┘
  • Table: Tập hợp các items (tương tự table trong SQL)
  • Item: Một record (tương tự row trong SQL) - tối đa 400KB
  • Attributes: Fields trong item (có thể khác nhau giữa các items!)

2. Primary Key - Quan trọng nhất!

DynamoDB có 2 loại primary key:

A. Partition Key only (Simple Primary Key)

Table: Products
Partition Key: product_id

┌─────────────┬──────────────────────────────┐
│ product_id  │ Attributes                   │
├─────────────┼──────────────────────────────┤
│ PROD-001    │ {"name": "iPhone", ...}      │
│ PROD-002    │ {"name": "MacBook", ...}     │
└─────────────┴──────────────────────────────┘

Mỗi item có partition key UNIQUE

B. Partition Key + Sort Key (Composite Primary Key)

Table: Orders
Partition Key: customer_id
Sort Key: order_date

┌──────────────┬─────────────┬─────────────────────────┐
│ customer_id  │ order_date  │ Attributes              │
├──────────────┼─────────────┼─────────────────────────┤
│ CUST-001     │ 2025-01-01  │ {"total": 100, ...}     │
│ CUST-001     │ 2025-01-15  │ {"total": 250, ...}     │
│ CUST-001     │ 2025-02-01  │ {"total": 75, ...}      │
│ CUST-002     │ 2025-01-10  │ {"total": 500, ...}     │
└──────────────┴─────────────┴─────────────────────────┘

Partition key + Sort key = UNIQUE
Có thể query tất cả orders của 1 customer (sorted by date!)

💡 Tip: Partition key quyết định data phân bố như thế nào. Chọn partition key có high cardinality (nhiều giá trị khác nhau) để tránh “hot partitions”.


Capacity Modes

DynamoDB có 2 capacity modes:

1. Provisioned Mode (Rẻ hơn, cần dự đoán)

Bạn chỉ định trước Read/Write Capacity Units (RCU/WCU):

1 RCU = 1 strongly consistent read/sec for item ≤ 4KB
      = 2 eventually consistent reads/sec for item ≤ 4KB

1 WCU = 1 write/sec for item ≤ 1KB

Ví dụ tính toán:

  • Item size: 8KB
  • 100 strongly consistent reads/sec cần: 100 × (8/4) = 200 RCU
  • 50 writes/sec cần: 50 × (8/1) = 400 WCU

2. On-Demand Mode (Đắt hơn, không cần dự đoán)

  • Trả theo request
  • Tự động scale
  • Tốt cho workloads unpredictable

Terraform

# Provisioned mode
resource "aws_dynamodb_table" "orders" {
  name           = "${var.project_name}-orders"
  billing_mode   = "PROVISIONED"
  read_capacity  = 20
  write_capacity = 10
  hash_key       = "customer_id"
  range_key      = "order_date"

  attribute {
    name = "customer_id"
    type = "S"
  }

  attribute {
    name = "order_date"
    type = "S"
  }

  # Auto scaling (khuyến nghị cho production)
  # Xem phần dưới
}

# On-demand mode
resource "aws_dynamodb_table" "sessions" {
  name         = "${var.project_name}-sessions"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "session_id"

  attribute {
    name = "session_id"
    type = "S"
  }

  ttl {
    attribute_name = "expires_at"
    enabled        = true
  }
}

Secondary Indexes (GSI và LSI)

Indexes cho phép query data theo các attributes khác ngoài primary key.

Global Secondary Index (GSI)

  • Partition key và sort key khác với table chính
  • Có thể tạo bất cứ lúc nào
  • Có RCU/WCU riêng
  • Eventually consistent only
resource "aws_dynamodb_table" "orders" {
  # ... table config ...

  # GSI: Query orders by product
  global_secondary_index {
    name            = "ProductIndex"
    hash_key        = "product_id"
    range_key       = "order_date"
    projection_type = "ALL"
    read_capacity   = 10
    write_capacity  = 5
  }

  attribute {
    name = "product_id"
    type = "S"
  }
}
Original table:
PK: customer_id, SK: order_date

GSI: ProductIndex
PK: product_id, SK: order_date

Query trên GSI:
"Lấy tất cả orders của product PROD-001 trong tháng 1"

Local Secondary Index (LSI)

  • Same partition key với table chính, khác sort key
  • Phải tạo khi tạo table (không thêm sau được!)
  • Share RCU/WCU với table chính
  • Có thể strongly consistent
resource "aws_dynamodb_table" "orders" {
  # ... table config ...
  hash_key  = "customer_id"
  range_key = "order_date"

  # LSI: Query orders by status (same partition key)
  local_secondary_index {
    name            = "StatusIndex"
    range_key       = "status"
    projection_type = "ALL"
  }

  attribute {
    name = "status"
    type = "S"
  }
}
Query trên LSI:
"Lấy tất cả orders của customer CUST-001 có status = 'pending'"

GSI vs LSI

FeatureGSILSI
Partition keyKhácSame
Tạo sau khi table exists✅ Có❌ Không
RCU/WCURiêngChung với table
ConsistencyEventually onlyStrongly/Eventually
Giới hạn20 per table5 per table

DynamoDB Streams

Capture changes (insert, update, delete) và trigger actions.

┌─────────────┐    ┌──────────────────┐    ┌─────────────┐
│  DynamoDB   │───►│  DynamoDB Stream │───►│   Lambda    │
│   Table     │    │  (24h retention) │    │  Function   │
└─────────────┘    └──────────────────┘    └─────────────┘

                                           ┌──────┴──────┐
                                           │ - Send email │
                                           │ - Update ES  │
                                           │ - Aggregate  │
                                           └─────────────┘

Terraform

resource "aws_dynamodb_table" "orders" {
  # ...

  stream_enabled   = true
  stream_view_type = "NEW_AND_OLD_IMAGES"  # Capture cả before và after
}

# Lambda trigger
resource "aws_lambda_event_source_mapping" "stream" {
  event_source_arn  = aws_dynamodb_table.orders.stream_arn
  function_name     = aws_lambda_function.process_order.arn
  starting_position = "LATEST"
  batch_size        = 100
}

DynamoDB Accelerator (DAX)

In-memory cache cho DynamoDB với microsecond latency.

Without DAX:
App → DynamoDB (1-10ms)

With DAX:
App → DAX (microseconds) → DynamoDB (nếu cache miss)

Khi nào dùng DAX?

✅ Read-heavy workloads ✅ Cần microsecond latency ✅ Same query patterns repeated

❌ Write-heavy workloads ❌ Strongly consistent reads required ❌ Complex queries

Terraform

resource "aws_dax_cluster" "cache" {
  cluster_name       = "${var.project_name}-dax"
  iam_role_arn       = aws_iam_role.dax.arn
  node_type          = "dax.t3.small"
  replication_factor = 3

  subnet_group_name  = aws_dax_subnet_group.main.name
  security_group_ids = [aws_security_group.dax.id]
}

DynamoDB Transactions

Hỗ trợ ACID transactions across multiple items/tables.

# Python boto3 example
response = dynamodb.transact_write_items(
    TransactItems=[
        {
            'Put': {
                'TableName': 'Orders',
                'Item': {'order_id': {'S': 'ORD-123'}, ...}
            }
        },
        {
            'Update': {
                'TableName': 'Inventory',
                'Key': {'product_id': {'S': 'PROD-001'}},
                'UpdateExpression': 'SET quantity = quantity - :dec',
                'ExpressionAttributeValues': {':dec': {'N': '1'}}
            }
        }
    ]
)
# Cả 2 operations succeed hoặc fail cùng nhau

⚠️ Lưu ý: Transactions cost 2x RCU/WCU compared to standard operations.


Global Tables (Multi-Region)

Active-active replication across AWS regions.

┌──────────────────┐         ┌──────────────────┐
│  Region: Tokyo   │◄───────►│  Region: Sydney  │
│  DynamoDB Table  │  sync   │  DynamoDB Table  │
└──────────────────┘         └──────────────────┘
     ▲                              ▲
     │ write                        │ write
     │                              │
  Users in Japan              Users in Australia

Terraform

resource "aws_dynamodb_table" "global" {
  name         = "GlobalUsers"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "user_id"

  attribute {
    name = "user_id"
    type = "S"
  }

  stream_enabled   = true  # Required for Global Tables
  stream_view_type = "NEW_AND_OLD_IMAGES"

  replica {
    region_name = "ap-northeast-1"  # Tokyo
  }

  replica {
    region_name = "ap-southeast-2"  # Sydney
  }
}

Best Practices

1. Partition Key Design

# ❌ Bad: Low cardinality
# Partition key = "status" (chỉ có: pending, completed, cancelled)
# → Hot partition vì hầu hết items có status = "completed"

# ✅ Good: High cardinality
# Partition key = "user_id" hoặc "order_id"
# → Data phân bố đều

2. Write Sharding cho hot items

# Counter item bị hot (mọi người đều update)
# Thêm random suffix để phân bố writes

partition_key = f"page_views#{random.randint(0, 9)}"

# Query: phải aggregate từ 10 shards
total = sum([get_item(f"page_views#{i}") for i in range(10)])

3. TTL để tự động xóa data

resource "aws_dynamodb_table" "sessions" {
  # ...
  
  ttl {
    attribute_name = "expires_at"  # Unix timestamp
    enabled        = true
  }
}

# Item sẽ tự động bị xóa sau expires_at (không tốn WCU!)

4. Sparse Indexes

# Chỉ index items có attribute "premium"
# Items không có attribute này sẽ không xuất hiện trong index
# → Index nhỏ hơn, RCU ít hơn

global_secondary_index {
  name     = "PremiumUsersIndex"
  hash_key = "premium"  # Chỉ users có attribute này
}

Practice Questions (SAA style)

1. Một ứng dụng gaming cần lưu trữ player scores với millions of writes per second. Điểm số cần query theo player_id và game_id. Thiết kế table nào phù hợp nhất?

A. RDS PostgreSQL với read replicas
B. DynamoDB với partition key = player_id, sort key = game_id
C. DynamoDB với partition key = game_id only
D. ElastiCache Redis

Đáp án: B - DynamoDB scales tốt cho high writes, composite key cho phép query cả theo player và game.


2. Một DynamoDB table có provisioned capacity 100 RCU. Mỗi item 8KB. Có thể thực hiện bao nhiêu strongly consistent reads per second?

A. 100
B. 50
C. 200
D. 25

Đáp án: B - 1 RCU = 1 strongly consistent read cho 4KB. Item 8KB cần 2 RCU. 100 RCU / 2 = 50 reads/sec.


3. Ứng dụng cần query DynamoDB theo 3 access patterns khác nhau mà không sử dụng được primary key. Giải pháp nào tốt nhất?

A. Tạo 3 tables khác nhau
B. Sử dụng Scan operation với filter
C. Tạo Global Secondary Indexes
D. Migrate sang Aurora

Đáp án: C - GSI cho phép query theo các attributes khác. Scan là expensive và không scalable.


Bài tiếp theo: API Gateway - Xây dựng REST/HTTP APIs cho serverless apps.