Trước đó, ứng dụng chính nhiều lần bị “nghẽn” vào giờ cao điểm: người dùng không truy cập được, truy vấn mất 2,5 giây, đơn hàng lỗi liên tục và log đầy deadlock errors.

Không ít ý kiến phản đối:
Họ từng bỏ qua NoSQL vì nghĩ rằng không thể xử lý tốt transaction. Nhưng đến khi buộc phải chọn giữa scale SQL theo chiều dọc hoặc tái thiết kế kiến trúc dữ liệu với NoSQL, họ đã chọn hướng thứ hai.
Kết quả: sau ba tháng, hệ thống chịu tải gấp 5 lần, truy vấn nhanh hơn 10 lần, downtime bằng 0.
1. Kiến trúc ban đầu
2. Vấn đề khi SQL chạm trần hiệu năng
Ví dụ truy vấn chậm
-- Our most problematic query (2.5s+ execution time)
SELECT
o.id, o.status, o.created_at,
c.name, c.email,
p.title, p.price,
i.quantity,
a.street, a.city, a.country,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as items_count
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
JOIN addresses a ON o.shipping_address_id = a.id
WHERE o.status = 'processing'
AND o.created_at > NOW() - INTERVAL '24 HOURS'
ORDER BY o.created_at DESC;
Nested Loop (cost=1.13..2947.32 rows=89 width=325)
-> Index Scan using orders_created_at on orders (cost=0.42..1234.56 rows=1000)
-> Materialize (cost=0.71..1701.23 rows=89 width=285)
-> Nested Loop (cost=0.71..1698.12 rows=89 width=285)
-> Index Scan using customers_pkey on customers
-> Index Scan using order_items_pkey on order_items
Hệ thống báo:
3. Những giải pháp không thành công
3.1 Tối ưu Query
Thêm composite indexes, dùng materialized views, viết lại query
-- Added composite indexes
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id);
-- Materialized views for common queries
CREATE MATERIALIZED VIEW order_summaries AS
SELECT
o.id,
COUNT(i.id) as items_count,
SUM(p.price * i.quantity) as total_amount
FROM orders o
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
GROUP BY o.id;
-- Query rewrite
WITH order_data AS (
SELECT
o.id, o.status, o.created_at,
c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'processing'
AND o.created_at > NOW() - INTERVAL '24 HOURS'
)
SELECT
od.*,
os.items_count,
os.total_amount
FROM order_data od
JOIN order_summaries os ON od.id = os.id;
→ Giảm còn 800ms, nhưng chưa đủ khi tải cao.
3.2 Cache với Redis
Triển khai caching + cache warming
// Redis caching layer
const getOrderDetails = async (orderId) => {
const cacheKey = `order:${orderId}:details`;
// Try cache first
let orderDetails = await redis.get(cacheKey);
if (orderDetails) {
return JSON.parse(orderDetails);
}
// Cache miss - query database
orderDetails = await db.query(ORDER_DETAILS_QUERY, [orderId]);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(orderDetails));
return orderDetails;
};
// Cache invalidation on updates
const updateOrder = async (orderId, data) => {
await db.query(UPDATE_ORDER_QUERY, [data, orderId]);
await redis.del(`order:${orderId}:details`);
};
Ngoài ra, họ còn thêm một bước cache warming để đẩy trước dữ liệu phổ biến vào cache:
// Warm cache for active orders
const warmOrderCache = async () => {
const activeOrders = await db.query(`
SELECT id FROM orders
WHERE status IN ('processing', 'shipped')
AND created_at > NOW() - INTERVAL '24 HOURS'
`);
await Promise.all(
activeOrders.map(order => getOrderDetails(order.id))
);
};
// Run every 5 minutes
cron.schedule('*/5 * * * *', warmOrderCache);
→ Nhanh hơn, nhưng invalidation phức tạp, dễ lỗi khi tải lớn.
3.3 Thêm Read Replica
Tăng 5 read replicas + load balancing
// Database connection pool with read-write split
const pool = {
write: new Pool({
host: 'master.database.aws',
max: 20,
min: 5
}),
read: new Pool({
hosts: [
'replica1.database.aws',
'replica2.database.aws',
'replica3.database.aws',
'replica4.database.aws',
'replica5.database.aws'
],
max: 50,
min: 10
})
};
// Load balancer for read replicas
const getReadConnection = () => {
const replicaIndex = Math.floor(Math.random() * 5);
return pool.read.connect(replicaIndex);
};
// Query router
const executeQuery = async (query, params, queryType = 'read') => {
const connection = queryType === 'write'
? await pool.write.connect()
: await getReadConnection();
try {
return await connection.query(query, params);
} finally {
connection.release();
}
};
→ Giảm tải đọc, nhưng replication lag gây mất đồng bộ dữ liệu thời gian thực.
4. Quyết định chuyển sang NoSQL
Ba tháng cố gắng không cứu nổi:
MongoDB được chọn thử nghiệm với order processing
// MongoDB order document model
{
_id: ObjectId("507f1f77bcf86cd799439011"),
status: "processing",
created_at: ISODate("2024-02-07T10:00:00Z"),
customer: {
_id: ObjectId("507f1f77bcf86cd799439012"),
name: "John Doe",
shipping_address: {
street: "123 Main St",
city: "San Francisco",
country: "USA"
}
},
items: [{
product_id: ObjectId("507f1f77bcf86cd799439013"),
title: "Gaming Laptop",
price: 1299.99,
quantity: 1,
variants: {
color: "black",
size: "15-inch"
}
}],
payment: {
method: "credit_card",
status: "completed",
amount: 1299.99
},
shipping: {
method: "express",
tracking_number: "1Z999AA1234567890",
estimated_delivery: ISODate("2024-02-10T10:00:00Z")
},
metadata: {
user_agent: "Mozilla/5.0...",
ip_address: "192.168.1.1"
}
}
Kết quả: query 2,3 giây trên PostgreSQL → còn <200ms trên MongoDB.
5. Thành quả