Khi ứng dụng cần cập nhật theo thời gian thực, câu trả lời mặc định trong năm 2025 là chọn một dịch vụ được quản lý: Firebase Realtime Database, Supabase Realtime, Pusher, Ably hoặc một lớp WebSocket được lưu trữ. Các dịch vụ này che đi sự phức tạp của việc đẩy cập nhật tới các máy khách đã kết nối. Chúng hoạt động tốt — cho đến khi không còn như vậy.
Tại Atlas Verified, chúng tôi bắt đầu với một nền tảng cơ sở dữ liệu được quản lý có đăng ký cập nhật theo thời gian thực. Khi hệ thống lớn lên, chúng tôi chạm tới các giới hạn của lớp trừu tượng đó. Các ràng buộc của việc gộp kết nối, vấn đề độ tin cậy theo thời gian thực, nhu cầu truy vấn không gian PostGIS và mong muốn có logic trigger tùy chỉnh đều thúc đẩy chúng tôi cùng một hướng: kiểm soát lớp cơ sở dữ liệu.
Chúng tôi chuyển sang PostgreSQL tự lưu trữ và xây dựng hệ thống thời gian thực bằng cơ chế LISTEN/NOTIFY gốc của PostgreSQL kết hợp với Server-Sent Events. Bài viết này trình bày lý do chúng tôi đưa ra quyết định đó, cách kiến trúc hoạt động và những điều đã học được.
Vì sao chúng tôi rời nền tảng được quản lý
Các nền tảng cơ sở dữ liệu được quản lý đưa ra một giá trị hấp dẫn: xác thực, bảo mật cấp hàng, đăng ký cập nhật thời gian thực, API được tạo tự động và bảng điều khiển — tất cả đều có sẵn. Đối với sản phẩm giai đoạn đầu, điều này thực sự có giá trị. Bạn có thể xây dựng một ứng dụng hoạt động mà không viết một dòng mã backend nào.
Các vấn đề xuất hiện khi ứng dụng trưởng thành và yêu cầu của bạn khác với các giả định của nền tảng.
Giới hạn kết nối. Các nền tảng được quản lý thường định tuyến kết nối cơ sở dữ liệu qua một bộ gộp kết nối áp đặt các giới hạn riêng. Khi có nhiều dịch vụ backend, mỗi dịch vụ cần nhóm kết nối riêng — máy chủ API, hệ thống tác vụ nền, bộ lắng nghe thời gian thực — bạn nhanh chóng dùng hết các kết nối sẵn có. Chúng tôi nhận thấy mình đang thiết kế giải pháp để né các giới hạn của bộ gộp kết nối thay vì giải quyết vấn đề thực tế của sản phẩm.
Độ tin cậy theo thời gian thực. Hệ thống thời gian thực của nền tảng được quản lý hoạt động bằng Change Data Capture trên Write-Ahead Log của PostgreSQL. Kiến trúc này tinh tế nhưng tạo ra các dạng lỗi mà chúng tôi không thể kiểm soát. Sao chép dựa trên WAL vốn có độ trễ. Dịch vụ thời gian thực đôi khi bị chậm, cung cấp các cập nhật đã cũ. Việc kết nối lại sau gián đoạn mạng không đáng tin cậy. Và gỡ lỗi các vấn đề này đòi hỏi quyền truy cập hạ tầng mà chúng tôi không kiểm soát.
Yêu cầu về phần mở rộng. Ứng dụng của chúng tôi xử lý tài liệu thương mại có dữ liệu địa lý: vị trí cảng, tuyến vận chuyển và vị trí tàu. PostGIS — phần mở rộng không gian cho PostgreSQL — là thiết yếu cho các truy vấn này. Nền tảng được quản lý hỗ trợ PostGIS, nhưng chúng tôi cần các phiên bản và cấu hình cụ thể không phải lúc nào cũng sẵn có.
Logic trigger tùy chỉnh. Chúng tôi cần trigger cơ sở dữ liệu phát ra các sự kiện riêng của ứng dụng — không chỉ các thông báo chung "hàng đã thay đổi". Khi trạng thái xử lý tài liệu thay đổi, chúng tôi cần thông báo cho đúng máy khách đang kết nối đã tải tài liệu lên. Hệ thống thời gian thực của nền tảng được quản lý hoạt động ở cấp bảng; chúng tôi cần cấp thực thể.
Quyết định chuyển đổi không xuất phát từ chi phí hay nguyên tắc. Nó mang tính thực dụng: chúng tôi dành nhiều thời gian né các ràng buộc của nền tảng hơn thời gian cần để tự xây dựng các tính năng.
LISTEN/NOTIFY: Cơ chế Pub/Sub ẩn của PostgreSQL
PostgreSQL có cơ chế xuất bản/đăng ký tích hợp từ phiên bản 9.0, phát hành năm 2010. Dù đã hơn 15 năm tuổi, LISTEN/NOTIFY vẫn ít được sử dụng một cách đáng ngạc nhiên. Phần lớn nhà phát triển không biết nó tồn tại.
Cơ chế rất đơn giản. Bất kỳ kết nối cơ sở dữ liệu nào cũng có thể thực thi LISTEN trên một kênh có tên để đăng ký. Bất kỳ kết nối nào cũng có thể thực thi NOTIFY để gửi thông điệp tới mọi bên đang lắng nghe kênh đó. Nội dung thông điệp là một chuỗi, giới hạn 8,000 byte.
Điều khiến LISTEN/NOTIFY mạnh mẽ đối với ứng dụng thời gian thực:
Không cần thêm hạ tầng. Không có bộ môi giới thông điệp riêng cần triển khai, giám sát hoặc mở rộng. Nếu có PostgreSQL, bạn có pub/sub. Không cần Redis, RabbitMQ hay Kafka — cho trường hợp sử dụng cụ thể này.
Bảo đảm theo giao dịch. Thông điệp NOTIFY chỉ được chuyển khi giao dịch bao quanh nó được commit. Nếu một giao dịch chèn một hàng và gửi thông báo, nhưng sau đó được rollback, thông báo sẽ không bao giờ được chuyển. Đây là đặc tính mà bạn không có được từ các bộ môi giới thông điệp bên ngoài nếu không điều phối cẩn thận.
Tích hợp trigger. Trigger PostgreSQL có thể gọi pg_notify trong quá trình thực thi. Điều này có nghĩa là bạn có thể để cơ sở dữ liệu tự động thông báo cho các bên lắng nghe mỗi khi dữ liệu cụ thể thay đổi, mà không cần mã ứng dụng trong luồng ghi.
Các giới hạn là có thật và cần được hiểu rõ:
Không lưu giữ lâu dài. Nếu không ai lắng nghe khi NOTIFY được kích hoạt, thông điệp sẽ bị mất. Không có phát lại, không có hàng đợi thông điệp, không có bảo đảm chuyển tới máy khách đã ngắt kết nối. Các bên lắng nghe phải xử lý việc kết nối lại và khôi phục trạng thái.
Kích thước nội dung thông điệp. Giới hạn 8,000 byte của nội dung thông báo nghĩa là bạn không thể gửi đối tượng lớn qua NOTIFY. Chúng tôi gửi nội dung tối thiểu — tên bảng, loại thao tác và khóa chính — rồi để máy khách lấy dữ liệu đầy đủ nếu cần.
Phạm vi một cơ sở dữ liệu. LISTEN/NOTIFY hoạt động trong một phiên bản PostgreSQL duy nhất. Nó không trải qua các bản sao hoặc cụm. Với kiến trúc mà mọi thao tác ghi đều tới một máy chủ chính, điều này không phải là giới hạn.
Kiến trúc của chúng tôi: Trigger, SSE và ghép kênh
Đây là cách các thành phần kết hợp với nhau.
Trigger cơ sở dữ liệu được cài đặt trên những bảng cần cập nhật thời gian thực: tài liệu, tin nhắn, hội thoại, thông báo, xác minh lô hàng và nhiều bảng khác. Khi một hàng được chèn, cập nhật hoặc xóa, trigger kích hoạt và gọi pg_notify với nội dung JSON chứa tên bảng, loại thao tác, khóa chính và các cột lọc nếu có.
Một kết nối lắng nghe chuyên dụng trong máy chủ API của chúng tôi thực thi LISTEN trên kênh thông báo và nhận mọi sự kiện trigger. Đây là kết nối PostgreSQL chuyên dụng, tồn tại lâu dài — tách biệt với nhóm kết nối truy vấn — chỉ làm nhiệm vụ lắng nghe thông báo.
Một bus sự kiện nhận thông báo từ bộ lắng nghe và định tuyến chúng tới các máy khách đang kết nối dựa trên đăng ký của họ. Một máy khách đăng ký tài liệu khớp với một mã lô hàng cụ thể chỉ nhận thông báo về tài liệu trong lô hàng đó.
Server-Sent Events chuyển thông báo tới trình duyệt. Mỗi máy khách đang kết nối mở một kết nối SSE duy nhất tới máy chủ API, chỉ định các kênh và bộ lọc mong muốn. Bus sự kiện đối chiếu thông báo nhận được với đăng ký của máy khách và đẩy các sự kiện khớp qua kết nối SSE phù hợp.
Vì sao chọn SSE thay vì WebSockets
Đây là một quyết định mà chúng tôi luôn hài lòng. Server-Sent Events đơn giản hơn WebSockets ở mọi phương diện quan trọng đối với trường hợp sử dụng của chúng tôi.
Được thiết kế một chiều. Nhu cầu thời gian thực của chúng tôi là từ máy chủ tới máy khách: máy chủ cho máy khách biết khi dữ liệu thay đổi. Máy khách không cần gửi thông điệp thời gian thực tới máy chủ — nó dùng yêu cầu HTTP tiêu chuẩn cho việc đó. Mô hình một chiều của SSE phù hợp với yêu cầu thực tế của chúng tôi.
Gốc HTTP. Kết nối SSE là yêu cầu HTTP tiêu chuẩn với một loại nội dung cụ thể. Chúng hoạt động qua mọi proxy HTTP, bộ cân bằng tải và CDN mà không cần cấu hình đặc biệt. WebSockets cần HTTP Upgrade, mà một số proxy xử lý không đúng và một số tường lửa doanh nghiệp chặn hoàn toàn.
Kết nối lại tự động. API EventSource của trình duyệt xử lý việc kết nối lại theo cơ chế tích hợp sẵn. Nếu kết nối bị ngắt, trình duyệt tự động kết nối lại với thời gian chờ tăng theo cấp số nhân. Với WebSockets, bạn phải tự triển khai logic kết nối lại — và ít nhất một lần sẽ làm sai.
Lĩnh vực duy nhất mà WebSockets vượt trội — giao tiếp hai chiều — đơn giản là chúng tôi không cần. Với ứng dụng cần gửi thông điệp thời gian thực từ máy khách tới máy chủ, WebSockets là lựa chọn đúng. Với ứng dụng đẩy thông báo từ máy chủ tới máy khách, SSE là lựa chọn đúng.
Xây dựng lớp truy vấn không dùng ORM
Khi rời nền tảng được quản lý, chúng tôi cũng bỏ API truy vấn được tạo tự động của nó. Chúng tôi cần một lớp truy cập cơ sở dữ liệu cho máy chủ API.
Các lựa chọn tiêu chuẩn là ORM như Prisma, TypeORM hoặc Drizzle — hoặc SQL trực tiếp với trình điều khiển. Chúng tôi chọn con đường thứ ba: một bộ xây dựng truy vấn nhẹ cung cấp API có thể nối chuỗi, đồng thời tạo và thực thi SQL trực tiếp ở bên dưới.
API quen thuộc. Mã nguồn hiện có của chúng tôi được viết để dùng thư viện máy khách của nền tảng được quản lý. Một giao diện API tương thích giúp giảm tối đa thay đổi mã khi chuyển đổi. Những truy vấn trước đây chạy qua máy khách của nền tảng có thể chạy qua bộ xây dựng truy vấn của chúng tôi với cú pháp giống hệt.
Không có phép thuật. ORM tạo ra SQL mà bạn không nhìn thấy và đôi khi không thể kiểm soát. Khi truy vấn chậm, bạn cần đọc SQL được tạo ra, vốn thường ít giống lời gọi ORM đã tạo nó. Bộ xây dựng truy vấn của chúng tôi tạo SQL có thể dự đoán — mỗi phương thức được nối chuỗi ánh xạ tới một mệnh đề SQL cụ thể, và bạn luôn có thể kiểm tra truy vấn được tạo.
Phép nối nhúng. Dữ liệu thương mại có tính quan hệ. Một lô hàng có tài liệu, tài liệu có các nút, và các nút có các kiểm tra xác minh. Khả năng diễn đạt các phép chọn lồng nhau và nhận kết quả lồng nhau — mà không phải tự viết truy vấn JOIN phức tạp mỗi lần — cải thiện đáng kể trải nghiệm nhà phát triển.
Kết quả là một lớp truy vấn chỉ gồm vài trăm dòng mã, xử lý phần lớn tương tác cơ sở dữ liệu của chúng tôi và tạo SQL mà chúng tôi luôn có thể đọc và tối ưu.
Phân bổ ngân sách nhóm kết nối
Khi tự lưu trữ PostgreSQL, bạn chịu trách nhiệm về ngân sách kết nối. Thiết lập max_connections của PostgreSQL xác định giới hạn tuyệt đối, và mỗi kết nối đều tiêu thụ bộ nhớ.
Chúng tôi nhận thấy việc xem kết nối như một ngân sách — một tài nguyên hữu hạn được chia sẻ giữa các dịch vụ — ngăn được một nhóm vấn đề thường gây khó khăn cho những nhóm không lên kế hoạch trước.
Mỗi dịch vụ khai báo số kết nối tối đa. Tổng mọi mức tối đa phải thấp hơn đáng kể giới hạn PostgreSQL, để còn dư cho các kết nối phát sinh — phiên psql, tập lệnh chuyển đổi và công cụ giám sát. Điều này nghe có vẻ hiển nhiên, nhưng hầu hết các nhóm không làm cho đến khi cạn kết nối trong môi trường vận hành. Nhờ phân bổ ngân sách kết nối từ đầu, chúng tôi đã tránh được loại cảnh báo lúc 3 giờ sáng đó.
Chuyển đổi an toàn ở quy mô lớn
Chuyển từ nền tảng được quản lý sang PostgreSQL tự lưu trữ đồng nghĩa với việc chuyển hàng trăm bảng, hàm, trigger và chỉ mục. Nhưng thách thức lớn hơn là xây dựng một hệ thống chuyển đổi tiếp tục hoạt động khi ứng dụng phát triển.
Hệ thống chuyển đổi tự động của chúng tôi hoạt động như sau: các tệp chuyển đổi được đặt tên bằng dấu thời gian và nằm trong một thư mục chuyên dụng. Bộ chạy chuyển đổi quét thư mục, so sánh với bảng theo dõi ghi nhận các chuyển đổi đã áp dụng và thực thi mọi chuyển đổi đang chờ theo thứ tự. Mỗi chuyển đổi chạy trong một giao dịch — nếu thất bại, nó rollback hoàn toàn và chặn triển khai cho đến khi vấn đề được sửa.
Bộ chạy chuyển đổi tự động thực thi mỗi lần triển khai, trước khi ứng dụng khởi động. Điều này nghĩa là thay đổi lược đồ được áp dụng trong quá trình triển khai, không cần bước thủ công. Nhà phát triển tạo tệp SQL có dấu thời gian, đẩy lên kho mã và lần triển khai tiếp theo áp dụng nó.
Các quyết định thiết kế chính giúp bảo đảm an toàn: chuyển đổi dùng các cơ chế bảo vệ tính lũy đẳng để không thất bại nếu đã được áp dụng một phần. Mỗi chuyển đổi phải là một tập lệnh SQL độc lập chạy mà không cần con người nhập liệu. Và nếu chuyển đổi thất bại, toàn bộ giao dịch rollback — ứng dụng không khởi động với một thay đổi lược đồ mới áp dụng một phần.
Những gì chúng tôi đạt được
Chuyển sang PostgreSQL tự lưu trữ với hệ thống thời gian thực riêng là một khoản đầu tư kỹ thuật lớn. Nhưng lợi ích thu được rất đáng kể:
Độ tin cậy. Thông báo thời gian thực đến trong vòng vài mili giây sau khi dữ liệu cơ sở thay đổi. Không có độ trễ sao chép WAL, không có sự cố dịch vụ bên thứ ba trên đường xử lý quan trọng.
Khả năng gỡ lỗi. Khi có vấn đề, chúng tôi có thể truy vết toàn bộ đường đi: trigger kích hoạt, thông báo được gửi, bus sự kiện định tuyến và SSE chuyển giao. Mọi bước đều nằm trong mã, nhật ký và hạ tầng của chúng tôi.
Tính linh hoạt. Thêm tính năng thời gian thực cho một bảng mới chỉ cần một chuyển đổi SQL cài đặt trigger. Không cần cấu hình nền tảng, bảng điều khiển bên thứ ba hay yêu cầu hỗ trợ.
Hiệu năng. Kết nối PostgreSQL trực tiếp với nhóm kết nối được tinh chỉnh có hiệu năng tốt hơn kết nối định tuyến qua hạ tầng của nền tảng được quản lý. Độ trễ truy vấn giảm đáng kể sau khi chuyển đổi.
Đánh đổi là trách nhiệm vận hành. Chúng tôi quản lý cơ sở dữ liệu, xử lý sao lưu, giám sát sao chép và lập kế hoạch công suất. Với một nhóm thành thạo vận hành PostgreSQL, điều này có thể quản lý được. Với nhóm chưa thành thạo, các lớp trừu tượng của nền tảng được quản lý có thể đáng giá dù có giới hạn.
Atlas Verified chạy trên PostgreSQL tự lưu trữ với hạ tầng thời gian thực riêng, xử lý hàng nghìn xác minh tuân thủ thương mại mỗi ngày trên toàn chuỗi cung ứng toàn cầu.
