Scalability Strategy: Hỏi AI về phương án sharding, caching (DynamoDB/Redis)
7/19/2026 · 12p đọc
title: "Scalability Strategy: Hỏi AI về phương án sharding, caching (DynamoDB/Redis)"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 2 — AI-Augmented Architecture & System Design"
order: 18
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "13 phút"
tags: ["scalability", "sharding", "caching", "redis", "dynamodb", "hot partition", "cache invalidation", "solutions architect", "ai-native"]
Scalability Strategy: Hỏi AI về phương án sharding, caching (DynamoDB/Redis)
Bạn từng chứng kiến cảnh này chưa: hệ thống chạy êm ru cả năm trời, rồi một chiến dịch marketing thành công bất ngờ, traffic tăng gấp 10 lần trong vài giờ, và bảng bookings — cái bảng vẫn luôn ổn — bắt đầu timeout hàng loạt. Query "lấy booking hôm nay" mà trước đây chạy 20ms giờ mất 8 giây. Connection pool cạn kiệt. CPU của database instance đỏ rực. Không ai touch code gì cả, nhưng hệ thống sập.
Đây không phải lỗi code. Đây là hệ quả của một thiết kế dữ liệu chưa từng được stress-test cho quy mô lớn hơn. Ở quy mô nhỏ, một bảng SQL với vài index cơ bản, không cache, chịu được mọi truy vấn. Nhưng khi dữ liệu tăng theo cấp số nhân và pattern truy cập lệch hẳn về một phía (ví dụ 90% query chỉ nhắm vào dữ liệu 7 ngày gần nhất), thiết kế "một bảng, một index" bắt đầu bộc lộ giới hạn vật lý: một node không thể vừa giữ toàn bộ dữ liệu lịch sử vừa phục vụ nổi lượng đọc/ghi tập trung vào một lát cắt nhỏ.
Vấn đề không phải là "chúng ta cần scale". Vấn đề là "chúng ta cần biết phải scale theo trục nào, và trục đó có khớp với cách người dùng thực sự truy vấn dữ liệu hay không". Đây chính là lúc AI trở thành một cộng sự đáng giá — không phải để nó tự quyết định kiến trúc, mà để nó giúp bạn suy luận có hệ thống qua các phương án caching và sharding, dựa trên access pattern thật, thay vì áp dụng công thức chung chung "cứ thêm Redis là xong".
Vấn đề
Giả sử bạn đang vận hành một module Booking trong hệ thống CRM — tương tự cấu trúc code/be/src/modules mà bất kỳ ai làm NestJS cũng quen thuộc. Ban đầu, bảng bookings chỉ có vài chục nghìn dòng, một PRIMARY KEY(id) tự tăng, và một vài index phụ trên customer_id, status. Mọi query đều nhanh vì toàn bộ working set nằm gọn trong buffer pool của MySQL.
Sáu tháng sau, bảng này có 50 triệu dòng. Query pattern thực tế trông như sau:
- 85% truy vấn là "lấy booking trong khoảng ngày X đến Y", với X, Y gần như luôn nằm trong 30 ngày gần nhất.
- 10% truy vấn là tra cứu lịch sử của một khách hàng cụ thể (
customer_id), có thể trải dài nhiều năm. - 5% còn lại là báo cáo tổng hợp, chạy vào giờ thấp điểm.
Đây chính là dạng bất đối xứng kinh điển: dữ liệu tăng vô hạn theo thời gian, nhưng "nhiệt" của truy vấn dồn vào một lát cắt hẹp gần hiện tại. Khi bảng phình to, index B-tree càng sâu, buffer pool không còn chứa nổi cả index lẫn dữ liệu nóng, và mỗi query "ngày gần đây" phải cạnh tranh I/O với hàng chục triệu dòng lịch sử mà gần như không ai đụng tới.
Ba lựa chọn kỹ thuật thường được đặt lên bàn ở giai đoạn này:
- Thêm cache (Redis) để giảm tải đọc cho các truy vấn lặp lại.
- Sharding dữ liệu ra nhiều phân vùng/bảng để giảm kích thước working set mỗi node phải quản lý.
- Đổi engine lưu trữ sang một hệ NoSQL có mô hình phân tán native như DynamoDB.
Cái khó không phải là biết ba lựa chọn này tồn tại — ai học kiến trúc cũng biết. Cái khó là chọn đúng khóa để cache và để shard, sao cho nó khớp với access pattern thật, chứ không phải khớp với cấu trúc bảng hiện tại. Chọn sai, bạn sẽ tạo ra một hệ thống trông "có vẻ scale" nhưng vẫn sập theo đúng cách cũ, chỉ là ở một tầng khác.
Kỹ thuật cốt lõi
Caching: đặt lớp cache đúng chỗ, cho đúng loại dữ liệu
Nguyên tắc cốt lõi: cache những gì được đọc nhiều lần và ít thay đổi trong khoảng thời gian đó. Redis không phải "tăng tốc mọi thứ" — nó là một lớp đệm có TTL, và bài toán thật sự là xác định cái gì xứng đáng nằm trong đó và bao lâu thì nó hết hạn.
Với pattern Booking ở trên, có ít nhất ba tầng cache khác nhau, mỗi tầng phục vụ một mục đích:
flowchart LR
Client[Client Request] --> API[API Layer / NestJS]
API --> L1{Cache Redis}
L1 -->|hit| Client
L1 -->|miss| DB[(Database / DynamoDB)]
DB --> L1
DB --> Client
subgraph "Loại dữ liệu cache"
C1["Booking list theo ngày gần đây<br/>TTL ngắn 30-60s, invalidate on write"]
C2["Booking detail theo ID<br/>TTL trung bình, invalidate on write"]
C3["Aggregate/report theo customer<br/>TTL dài, refresh theo batch"]
end
Ba tầng này khác nhau về TTL và chiến lược invalidation, vì chúng khác nhau về độ nhạy cảm với dữ liệu cũ (staleness tolerance). Booking list "hôm nay" cần gần real-time — TTL ngắn, invalidate ngay khi có write. Báo cáo tổng hợp có thể chấp nhận trễ vài phút — TTL dài hơn, refresh theo lịch.
Sharding: chọn khóa theo hướng truy vấn, không theo hướng lưu trữ
Sharding là phân mảnh dữ liệu ra nhiều partition vật lý để mỗi partition chỉ cần xử lý một phần tải. Trong các hệ như DynamoDB, đơn vị sharding được quyết định bởi partition key (và tùy chọn sort key) mà bạn khai báo khi thiết kế bảng — DynamoDB tự động phân phối dữ liệu dựa trên hash của partition key, mỗi partition có giới hạn throughput riêng.
Sai lầm phổ biến nhất: chọn partition key theo trực giác "khóa định danh duy nhất" (như booking_id) hoặc theo trực giác "cái gì cũng vào một chỗ" (như status). Cả hai đều dẫn tới vấn đề, chỉ theo hai hướng ngược nhau:
booking_idngẫu nhiên → phân phối tải đều, nhưng bạn mất khả năng query hiệu quả theo khoảng ngày (phải scan toàn bộ hoặc dùng Global Secondary Index tốn kém).status(chỉ vài giá trị) → tất cả booking "confirmed" hôm nay dồn vào đúng 1-2 partition, trong khi hàng chục partition khác gần như rảnh. Đây chính là hiện tượng hot partition.
Cách tiếp cận đúng là suy luận ngược từ access pattern: 85% query cần "khoảng ngày gần đây" theo customer_id hoặc theo tenant/workspace_id (tương tự cách CRM này đã dùng workspace_id làm ranh giới multi-tenant, như đã ghi nhận ở module Risk). Một chiến lược hợp lý là composite key: partition key theo tenant_id (hoặc tenant_id + date_bucket nếu một tenant đơn lẻ đã quá lớn), sort key theo booking_date hoặc timestamp dạng ISO để tận dụng range query native của DynamoDB trên sort key.
Nếu một tenant lớn vẫn tạo hot partition dù đã theo tenant_id, kỹ thuật "write sharding" (thêm suffix ngẫu nhiên hoặc theo giờ vào partition key, ví dụ tenant_123#2026-07) giúp trải đều ghi trong khi vẫn giữ được khả năng truy vấn theo khoảng thời gian nhờ sort key.
Thực hành
Dưới đây là prompt mẫu thực tế — dùng đúng nguyên văn với AI coding assistant (Claude Code, Cursor, hoặc chat trực tiếp), đưa vào schema và access pattern cụ thể để nhận phân tích có cấu trúc.
Prompt mẫu: phân tích bottleneck và đề xuất sharding key + cache layer
Vai trò: Bạn là Solutions Architect có kinh nghiệm với DynamoDB và Redis ở quy mô production.
Bối cảnh hệ thống:
- Module Booking trong CRM, hiện dùng MySQL, bảng `bookings` có 50 triệu dòng,
tăng khoảng 500k dòng/tháng.
- Schema hiện tại (rút gọn):
- id (PK, auto-increment)
- tenant_id (FK, ~2000 tenant, phân phối lệch: 20 tenant lớn nhất chiếm 60% dữ liệu)
- customer_id (FK)
- booking_date (DATE)
- status (ENUM: pending, confirmed, cancelled, completed)
- amount (DECIMAL)
- created_at, updated_at (TIMESTAMP)
Access pattern thực tế (đã đo qua APM trong 30 ngày):
- 85% query: SELECT theo tenant_id + khoảng booking_date (chủ yếu 7-30 ngày gần nhất)
- 10% query: SELECT theo customer_id, không giới hạn ngày (tra lịch sử khách hàng,
có thể quét nhiều năm)
- 5% query: Aggregate (SUM amount, COUNT theo status) cho báo cáo, chạy batch
vào 2h sáng, không cần real-time
Triệu chứng khi traffic tăng đột biến (ví dụ chiến dịch marketing):
- Query "booking theo ngày gần đây" từ 20ms tăng lên 5-8s
- CPU database instance đạt 95%+ trong giờ cao điểm
- Connection pool timeout dẫn tới lỗi 5xx ở API layer
Yêu cầu phân tích, trả lời theo đúng cấu trúc:
1. BOTTLENECK: Chỉ ra chính xác vì sao pattern truy cập này gây nghẽn ở kiến trúc
hiện tại (không cache, không shard). Giải thích bằng thuật ngữ index/buffer pool,
không chỉ nói chung chung "database quá tải".
2. SHARDING KEY: Đề xuất partition key (và sort key nếu áp dụng DynamoDB) phù hợp
với access pattern trên. Với mỗi phương án, phân tích:
- Khả năng phục vụ 85% query chính (theo tenant + khoảng ngày) hiệu quả ra sao
- Rủi ro hot partition với 20 tenant lớn chiếm 60% dữ liệu, và cách giảm thiểu
(ví dụ write sharding, key suffix)
- Cách xử lý 10% query theo customer_id không giới hạn ngày (có cần GSI/index phụ không)
3. CACHE LAYER: Đề xuất Redis nên đặt ở tầng nào (API layer, service layer, hay
read-through trước DB), key pattern cụ thể, TTL đề xuất cho từng loại query
(booking list gần đây / booking detail / aggregate report), và chiến lược
invalidation khi có write (cancel booking, update status).
4. TRADE-OFF: Nêu rõ 2-3 đánh đổi của phương án đề xuất (chi phí vận hành, độ phức
tạp code, khả năng consistency) mà kỹ sư implement cần biết trước khi bắt tay vào.
Không đề xuất "dùng chung chung Redis + sharding" — hãy gắn cụ thể với schema và
số liệu access pattern đã cho ở trên.
Ví dụ output đáng tin cậy sẽ trông như thế nào
Một câu trả lời chất lượng từ AI, sau prompt trên, cần đi tới mức cụ thể như bảng dưới — nếu AI chỉ trả lời chung chung ("dùng Redis để cache, dùng tenant_id để shard") mà không phân tích trade-off, bạn cần hỏi tiếp để ép nó cụ thể hóa.
| Thành phần | Đề xuất cụ thể | Lý do |
|---|---|---|
| Partition key (DynamoDB) | tenant_id cho tenant nhỏ; tenant_id#YYYYMM cho 20 tenant lớn |
Cân bằng giữa gom nhóm truy vấn theo tenant và tránh hot partition ở tenant lớn |
| Sort key | booking_date#booking_id |
Cho phép range query native theo ngày mà không cần scan |
| GSI (Global Secondary Index) | customer_id (partition) + booking_date (sort) |
Phục vụ 10% query tra cứu lịch sử theo khách hàng |
| Cache key — booking list | bk:list:{tenant_id}:{date_range_hash} |
TTL 30-60s, đủ ngắn để chấp nhận staleness nhỏ cho dashboard |
| Cache key — booking detail | bk:detail:{booking_id} |
TTL 5 phút, invalidate ngay khi update/cancel |
| Cache key — aggregate report | bk:agg:{tenant_id}:{date} |
TTL 6-12h, refresh theo batch job, không cần invalidate on write |
| Invalidation | Publish event booking.updated qua RabbitMQ, subscriber xóa cache key liên quan |
Tránh cache stale khi có write, tách rời logic invalidation khỏi write path chính |
Snippet minh họa: cache-aside pattern trong NestJS service
// booking.service.ts — minh họa cache-aside với TTL khác nhau theo loại truy vấn
import { Injectable } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
@Injectable()
export class BookingService {
constructor(
private readonly redis: Redis,
private readonly bookingRepository: BookingRepository,
) {}
private readonly TTL = {
RECENT_LIST: 45, // giây — dữ liệu "hôm nay" chấp nhận trễ ngắn
DETAIL: 300, // 5 phút
AGGREGATE: 6 * 3600, // 6 giờ — báo cáo không cần real-time
};
async getRecentBookings(tenantId: string, fromDate: string, toDate: string) {
const cacheKey = `bk:list:${tenantId}:${fromDate}:${toDate}`;
const cached = await this.redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const data = await this.bookingRepository.findByTenantAndDateRange(
tenantId,
fromDate,
toDate,
);
await this.redis.set(cacheKey, JSON.stringify(data), 'EX', this.TTL.RECENT_LIST);
return data;
}
async invalidateOnWrite(tenantId: string, bookingId: string, bookingDate: string) {
// Xóa cache detail chính xác — không đoán TTL, xóa ngay khi có write
await this.redis.del(`bk:detail:${bookingId}`);
// List cache theo pattern — cần SCAN thay vì KEYS trong production để tránh block Redis
const pattern = `bk:list:${tenantId}:*`;
const stream = this.redis.scanStream({ match: pattern, count: 100 });
stream.on('data', (keys: string[]) => {
if (keys.length) this.redis.del(...keys);
});
}
}
Cạm bẫy thường gặp
1. Thêm cache mà không có chiến lược invalidation rõ ràng. Đây là lỗi phổ biến nhất khi đội ngũ dưới áp lực "phải giảm tải database ngay". Cache được thêm vội, TTL được đặt tùy tiện (hoặc không đặt — cache vĩnh viễn), và không ai định nghĩa rõ "khi nào cache này trở thành stale và ai chịu trách nhiệm xóa nó". Hậu quả: người dùng cancel một booking, nhưng dashboard vẫn hiển thị nó là "confirmed" trong 10 phút tới vì cache chưa hết hạn và không ai invalidate khi write. Nguyên tắc bắt buộc: mọi cache key phải có câu trả lời rõ cho câu hỏi "invalidate bằng cách nào — TTL, event-driven, hay cả hai?" trước khi merge code.
2. Chọn sharding key theo cấu trúc bảng thay vì theo access pattern. Đây là cạm bẫy prompt AI cũng dễ mắc nếu bạn không cung cấp đủ số liệu access pattern — nó sẽ đề xuất khóa "trông hợp lý về mặt lý thuyết" (như id hoặc created_at) mà không biết truy vấn thật sự đi theo hướng nào. Kết quả kinh điển là hot partition: nếu bạn shard theo status (chỉ 4 giá trị) hoặc theo created_at dạng ngày cố định mà không tính tới tenant lớn, toàn bộ traffic của giờ cao điểm dồn vào 1-2 partition trong khi hàng chục partition khác rảnh rỗi — bạn đã "sharding" nhưng vẫn chịu đúng vấn đề nghẽn cổ chai như trước, chỉ khác là giờ đây khó debug hơn vì dữ liệu đã phân tán.
3. Tin AI đề xuất một schema NoSQL "đẹp" mà không kiểm tra chi phí truy vấn phụ. DynamoDB phạt nặng các truy vấn không theo đúng partition/sort key đã thiết kế — bạn phải dùng Scan (tốn kém, chậm) hoặc thêm GSI (tốn thêm chi phí lưu trữ và write capacity). Khi AI đề xuất một partition key tối ưu cho 85% query chính, luôn hỏi ngược lại: "còn 10-15% query còn lại thì sao, chi phí GSI là bao nhiêu, và có case nào buộc phải Scan không?" — đừng để một thiết kế tối ưu cho use-case phổ biến nhất âm thầm biến các use-case phụ thành thảm họa hiệu năng.
🧭 Góc nhìn Solutions Architect
Nếu ngày mai một tenant duy nhất tăng gấp 10 lần traffic, partition key hiện tại có tự động phân tán tải đó ra, hay dồn hết vào một điểm nóng? Cache invalidation của tôi có phụ thuộc vào một single point of failure (một consumer, một job) mà nếu nó chết, dữ liệu stale sẽ âm thầm tồn tại bao lâu trước khi ai đó phát hiện? Và nếu phải rollback sharding key sau khi phát hiện sai lầm, chi phí migrate dữ liệu đã phân mảnh là gì — tôi có đang tự khóa mình vào một quyết định khó đảo ngược không?
🔗 Bài viết liên quan
- [Bài 9] AI & Database Schema Design: ERD, Indexing — nền tảng thiết kế schema trước khi tính đến sharding (ans-09-ai-database-schema-design-erd-indexing.md)
- [Bài 13] Cost Optimization with AI — vì mọi lớp cache và shard thêm vào đều có chi phí vận hành cần cân đối (ans-13-cost-optimization-with-ai.md)
Bài trước: Infrastructure as Code (IaC) · Bài tiếp theo: Disaster Recovery