ElastiCache: Redis, Memcached và Caching Strategies

Tăng performance với in-memory caching. Lazy loading, write-through và session management.

Tại sao cần Caching?

Mỗi lần user request, application phải:

  1. Parse request
  2. Query database (chậm nhất!)
  3. Process data
  4. Return response

Database query thường mất 50-100ms. Với caching, cùng data chỉ mất 1-5ms.

Không có cache:
User ──► App ──► Database (100ms) ──► App ──► User

               Bottleneck!

Có cache:
User ──► App ──► Cache (2ms) ──► App ──► User

                    └── Cache miss ──► Database ──► Cache

Amazon ElastiCache

ElastiCache là managed in-memory caching service. Hai engines:

FeatureRedisMemcached
Data structuresStrings, Lists, Sets, Hashes, Sorted SetsStrings only
PersistenceCó (snapshots, AOF)Không
ReplicationCó (Multi-AZ)Không
Pub/SubKhông
TransactionsKhông
Lua scriptingKhông
Multi-threadedSingle-threadedMulti-threaded
Use caseComplex data, sessions, leaderboardsSimple caching

Khuyến nghị: Dùng Redis cho hầu hết use cases.


Caching Strategies

1. Lazy Loading (Cache-Aside)

Data chỉ được load vào cache khi cần.

Read:
1. Check cache
2. If MISS: query DB, store in cache, return
3. If HIT: return from cache

Write:
1. Write to database
2. Invalidate cache (hoặc không làm gì)
def get_user(user_id):
    # 1. Check cache
    cache_key = f"user:{user_id}"
    cached = redis.get(cache_key)
    
    if cached:
        return json.loads(cached)  # Cache HIT
    
    # 2. Cache MISS - query database
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    # 3. Store in cache với TTL
    redis.setex(cache_key, 3600, json.dumps(user))  # 1 hour TTL
    
    return user

Pros:

  • Chỉ cache data thực sự được dùng
  • Cache failures không ảnh hưởng app

Cons:

  • Cache miss penalty (first request chậm)
  • Data có thể stale (cần TTL)

2. Write-Through

Data được write đồng thời vào cache và database.

Write:
1. Write to cache
2. Write to database (same transaction)

Read:
1. Always read from cache (data luôn fresh)
def update_user(user_id, data):
    cache_key = f"user:{user_id}"
    
    # 1. Update database
    db.execute("UPDATE users SET name = ? WHERE id = ?", data['name'], user_id)
    
    # 2. Update cache
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    redis.setex(cache_key, 3600, json.dumps(user))
    
    return user

Pros:

  • Data trong cache luôn fresh
  • Reads luôn nhanh

Cons:

  • Write latency tăng
  • Có thể cache data không bao giờ được đọc

3. Write-Behind (Write-Back)

Write vào cache trước, async write vào database sau.

Write:
1. Write to cache
2. Return to user immediately
3. Background process writes to database

(Rủi ro: mất data nếu cache crash trước khi persist)

Pros:

  • Write latency thấp nhất
  • Có thể batch writes

Cons:

  • Phức tạp để implement
  • Risk mất data

4. TTL (Time-To-Live)

Đơn giản nhất - data tự expire sau một thời gian.

# Set với TTL
redis.setex("user:123", 3600, user_data)  # Expire sau 1 giờ

# TTL cho different data types
CACHE_TTL = {
    'user_profile': 3600,      # 1 hour
    'product_list': 300,       # 5 minutes
    'homepage': 60,            # 1 minute
    'real_time_data': 10,      # 10 seconds
}

Terraform: ElastiCache Redis

Redis Replication Group (Production)

# Subnet Group
resource "aws_elasticache_subnet_group" "redis" {
  name       = "${var.project_name}-redis-subnet"
  subnet_ids = var.private_subnet_ids

  tags = {
    Name = "${var.project_name}-redis-subnet"
  }
}

# Security Group
resource "aws_security_group" "redis" {
  name        = "${var.project_name}-redis-sg"
  description = "Security group for ElastiCache Redis"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 6379
    to_port         = 6379
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
    description     = "Redis from app servers"
  }

  tags = {
    Name = "${var.project_name}-redis-sg"
  }
}

# Redis Replication Group (Cluster Mode Disabled)
resource "aws_elasticache_replication_group" "redis" {
  replication_group_id = "${var.project_name}-redis"
  description          = "Redis cluster for ${var.project_name}"

  # Engine
  engine               = "redis"
  engine_version       = "7.0"
  node_type            = "cache.t3.medium"
  port                 = 6379

  # Replication (1 primary + N replicas)
  num_cache_clusters         = 2  # 1 primary + 1 replica
  automatic_failover_enabled = true
  multi_az_enabled           = true

  # Network
  subnet_group_name  = aws_elasticache_subnet_group.redis.name
  security_group_ids = [aws_security_group.redis.id]

  # Encryption
  at_rest_encryption_enabled = true
  transit_encryption_enabled = true
  auth_token                 = random_password.redis_auth.result  # Redis AUTH

  # Maintenance
  maintenance_window       = "sun:05:00-sun:06:00"
  snapshot_window          = "04:00-05:00"
  snapshot_retention_limit = 7

  # Parameter group
  parameter_group_name = aws_elasticache_parameter_group.redis.name

  # Auto minor version upgrade
  auto_minor_version_upgrade = true

  tags = {
    Name = "${var.project_name}-redis"
  }
}

# Parameter Group
resource "aws_elasticache_parameter_group" "redis" {
  family = "redis7"
  name   = "${var.project_name}-redis-params"

  parameter {
    name  = "maxmemory-policy"
    value = "volatile-lru"  # Xóa keys có TTL trước khi hết memory
  }

  parameter {
    name  = "notify-keyspace-events"
    value = "Ex"  # Enable keyspace notifications
  }
}

# Random AUTH token
resource "random_password" "redis_auth" {
  length  = 32
  special = false
}

# Store in Secrets Manager
resource "aws_secretsmanager_secret_version" "redis" {
  secret_id = aws_secretsmanager_secret.redis.id
  secret_string = jsonencode({
    host      = aws_elasticache_replication_group.redis.primary_endpoint_address
    port      = 6379
    auth      = random_password.redis_auth.result
    reader    = aws_elasticache_replication_group.redis.reader_endpoint_address
  })
}

Redis Cluster Mode Enabled

Cho workloads cần horizontal scaling (more data, more throughput):

resource "aws_elasticache_replication_group" "redis_cluster" {
  replication_group_id = "${var.project_name}-redis-cluster"
  description          = "Redis cluster mode enabled"

  engine         = "redis"
  engine_version = "7.0"
  node_type      = "cache.r6g.large"

  # Cluster mode enabled
  num_node_groups         = 3  # 3 shards
  replicas_per_node_group = 1  # 1 replica per shard

  automatic_failover_enabled = true
  multi_az_enabled           = true

  subnet_group_name  = aws_elasticache_subnet_group.redis.name
  security_group_ids = [aws_security_group.redis.id]

  at_rest_encryption_enabled = true
  transit_encryption_enabled = true
}

Session Management

Redis là giải pháp phổ biến cho session storage trong distributed systems.

Tại sao không lưu sessions trên EC2?

User ──► ALB ──► EC2-1 (has session)

         Next request goes to EC2-2

         Session not found! Login lại...

Session trong Redis

User ──► ALB ──► EC2-1 ──► Redis (get session)

         Next request to EC2-2

         EC2-2 ──► Redis ──► Same session!

Python Flask Example

from flask import Flask, session
from flask_session import Session
import redis

app = Flask(__name__)

# Configure Redis session
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.Redis(
    host='redis-cluster.xxx.cache.amazonaws.com',
    port=6379,
    password='...',
    ssl=True
)
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True

Session(app)

@app.route('/login')
def login():
    session['user_id'] = '12345'
    session['email'] = 'user@example.com'
    return 'Logged in'

@app.route('/profile')
def profile():
    user_id = session.get('user_id')
    # Works regardless of which EC2 handles request!
    return f'User: {user_id}'

Cache Invalidation Patterns

Pattern 1: Delete on Write

def update_user(user_id, data):
    # Update database
    db.update(user_id, data)
    
    # Delete cache (next read will repopulate)
    redis.delete(f"user:{user_id}")

Pattern 2: Event-Driven Invalidation

# Lambda trigger on DynamoDB stream
resource "aws_lambda_event_source_mapping" "cache_invalidator" {
  event_source_arn  = aws_dynamodb_table.users.stream_arn
  function_name     = aws_lambda_function.cache_invalidator.arn
  starting_position = "LATEST"
}
def lambda_handler(event, context):
    for record in event['Records']:
        if record['eventName'] in ['INSERT', 'MODIFY', 'REMOVE']:
            user_id = record['dynamodb']['Keys']['id']['S']
            redis.delete(f"user:{user_id}")

Practice Questions (SAA style)

1. Ứng dụng web cần session persistence khi users được routed đến different EC2 instances. Giải pháp nào phù hợp và scalable nhất?

A. Enable sticky sessions trên ALB
B. Store sessions trong DynamoDB
C. Store sessions trong ElastiCache Redis
D. Store sessions trên EFS shared filesystem

Đáp án: C - ElastiCache Redis cho latency thấp nhất và được thiết kế cho session storage.


2. Một e-commerce site cần cache product catalog thường xuyên được update. Data trong cache cần fresh nhưng performance vẫn quan trọng. Caching strategy nào phù hợp?

A. Lazy loading với TTL ngắn
B. Write-through caching
C. Write-behind caching
D. Read-through caching

Đáp án: B - Write-through đảm bảo cache luôn up-to-date khi data thay đổi.


Bài tiếp theo: Data Analytics trên AWS - Kinesis, Athena, Glue.