1. Recap & System View
Bạn đã học detail ở Database Ch10–11: replication mechanics, sharding strategy, consistent hashing, caching pattern. Ở đây ta nhìn từ kiến trúc hệ thống: data layer ghép thế nào với app, queue, cache, và các pattern phức tạp như CQRS / Event Sourcing.
1.1. Decision tree: scale data layer
- App chậm? → Đo trước. Đa số trường hợp là missing index hoặc N+1, không phải scale issue.
- Read-heavy? → Cache (Redis) trước, sau đó read replica.
- Write-heavy? → Tăng vertical (NVMe, RAM, CPU). Sau đó cân nhắc sharding.
- Specific access pattern (search, time-series, graph)? → Polyglot — bổ sung specialized DB.
- Cuối cùng: shard primary DB.
Sharding là bước cuối cùng, không phải bước đầu. Một Postgres tuned tốt có thể xử lý hàng terabyte data + 50k QPS.
2. Read Replica Routing
Pattern: write → primary; read → replica (ngẫu nhiên hoặc gần nhất).
2.1. Implementation
// Hai connection pool riêng
const writeDb = new Pool({ host: 'primary', max: 50 });
const readDb = new Pool({ host: 'replica-lb', max: 100 }); // LB to 3 replicas
// Helper
async function readQuery(sql: string, params: any[] = []) {
return readDb.query(sql, params);
}
async function writeQuery(sql: string, params: any[] = []) {
return writeDb.query(sql, params);
}
// Code:
const user = await readQuery('SELECT * FROM users WHERE id=$1', [userId]);
await writeQuery('UPDATE users SET name=$2 WHERE id=$1', [userId, name]);
2.2. Read-your-writes problem
User POST update profile → primary OK → response. User redirect → GET profile → replica chưa apply → user thấy data cũ.
2.3. Mitigations
2.3.a. Sticky write (route về primary trong N giây sau write)
async function read(userId: number) {
// Nếu user vừa write trong 5 giây qua → đọc primary
const recentWrite = await redis.get(`recent_write:${userId}`);
if (recentWrite) return writeDb.query(...);
return readDb.query(...);
}
async function write(userId: number, data: any) {
await writeDb.query(...);
await redis.set(`recent_write:${userId}`, '1', 'EX', 5);
}
2.3.b. Replication lag aware
async function readWithFreshness(query: string, maxLag = 100) {
const lag = await getReplicationLag(); // ms
if (lag > maxLag) {
return writeDb.query(query); // fallback primary
}
return readDb.query(query);
}
2.3.c. Synchronous replication
Postgres synchronous_standby_names. Đảm bảo primary chờ replica ack trước khi commit. Latency tăng nhưng RPO=0 và replica luôn fresh.
2.4. Routing strategies
- Random — đơn giản, đều load.
- Geographic — replica gần app.
- Least-loaded — replica ít connection.
- Read-only DB session — ProxySQL, PgBouncer route theo statement type (SELECT vs INSERT/UPDATE).
4. Hot Partition / Hot Shard Problem
Vài shard nhận traffic gấp nhiều lần shard khác → bottleneck dù tổng load OK.
4.1. Cause cases
- Celebrity user — Twitter user 100M follower. Mọi follower fetch tweet → shard chứa user đó hot.
- Skewed shard key — country='US' chiếm 80% data.
- Time-based — ghi luôn vào shard "ngày hôm nay".
- Auto-increment ID — mọi insert đến shard cuối.
4.2. Mitigations
4.2.a. Better shard key
Nếu phát hiện sớm, đổi shard key. Vd: hash thay vì range; composite key thay đơn.
4.2.b. Replicate hot key
Detect celebrity → replicate riêng sang nhiều node, route theo round robin.
4.2.c. Salt key — distribute write
// Counter cho celebrity tweet — split thành N counter
// Thay vì:
INCR tweet:celebrity:likes // tất cả đụng 1 key
// Dùng N salt:
const shard = userId % 100;
INCR tweet:celebrity:likes:shard:${shard}
// Read: SUM tất cả N counter
const total = await Promise.all(
Array.from({length: 100}, (_, i) => redis.get(`tweet:celebrity:likes:shard:${i}`))
).then(arr => arr.reduce((s, v) => s + Number(v ?? 0), 0));
4.2.d. Cache hot key
Detect hot key → cache ở mỗi app instance (multi-tier). Giảm load đến shard storage.
4.2.e. Shard splitting
Identify hot shard → split tiếp thành 2-4 sub-shard. DynamoDB, Cassandra tự làm. Postgres phải manual.
4.3. Detect hot shard
Monitor metrics per-shard:
- QPS / shard.
- CPU / shard.
- Disk I/O / shard.
Khi 1 shard > 2× median → đã hot, cần action.
5. CDC — Change Data Capture
Đã đề cập Database Ch11. Đây ta đào sâu architecture pattern.
5.1. CDC là gì?
Streaming mọi thay đổi từ DB primary đến downstream consumer (search, analytics, cache, audit log) real-time.
5.2. Lợi ích pattern này
- Single source of truth — DB primary; mọi consumer dùng cùng data.
- Decouple — thêm consumer mới không động source.
- Eventual consistency — downstream lag vài giây, OK với search/analytics.
- Reliable — Kafka persist event, consumer fail recover được.
5.3. Tools
- Debezium — open-source, support Postgres/MySQL/Mongo/Oracle.
- AWS DMS — managed CDC.
- Fivetran, Airbyte — SaaS connectors.
- Postgres logical replication — built-in (publication/subscription).
5.4. Use case patterns
- Sync DB → search: Postgres → Elastic. Search luôn fresh.
- Cache invalidation: CDC detect update → invalidate cache key. Tránh "UPDATE → DEL cache" race.
- Audit log: stream mọi change vào append-only log.
- Materialized view: maintain read model riêng.
- Cross-service event: order service publish "order.created" event qua CDC trên outbox table.
5.5. Outbox Pattern
Vấn đề: app cần atomic "save order + publish event". 2 hệ tách (DB + Kafka) → 2-phase commit khó.
Outbox: app save event vào bảng cùng DB transaction → CDC pull bảng đó → push Kafka. Atomic guarantees.
BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO outbox (event_type, payload) VALUES ('order.created', '{...}');
COMMIT;
-- Debezium đọc outbox table → publish Kafka
-- Nếu CDC fail, retry — không mất event
-- Nếu DB transaction rollback, outbox cũng rollback — không có event lạc
6. CQRS — Command Query Responsibility Segregation
6.1. Khái niệm
Tách read model và write model thành 2 cấu trúc data riêng. Write theo schema chuẩn (3NF, transactional), read theo schema tối ưu cho query (denormalized, indexed cho specific use case).
6.2. Khi nào dùng CQRS?
- Read và write workload khác nhau cực rõ — vd: 99% read, 1% write.
- Read pattern phức tạp, không fit normalized schema.
- Cần scale read và write độc lập.
- Reporting/analytics khác hoàn toàn với transactional.
6.3. Khi KHÔNG dùng
- App CRUD đơn giản — 2 model = 2× complexity không đáng.
- Team nhỏ.
- Read và write tương tự nhau.
6.4. Trade-off
- (+) Read query cực nhanh (denormalized).
- (+) Scale read/write độc lập.
- (−) Eventual consistency giữa write → read model.
- (−) Complexity 2× — code, deploy, test.
- (−) Read model "out of sync" temporarily — UI có thể stale.
7. Event Sourcing
7.1. Khái niệm
Thay vì lưu state hiện tại, lưu chuỗi event. Current state = replay events.
// ❌ State-based:
account: { id: 1, balance: 1500 }
// ✓ Event-sourced:
events:
{ type: 'AccountOpened', id: 1, time: t0 }
{ type: 'Deposited', amount: 1000, time: t1 }
{ type: 'Deposited', amount: 500, time: t2 }
{ type: 'Withdrawn', amount: 200, time: t3 }
{ type: 'Deposited', amount: 200, time: t4 }
// State = sum: 1000 + 500 - 200 + 200 = 1500
7.2. Lợi ích
- Audit log built-in — mọi thay đổi đều ghi.
- Time travel — replay đến thời điểm bất kỳ.
- Multiple read model — derive từ event stream.
- Debug — xem chuỗi event để hiểu state hiện tại.
- Integration — event là natural unit để publish.
7.3. Trade-off
- (−) Query state cần replay → phải có snapshot định kỳ.
- (−) Schema event evolution khó (event là immutable).
- (−) Event store lớn theo thời gian.
- (−) Eventual consistency — read model lag.
- (−) Learning curve cao.
7.4. Khi nào dùng?
- Audit là yêu cầu pháp lý (banking, healthcare).
- Workflow phức tạp với nhiều state transition (booking, order pipeline).
- Cần derive nhiều read model khác nhau.
- Domain phù hợp event-driven (DDD).
7.5. CQRS + Event Sourcing
Hai pattern thường đi cùng:
- Write side: lưu event vào event store.
- Read side: subscribe event, build read model (Postgres view, Elastic, cache).
Nhưng: không bắt buộc. CQRS dùng được không Event Sourcing. Event Sourcing dùng được không CQRS.
8. Polyglot Persistence
"Right tool for right job" — dùng nhiều DB khác nhau cho các phần khác nhau của hệ.
8.1. Ví dụ kiến trúc thực tế (e-commerce)
8.2. Trade-off
- (+) Mỗi DB tối ưu cho 1 use case → performance tốt.
- (+) Scale độc lập.
- (−) Operational complexity — backup, monitor, upgrade nhiều system.
- (−) Data consistency cross-DB khó.
- (−) Learning curve cho team.
8.3. Quy tắc thực dụng
- Bắt đầu với Postgres làm "everything" — JSONB, tsvector, geometry, ltree đủ cho hầu hết case.
- Thêm Redis khi cần cache/session/queue.
- Thêm Elasticsearch khi search là core feature, Postgres tsvector không đủ.
- Thêm Cassandra/ClickHouse cho specific scale (write throughput, OLAP).
- Mỗi DB mới = 1 production responsibility — chỉ thêm khi profile chỉ rõ cần.
9. Distributed Transactions & Saga Pattern
9.1. Vấn đề
Transaction trong 1 DB = ACID. Cross-DB hoặc cross-service?
Ví dụ booking: trừ tiền (payment service) + giảm inventory (inventory service) + tạo booking (booking service). Nếu 1 fail thì sao?
9.2. Two-Phase Commit (2PC)
Solution cổ điển — coordinator hỏi mọi participant "ready?" → tất cả OK thì commit, không thì rollback.
Vấn đề:
- Coordinator chết giữa phase 2 → participant kẹt (locked).
- Latency cao (2 round-trip + lock).
- Không scale ngang.
Đa số system hiện đại tránh 2PC.
9.3. Saga Pattern
Chuỗi local transaction. Mỗi bước có compensating action để undo nếu sau đó fail.
9.4. Saga implementation styles
9.4.a. Choreography
Mỗi service publish event sau khi xong, service tiếp theo subscribe và react. Không có coordinator central.
PaymentService → publish "payment.completed"
▼
InventoryService subscribe → decrement → publish "inventory.reserved"
▼
BookingService subscribe → create → publish "booking.created"
Khi fail: service publish "*.failed" event, các service trước subscribe và compensate.
- (+) Decentralized, loosely coupled.
- (−) Khó visualize toàn flow; debug khó.
9.4.b. Orchestration
1 orchestrator service điều phối từng bước.
async function bookingSaga(bookingData) {
const completed = [];
try {
await paymentService.charge(bookingData);
completed.push('payment');
await inventoryService.reserve(bookingData);
completed.push('inventory');
await bookingService.create(bookingData);
completed.push('booking');
return { success: true };
} catch (err) {
// Compensate ngược lại
if (completed.includes('inventory')) await inventoryService.release(bookingData);
if (completed.includes('payment')) await paymentService.refund(bookingData);
return { success: false, error: err.message };
}
}
- (+) Flow rõ, dễ debug.
- (−) Orchestrator = central → có thể trở thành SPOF/bottleneck.
Tools: AWS Step Functions, Temporal, Camunda. Hỗ trợ retry, timeout, compensation tự động.
9.5. Idempotency — yêu cầu bắt buộc
Saga compensate có thể chạy lại nhiều lần do retry. Mọi action phải idempotent:
// ❌ Không idempotent
function refund(orderId, amount) {
account.balance += amount;
}
// ✓ Idempotent qua idempotency key
async function refund(orderId, amount) {
const exists = await db.query('SELECT 1 FROM refunds WHERE order_id=$1', [orderId]);
if (exists.rowCount > 0) return; // đã refund
await db.query('INSERT INTO refunds (order_id, amount) VALUES ($1, $2)', [orderId, amount]);
account.balance += amount;
}
10. Bài tập
- Thiết kế read replica routing cho app:
- 3 replica + 1 primary.
- "Read your writes" cho profile update (5 giây).
- Background reporting đọc từ replica có lag < 30s.
- Cho domain Twitter, đề xuất shard key cho 3 entity: User, Tweet, Follow. Lý giải.
- Phát hiện hot shard: shard 5 có QPS 50k, 11 shard khác mỗi cái 5k. Đề xuất 3 cách fix.
- Implement Outbox pattern cho service Order: save order + publish event "order.created" atomic. Mock CDC reader.
- So sánh CQRS vs đơn giản 1 model với index tốt: cho 2 use case, mỗi cái phân tích trade-off.
- Implement Saga orchestration cho booking flow (payment → inventory → booking) với compensation. Test scenario fail ở booking step.
- Polyglot persistence: design e-commerce mini cho Twitter clone — chọn DB nào cho user/tweet/timeline/search/notification.
11. Quiz
Quiz cuối Chương 4
Sharding nên là bước:
"Read your writes" với async replica:
Hot shard "celebrity user" trên Twitter, 1 cách fix là:
Outbox pattern giải quyết:
CQRS phù hợp khi:
Event Sourcing nghĩa là:
Saga vs 2PC:
Polyglot persistence quy tắc thực dụng:
Hoàn thành Chương 4. Tiếp theo: Chương 5 — Message Queue & Async Communication →