PostgreSQL LISTEN/NOTIFY có thể cấp tốc cho dashboard và cảnh báo trực tiếp mà không cần nhiều cấu hình. Tìm hiểu phạm vi phù hợp, giới hạn và khi nào cần thêm broker.

“Cập nhật trực tiếp” trong UI thường có nghĩa màn hình thay đổi ngay sau khi có sự kiện, mà không cần người dùng tải lại. Một con số tăng trên dashboard, một badge đỏ xuất hiện trong hộp thư, admin thấy đơn hàng mới, hoặc một toast hiện lên báo “Build finished” hay “Payment failed”. Mấu chốt là cảm giác thời gian: nó có vẻ tức thì, dù thực tế có thể chậm một vài giây.
Nhiều đội bắt đầu bằng polling: trình duyệt hỏi server “có gì mới không?” mỗi vài giây. Polling hoạt động, nhưng có hai điểm yếu phổ biến.
Đầu tiên, nó có cảm giác trễ vì người dùng chỉ thấy thay đổi ở lần poll tiếp theo.
Thứ hai, nó có thể tốn kém vì bạn kiểm tra lặp đi lặp lại ngay cả khi không có gì thay đổi. Nhân lên với hàng nghìn người dùng và nó trở thành tiếng ồn.
PostgreSQL LISTEN/NOTIFY tồn tại cho trường hợp đơn giản hơn: “báo tôi biết khi có gì đó thay đổi.” Thay vì hỏi đi hỏi lại, app của bạn có thể chờ và phản ứng khi database gửi một tín hiệu nhỏ.
Nó phù hợp cho những UI mà một cái chạm nhẹ là đủ. Ví dụ:
Đổi lại là sự đơn giản so với đảm bảo. LISTEN/NOTIFY dễ thêm vào vì đã có sẵn trong Postgres, nhưng nó không phải là hệ thống nhắn tin hoàn chỉnh. Thông báo là một gợi ý, không phải bản ghi bền vững. Nếu listener bị ngắt kết nối, nó có thể bỏ lỡ tín hiệu.
Cách thực tế để dùng: để NOTIFY đánh thức app của bạn, rồi app đọc lại dữ liệu chính từ bảng.
Hãy nghĩ về PostgreSQL LISTEN/NOTIFY như một chiếc chuông cửa đơn giản được tích hợp trong database. App của bạn có thể chờ tiếng chuông, và phần khác của hệ thống có thể kéo chuông khi có thay đổi.
Một thông báo gồm hai phần: tên kênh và một payload tùy chọn. Kênh giống nhãn chủ đề (ví dụ orders_changed). Payload là một chuỗi văn bản ngắn bạn đính kèm (ví dụ id đơn hàng). PostgreSQL không ép cấu trúc nào, nên các nhóm thường gửi các chuỗi JSON nhỏ.
Một thông báo có thể được kích hoạt từ mã ứng dụng (API server của bạn chạy NOTIFY) hoặc từ chính database bằng trigger (trigger chạy NOTIFY sau insert/update/delete).
Ở phía nhận, server của bạn mở một kết nối database và chạy LISTEN channel_name. Kết nối đó giữ mở. Khi có NOTIFY channel_name, 'payload', PostgreSQL đẩy thông báo tới tất cả kết nối đang lắng nghe kênh đó. App của bạn sau đó phản ứng (làm mới cache, truy vấn hàng thay đổi, đẩy event WebSocket về trình duyệt, v.v.).
NOTIFY nên được hiểu là một tín hiệu, không phải dịch vụ giao nhận:
Dùng theo cách này, PostgreSQL LISTEN/NOTIFY có thể cấp sức cho cập nhật UI trực tiếp mà không cần hạ tầng bổ sung.
LISTEN/NOTIFY tỏa sáng khi UI của bạn chỉ cần một cú chạm rằng có gì đó đổi, không cần luồng sự kiện đầy đủ. Nghĩ “làm mới widget này” hay “có mục mới” hơn là “xử lý từng click theo thứ tự.”
Nó hoạt động tốt khi database đã là nguồn chân thực và bạn muốn UI đồng bộ với nó. Mẫu phổ biến: ghi row, gửi thông báo nhỏ có ID, rồi UI (hoặc API) fetch trạng thái mới nhất.
LISTEN/NOTIFY thường đủ khi hầu hết điều sau đúng:
Ví dụ cụ thể: dashboard nội bộ hiển thị “tickets mở” và badge “ghi chú mới”. Khi agent thêm ghi chú, backend ghi vào Postgres và NOTIFY ticket_changed với ticket ID. Trình duyệt nhận qua WebSocket và refetch đúng card ticket đó. Không cần hạ tầng thêm, UI vẫn cảm giác trực tiếp.
LISTEN/NOTIFY có thể rất tốt lúc ban đầu, nhưng có giới hạn cứng. Những hạn chế đó hiện ra khi bạn dùng thông báo như hệ thống nhắn tin thay vì chỉ là một cú chạm nhẹ.
Khoảng cách lớn nhất là độ bền. Một NOTIFY không phải job có xếp hàng. Nếu không ai lắng nghe vào thời điểm đó, thông điệp bị mất. Ngay cả khi listener kết nối, crash, deploy, gián đoạn mạng, hoặc database restart có thể làm mất kết nối. Bạn sẽ không tự động nhận lại các thông báo đã bỏ lỡ.
Disconnect đặc biệt khó cho tính năng hướng tới người dùng. Hãy tưởng tượng dashboard hiển thị đơn hàng mới. Tab trình duyệt ngủ, WebSocket reconnect, và UI trông “đóng băng” vì bỏ lỡ vài sự kiện. Bạn có thể xử lý, nhưng cách xử lý không còn là “chỉ LISTEN/NOTIFY”: bạn phải rebuild state bằng cách query database và dùng NOTIFY chỉ như gợi ý để refresh.
Fan-out cũng là vấn đề phổ biến. Một event có thể đánh thức hàng trăm hoặc hàng nghìn listener (nhiều app server, nhiều người dùng). Nếu bạn dùng một kênh ồn ào như orders, mọi listener đều thức dậy dù chỉ một người quan tâm. Điều này có thể tạo ra đợt CPU và áp lực kết nối vào lúc xấu nhất.
Kích thước payload và tần suất là bẫy cuối cùng. Payload NOTIFY nhỏ, và sự kiện tần suất cao có thể tích tụ nhanh hơn khả năng xử lý của client.
Chú ý các dấu hiệu sau:
Lúc đó, giữ NOTIFY như một “cú chạm”, và chuyển độ tin cậy sang bảng hoặc message broker phù hợp.
Mẫu đáng tin cậy với LISTEN/NOTIFY là coi NOTIFY như một cú chạm, không phải nguồn chân thực. Row trong database là sự thật; thông báo chỉ báo app khi nào cần nhìn lại.
Thực hiện ghi trong transaction, và chỉ gửi thông báo sau khi thay đổi dữ liệu đã được commit. Nếu notify quá sớm, client có thể thức dậy và không thấy dữ liệu.
Một thiết lập phổ biến là trigger fire trên INSERT/UPDATE và gửi một thông điệp nhỏ.
NOTIFY dashboard_updates, '{\\\"type\\\":\\\"order_changed\\\",\\\"order_id\\\":123}'::text;
Đặt tên kênh phù hợp với cách mọi người nghĩ về hệ thống. Ví dụ: dashboard_updates, user_notifications, hoặc theo tenant như tenant_42_updates.
Giữ payload nhỏ. Đưa id và type, không phải toàn bộ record. Một dạng mặc định hữu dụng là:
type (đã xảy ra gì)id (cái gì thay đổi)tenant_id hoặc user_idĐiều này giữ băng thông thấp và tránh rò rỉ dữ liệu nhạy cảm vào logs thông báo.
Kết nối sẽ rớt. Lên kế hoạch cho điều đó.
Khi kết nối, chạy LISTEN cho tất cả kênh cần. Khi ngắt, reconnect với backoff ngắn. Khi reconnect, LISTEN lại (subscription không tồn tại trên kết nối mới). Sau reconnect, làm một refetch nhanh các “thay đổi gần đây” để bù trừ các sự kiện có thể đã bị bỏ lỡ.
Với hầu hết cập nhật UI trực tiếp, refetch là an toàn nhất: client nhận {type, id} rồi hỏi server trạng thái mới nhất.
Patch gia tăng có thể nhanh hơn, nhưng dễ sai (sự kiện lệch thứ tự, thất bại từng phần). Cách cân bằng tốt: refetch các lát nhỏ (một hàng order, một card ticket, một badge count) và để các aggregate nặng hơn trên timer ngắn.
Khi bạn từ một dashboard admin đơn lẻ tới nhiều user theo dõi cùng một số liệu, thói quen tốt quan trọng hơn SQL thông minh. LISTEN/NOTIFY vẫn có thể hoạt động tốt, nhưng bạn phải định hình luồng sự kiện từ database tới browser.
Mẫu cơ bản: mỗi instance app mở một kết nối lâu dài LISTEN, rồi đẩy cập nhật tới clients kết nối. “Một listener cho mỗi instance” đơn giản và thường ổn nếu bạn có ít app server và có thể chịu reconnect thỉnh thoảng.
Nếu bạn có nhiều instance app (hoặc serverless), một service listener chia sẻ có thể dễ quản lý hơn. Một process nhỏ lắng nghe một lần, rồi fan-out cập nhật cho phần còn lại của stack. Nó cũng cho bạn một nơi để thêm batching, metrics và backpressure.
Đối với browser, thường đẩy bằng WebSockets (hai chiều, tốt cho UI tương tác) hoặc Server-Sent Events (SSE) (một chiều, đơn giản hơn cho dashboard). Dù cách nào, tránh gửi “làm mới mọi thứ.” Gửi tín hiệu gọn như “order 123 thay đổi” để UI chỉ refetch những gì cần.
Để tránh UI rung lắc, thêm vài biện pháp bảo vệ:
Thiết kế kênh cũng quan trọng. Thay vì một kênh toàn cục, phân vùng theo tenant, team hoặc feature để client chỉ nhận sự kiện liên quan. Ví dụ: notify:tenant_42:billing và notify:tenant_42:ops.
LISTEN/NOTIFY có vẻ đơn giản, nên nhiều đội triển khai nhanh rồi bị bất ngờ khi chạy production. Hầu hết vấn đề đến từ việc đối xử nó như một queue đảm bảo.
Nếu app reconnect (deploy, mạng bị gián đoạn, DB failover), mọi NOTIFY gửi lúc bạn offline là mất. Cách sửa là coi thông báo như một tín hiệu rồi kiểm tra lại database.
Mẫu thực tế: lưu event thực vào một bảng (có id và created_at), rồi khi reconnect fetch mọi event mới hơn id cuối bạn thấy.
Payload LISTEN/NOTIFY không dành cho JSON lớn. Payload lớn tạo thêm parsing, công việc, và nguy cơ chạm giới hạn.
Dùng payload cho gợi ý nhỏ như "order:123". Rồi app đọc trạng thái mới nhất từ database.
Sai lầm phổ biến là thiết kế UI dựa vào nội dung payload như thể đó là nguồn chân thực. Điều này làm thay đổi schema và phiên bản client trở nên rắc rối.
Giữ tách biệt rõ: notify rằng có gì đó thay đổi, rồi fetch dữ liệu hiện tại bằng query bình thường.
Trigger NOTIFY trên mọi thay đổi hàng có thể làm ngập hệ thống, đặc biệt với các bảng bận rộn.
Chỉ notify khi có chuyển trạng thái ý nghĩa (ví dụ status thay đổi). Nếu cập nhật quá ồn, gom thông báo (một notify mỗi transaction hoặc theo cửa sổ thời gian) hoặc chuyển các cập nhật ồn ra khỏi đường dẫn notify.
Dù DB có thể gửi thông báo, UI vẫn có thể nghẽn. Dashboard re-render trên mọi sự kiện có thể làm treo.
Debounce cập nhật trên client, gom burst thành một lần refresh, và ưu tiên “invalidate và refetch” hơn “áp dụng mọi delta.” Ví dụ: bell notification có thể cập nhật tức thì, nhưng dropdown chỉ refresh tối đa một vài giây một lần.
LISTEN/NOTIFY tuyệt khi bạn muốn một tín hiệu nhỏ để app fetch dữ liệu mới. Nó không phải hệ thống nhắn tin đầy đủ.
Trước khi xây UI xung quanh nó, trả lời các câu sau:
Quy tắc thực tế: nếu bạn có thể coi NOTIFY là cú chạm (“đi đọc lại row”) hơn là payload, bạn đang ở vùng an toàn.
Ví dụ: dashboard admin hiển thị số đơn mới. Nếu một thông báo bị bỏ lỡ, poll tiếp theo hoặc refresh trang vẫn cho con số đúng. Đó là một tình huống phù hợp. Nhưng nếu bạn gửi "charge this card" hay "ship this package" thì bỏ lỡ là một sự cố thực sự.
Giả sử một app bán hàng nhỏ: dashboard hiển thị doanh thu hôm nay, tổng đơn hàng và danh sách “đơn gần đây”. Đồng thời mỗi nhân viên bán hàng cần một thông báo nhanh khi đơn họ sở hữu được trả hoặc gửi.
Cách đơn giản là coi PostgreSQL là nguồn chân thực, và dùng LISTEN/NOTIFY chỉ như cú chạm rằng có gì đó thay đổi.
Khi đơn được tạo hoặc trạng thái thay đổi, backend thực hiện hai việc trong một request: ghi row (hoặc update) và sau đó gửi NOTIFY với payload nhỏ (thường chỉ order ID và loại event). UI không dựa vào payload NOTIFY cho toàn bộ dữ liệu.
Luồng thực tế như sau:
orders_events với {\\\"type\\\":\\\"status_changed\\\",\\\"order_id\\\":123}.Điều này giữ NOTIFY nhẹ và giới hạn các truy vấn tốn kém.
Khi traffic tăng, các vết nứt xuất hiện: đợt event có thể làm quá tải một listener, thông báo bị bỏ lỡ khi reconnect, và bạn bắt đầu cần delivery đảm bảo và replay. Thường lúc đó bạn thêm một lớp đáng tin cậy hơn (outbox table + worker, rồi broker nếu cần) trong khi vẫn giữ Postgres là nguồn chân thực.
LISTEN/NOTIFY tuyệt khi bạn cần một tín hiệu nhanh. Nó không được thiết kế thành hệ thống nhắn tin đầy đủ. Khi bạn bắt đầu dựa vào event như nguồn chân thực, đã đến lúc bổ sung broker.
Nếu xuất hiện bất kỳ điều này, broker sẽ cứu bạn khỏi nhiều phiền toái:
LISTEN/NOTIFY không lưu messages để xử lý sau. Nó là tín hiệu push, không phải log được giữ. Điều đó phù hợp cho “làm mới widget dashboard”, nhưng rủi ro cho “thực hiện thanh toán” hoặc “giao hàng”.
Broker cho bạn mô hình luồng tin thật sự: queue (công việc cần làm), topic (phát cho nhiều bên), retention (giữ message từ vài phút đến vài ngày), và acknowledgments (consumer xác nhận đã xử lý). Điều này cho phép tách “DB thay đổi” khỏi “mọi thứ phải xảy ra vì thay đổi đó”.
Bạn không cần chọn công cụ phức tạp nhất. Các lựa chọn phổ biến: Redis (pub/sub hoặc streams), NATS, RabbitMQ, Kafka. Tùy chọn đúng phụ thuộc vào bạn cần queue đơn giản, fan-out nhiều service, hay khả năng replay lịch sử.
Bạn có thể chuyển mà không viết lại lớn. Mẫu thực tế là giữ NOTIFY làm wake-up trong khi broker trở thành nguồn giao nhận.
Bắt đầu bằng ghi một “event row” vào bảng trong cùng transaction với thay đổi nghiệp vụ, rồi một worker publish event đó lên broker. Trong quá trình chuyển giao, NOTIFY vẫn báo lớp UI “check for new events”, trong khi worker nền tiêu thụ từ broker với retry và auditing.
Cách này dashboard vẫn mượt, và workflow quan trọng không còn phụ thuộc vào thông báo best-effort.
Chọn một màn hình (một ô dashboard, một badge, một toast “thông báo mới”) và nối end-to-end. Với LISTEN/NOTIFY bạn có thể có kết quả hữu ích nhanh, miễn là giữ phạm vi chặt và đo đạc khi chạy traffic thực.
Bắt đầu với mẫu đơn giản đáng tin: ghi row, commit, rồi phát một tín hiệu nhỏ rằng có gì đó thay đổi. Ở UI, phản ứng với tín hiệu bằng cách fetch trạng thái mới (hoặc lát bạn cần). Điều này giữ payload nhỏ và tránh lỗi tinh vi khi messages đến lệch thứ tự.
Thêm observability cơ bản sớm. Bạn không cần công cụ cao cấp ngay lúc đầu, nhưng bạn cần câu trả lời khi hệ thống ồn ào:
Giữ hợp đồng đơn giản và viết ra. Quyết định tên kênh, tên event và cấu trúc payload (dù chỉ là ID). Một “event catalog” ngắn trong repo ngăn drift.
Nếu bạn muốn xây nhanh và giữ stack nhỏ, nền tảng như Koder.ai (koder.ai) có thể giúp bạn triển khai phiên bản đầu với UI React, backend Go và PostgreSQL, rồi lặp khi yêu cầu rõ hơn.
Dùng LISTEN/NOTIFY khi bạn chỉ cần một tín hiệu nhanh rằng có gì đó thay đổi, như làm mới số lượng badge hoặc một ô dashboard. Xem thông báo như một cú chạm để refetch dữ liệu thực từ bảng, chứ không phải là nguồn dữ liệu chính.
Polling kiểm tra thay đổi theo lịch, nên người dùng thường thấy cập nhật muộn và server thực hiện công việc ngay cả khi không có thay đổi. LISTEN/NOTIFY đẩy một tín hiệu nhỏ ngay khi thay đổi xảy ra, thường cảm giác nhanh hơn và tránh nhiều request rỗng.
Không, nó là best-effort. Nếu listener bị ngắt kết nối trong lúc NOTIFY xảy ra, listener đó có thể bỏ lỡ vì thông báo không được lưu để phát lại sau.
Giữ payload nhỏ và coi nó như một gợi ý. Một mặc định hữu dụng là một chuỗi JSON nhỏ chỉ gồm type và id, rồi ứng dụng sẽ truy vấn Postgres để lấy trạng thái hiện tại.
Một mô hình phổ biến là gửi thông báo sau khi ghi đã được commit. Nếu bạn notify quá sớm, client có thể thức dậy và không tìm thấy hàng mới.
Code ứng dụng thường dễ hiểu và dễ test hơn vì nó tường minh. Trigger hữu ích khi nhiều writer chạm vào cùng bảng và bạn muốn hành vi nhất quán bất kể ai sửa dữ liệu.
Xem reconnect là hành vi bình thường. Khi reconnect, chạy lại LISTEN cho các kênh cần thiết và thực hiện một refetch nhanh các thay đổi gần đây để bù những gì có thể đã bị bỏ lỡ khi offline.
Đừng để mỗi browser kết nối trực tiếp tới Postgres. Một cấu hình điển hình là mỗi instance backend mở một kết nối lâu dài rồi chuyển tiếp cập nhật tới browser qua WebSocket hoặc SSE, và UI sẽ refetch dữ liệu cần thiết.
Dùng các kênh hẹp hơn để chỉ những consumer phù hợp bị đánh thức, và gom nhóm các đợt ồn ào. Debounce vài trăm ms và gom các cập nhật trùng lặp để UI và backend không bị quá tải.
Nâng cấp khi bạn cần độ bền, retry, nhóm consumer, đảm bảo thứ tự, hoặc audit/replay. Nếu việc bỏ lỡ một sự kiện có thể gây sự cố nghiêm trọng (billing, giao hàng), hãy dùng outbox + worker hoặc broker thay vì chỉ dựa vào NOTIFY.