Cơn ác mộng Polling và bài toán Real-time
2 giờ sáng, PagerDuty réo inh ỏi. Dashboard hiển thị con server production 8 vCPU chạm ngưỡng 98% CPU. Thủ phạm chẳng đâu xa lạ: 15.000 client liên tục gửi request polling mỗi 2 giây để check đơn hàng mới. Tệ hơn nữa, cụm RabbitMQ bên cạnh vừa dính OOMKilled vì nghẽn bộ nhớ. Cả cụm hạ tầng gánh tải khủng khiếp chỉ để phục vụ đúng một việc: đẩy thông báo đơn hàng tức thì.
Xử lý bài toán đồng bộ dữ liệu tức thì từ Database lên UI, anh em backend thường chọn một trong 3 hướng:
- Short Polling hoặc Long Polling: Client cứ 2-3 giây lại gửi HTTP GET lên server. Cách này dễ làm nhất nhưng tốn tài nguyên vô nghĩa.
- Dedicated Message Broker (RabbitMQ, Kafka, Redis Pub/Sub): Dùng cụm broker trung gian quản lý queue và phân phối message. Khi DB thay đổi, backend bắn event sang broker, rồi worker bốc ra đẩy tiếp qua WebSocket.
- PostgreSQL LISTEN/NOTIFY kết hợp WebSocket: Tận dụng engine Pub/Sub có sẵn trong Postgres. Database tự bắn tín hiệu qua channel nội bộ khi có dữ liệu mới. Backend giữ một kết nối
LISTEN, nhận payload rồi broadcast thẳng xuống client qua WebSocket.
Mổ xẻ ưu nhược điểm từng kiến trúc
Sau nhiều đợt tối ưu hệ thống chịu tải cao, mình tổng hợp chi phí thực tế cho từng giải pháp:
1. Polling truyền thống
- Ưu điểm: Dễ viết nhất. Frontend chỉ cần một đoạn
setIntervalgọifetch()là xong. - Nhược điểm: Lãng phí băng thông trầm trọng. Thực tế hơn 95% request trả về dữ liệu rỗng. Vào khung giờ sale, hàng triệu request đổ về dễ dàng làm cạn kiệt connection pool (50-100 connections) của database, kéo sập cả hệ thống.
2. Message Broker chuyên dụng (RabbitMQ, Redis)
- Ưu điểm: Khả năng scale ấn tượng. Hệ thống có thể gánh hàng trăm nghìn message/giây, hỗ trợ routing linh hoạt và cơ chế backpressure chuẩn chỉ.
- Nhược điểm: Đội chi phí vận hành. Bạn phải dựng cụm cluster, cấu hình alerting và duy trì thêm hạ tầng. Nguy hiểm nhất là lỗi Dual-write: Database vừa commit xong thì app server đột ngột crash trước khi kịp publish sang RabbitMQ. Hậu quả là dữ liệu đã lưu trong DB nhưng khách hàng chẳng nhận được thông báo nào.
3. PostgreSQL LISTEN và NOTIFY
- Ưu điểm:
- Zero Infra Overhead: Không tốn thêm một container hay cụm máy chủ trung gian. PostgreSQL bạn đang chạy CRUD chính là broker.
- Đảm bảo Transactional Integrity: Lệnh
NOTIFYchỉ được phát đi khi transaction đãCOMMITthành công. Nếu transaction bịROLLBACK, thông báo tự động hủy. Tuyệt đối không sinh ra thông báo rác. - Tốn cực ít tài nguyên: Kết nối lắng nghe dùng persistent connection non-blocking, chỉ ngốn khoảng 2-5MB RAM phía backend.
- Nhược điểm:
- Payload giới hạn cứng ở mức 8.000 bytes. Con số này đủ cho JSON chứa ID và vài trường metadata, nhưng không thể nhồi cả object lớn.
- Không lưu trữ hàng đợi (không persistence). Nếu Node.js server khởi động lại đúng tích tắc Postgres bắn
NOTIFY, message đó sẽ biến mất vĩnh viễn. - Cạm bẫy Connection Pooler: Cơ chế này cần session-level connection. Nếu bạn dùng PgBouncer ở chế độ
transaction pooling, lệnhLISTENsẽ không hoạt động. Giải pháp là trỏ kết nối listener trực tiếp vào Postgres hoặc mở riêng một pool chạy chế độsession.
Khi nào nên dùng PostgreSQL LISTEN/NOTIFY?
Nếu bạn đang xây dựng sàn giao dịch triệu CCU hay hệ thống log hàng terabyte mỗi ngày, Kafka hay RabbitMQ là khoản đầu tư bắt buộc. Ngược lại, với phần lớn ứng dụng vừa và nhỏ, SaaS B2B, trang Admin Dashboard, chuông thông báo đơn hàng nội bộ hay cập nhật tiến độ xuất báo cáo (dưới 500-1.000 events/giây), việc dựng cụm Redis/RabbitMQ chỉ tăng thêm điểm lỗi tiềm ẩn (point of failure).
Postgres LISTEN/NOTIFY phát huy sức mạnh tối đa khi bạn cần một kiến trúc gọn nhẹ. Bạn muốn đảm bảo 100% dữ liệu ghi đĩa an toàn mới báo cho user, đồng thời giữ chi phí cloud ở mức tối thiểu.
Trong quá trình setup data ban đầu cho hệ thống thông báo hoặc danh mục, anh em thường phải chuyển đổi các file cấu hình. Khi cần convert nhanh CSV sang JSON để import vào database, mình hay dùng công cụ tại toolcraft.app/vi/tools/data/csv-to-json — tool này chạy hoàn toàn trên trình duyệt, không đẩy dữ liệu lên server ngoài nên an tâm về bảo mật.
Triển khai thực tế: PostgreSQL + Node.js + WebSocket
Bắt tay vào code thực chiến. Toàn bộ luồng gồm 4 bước đơn giản.
Bước 1: Tạo Trigger và Function trong PostgreSQL
Tạo bảng orders, một function gom dữ liệu thành JSON rồi gọi pg_notify, sau đó kích hoạt tại sự kiện AFTER INSERT.
-- 1. Tạo bảng đơn hàng
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
total_amount NUMERIC(12, 2) NOT NULL,
status VARCHAR(50) DEFAULT 'PENDING',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 2. Viết trigger function bắn NOTIFY
CREATE OR REPLACE FUNCTION notify_new_order()
RETURNS TRIGGER AS $$
DECLARE
notification_payload JSON;
BEGIN
-- Đóng gói dữ liệu tối giản (tránh vượt ngưỡng 8000 bytes)
notification_payload = json_build_object(
'id', NEW.id,
'customer_name', NEW.customer_name,
'total_amount', NEW.total_amount,
'status', NEW.status,
'created_at', NEW.created_at
);
-- Bắn payload vào channel 'new_order_channel'
PERFORM pg_notify('new_order_channel', notification_payload::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 3. Gắn trigger vào bảng orders
DROP TRIGGER IF EXISTS trg_orders_notify ON orders;
CREATE TRIGGER trg_orders_notify
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION notify_new_order();
Bước 2: Dựng Backend Node.js lắng nghe LISTEN và phát WebSocket
Ở tầng backend, cài hai thư viện phổ biến là pg và ws:
npm install pg ws
Khởi tạo WebSocket server và mở riêng một kết nối Postgres chuyên dụng để duy trì lệnh LISTEN:
// server.js
const { Client } = require('pg');
const { WebSocketServer } = require('ws');
// 1. Khởi tạo WebSocket server trên port 8080
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
console.log('[WebSocket] Client đã kết nối.');
ws.on('close', () => console.log('[WebSocket] Client ngắt kết nối.'));
});
function broadcast(data) {
wss.clients.forEach((client) => {
if (client.readyState === 1) { // 1 = OPEN
client.send(JSON.stringify(data));
}
});
}
// 2. Mở dedicated connection đến PostgreSQL (tránh dùng chung pool với CRUD)
const pgClient = new Client({
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:password@localhost:5432/production_db'
});
async function startListening() {
try {
await pgClient.connect();
console.log('[PostgreSQL] Kết nối database thành công.');
// Đăng ký nhận tin từ channel
await pgClient.query('LISTEN new_order_channel');
console.log('[PostgreSQL] Đang chờ NOTIFY trên channel: new_order_channel');
// Xử lý event khi Postgres bắn notification
pgClient.on('notification', (msg) => {
try {
const payload = JSON.parse(msg.payload);
broadcast({
event: 'ORDER_CREATED',
data: payload
});
} catch (err) {
console.error('Lỗi parse JSON payload:', err);
}
});
pgClient.on('error', (err) => {
console.error('[PostgreSQL Error]', err);
process.exit(1); // Để process manager (Docker/PM2) tự restart lại kết nối
});
} catch (error) {
console.error('Không thể kết nối đến PostgreSQL:', error);
setTimeout(startListening, 5000);
}
}
startListening();
Bước 3: Nhận thông báo thời gian thực trên Frontend
Phía giao diện web, mở kết nối WebSocket tới server và cập nhật UI ngay khi nhận message:
// client.js
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
console.log('Đã kết nối tới WebSocket server.');
};
socket.onmessage = (event) => {
const response = JSON.parse(event.data);
if (response.event === 'ORDER_CREATED') {
const order = response.data;
console.log('Đơn hàng mới:', order);
// Thay thế alert bằng Toast notification trên UI thực tế
alert(`Đơn hàng #${order.id} từ ${order.customer_name} ($${order.total_amount}) vừa được tạo!`);
}
};
socket.onerror = (err) => {
console.error('Lỗi kết nối WebSocket:', err);
};
Bước 4: Kiểm tra kết quả
Chạy thử nghiệm bằng cách mở console psql và chèn một bản ghi mẫu:
INSERT INTO orders (customer_name, total_amount)
VALUES ('Nguyen Van A', 450.00);
Ngay khi transaction commit, trigger kích hoạt pg_notify. Trong vòng chưa đầy 10ms, trình duyệt lập tức hiện thông báo mà không cần gửi bất kỳ request polling nào. Không tốn thêm một megabyte RAM cho Redis, CPU database duy trì ở mức dưới 5%, và bạn hoàn toàn có thể yên tâm ngủ ngon qua đêm mà không lo chuông PagerDuty reo.

