Lựa chọn và chuẩn bị PostgreSQL cho Event Store
Tại sao chọn PostgreSQL thay vì MongoDB cho bài thực hành này
Trong kiến trúc Event Sourcing, tính toàn vẹn dữ liệu (ACID) và khả năng truy vấn phức tạp theo chuỗi sự kiện là yếu tố sống còn.
Mặc dù MongoDB hỗ trợ lưu trữ dạng JSON linh hoạt, nhưng PostgreSQL cung cấp sự đảm bảo về transaction (giao dịch) chặt chẽ hơn cho các Aggregate có quan hệ phụ thuộc, đồng thời hỗ trợ tốt việc truy vấn theo phiên bản (Version) và Aggregate ID nhờ chỉ mục B-Tree hiệu năng cao.
Chúng ta sẽ sử dụng PostgreSQL 16 (mặc định trên Ubuntu 24.04) để xây dựng Event Store.
Cài đặt và khởi động dịch vụ PostgreSQL
Bước đầu tiên là đảm bảo PostgreSQL đã được cài đặt và đang chạy trên máy chủ Ubuntu 24.04 của bạn.
sudo apt update && sudo apt install postgresql postgresql-contrib -y
Kết quả mong đợi: Dịch vụ được cài đặt và khởi động tự động. Bạn sẽ thấy thông báo "Processing triggers for man-db" và không có lỗi.
Chuyển sang user postgres để thực hiện các cấu hình quản trị ban đầu.
sudo -i -u postgres
Kết quả mong đợi: Prompt dòng lệnh thay đổi từ `user@host` sang `postgres@host`.
Chạy lệnh để truy cập giao diện shell của PostgreSQL (psql).
psql
Kết quả mong đợi: Giao diện shell xuất hiện với prompt `postgres=#`.
Thiết kế Schema cho Event Store
Tạo Database và Schema chuyên biệt
Tạo một database riêng biệt tên là `event_store_db` để tách biệt hoàn toàn với database Read Models hoặc Command Side, tránh xung đột namespace và dễ dàng backup/xử lý sự cố.
CREATE DATABASE event_store_db;
Kết quả mong đợi: Thông báo `CREATE DATABASE`.
Chuyển ngữ cảnh sang database vừa tạo.
\c event_store_db
Kết quả mong đợi: Thông báo `You are now connected to database "event_store_db" as user "postgres"`.
Xây dựng bảng lưu trữ Event (Event Store Table)
Bảng này là trái tim của Event Sourcing. Nó lưu trữ toàn bộ lịch sử thay đổi trạng thái của Aggregate.
Các trường bắt buộc:
- id: Primary Key tự tăng.
- aggregate_id: Khóa duy nhất của Aggregate (ví dụ: OrderID, CustomerID).
- aggregate_type: Tên lớp của Aggregate để phân loại sự kiện.
- event_type: Tên sự kiện đã xảy ra (ví dụ: "OrderCreated", "OrderShipped").
- data: Payload chứa dữ liệu thay đổi trạng thái, lưu dưới dạng JSONB để tối ưu query và lưu trữ.
- version: Số phiên bản để đảm bảo Optimistic Concurrency Control (kiểm soát phiên bản).
- metadata: JSONB chứa thông tin bổ sung như timestamp, correlation_id, user_id.
Chạy lệnh SQL dưới đây để tạo bảng với các ràng buộc và chỉ mục cần thiết.
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
aggregate_id VARCHAR(255) NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
event_type VARCHAR(100) NOT NULL,
data JSONB NOT NULL,
version INTEGER NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Kết quả mong đợi: Thông báo `CREATE TABLE`.
Tối ưu hóa chỉ mục (Indexing) cho Event Store
Việc đọc Event Store thường diễn ra theo 2 chiều: đọc lịch sử của một Aggregate cụ thể (theo aggregate_id) và đọc toàn bộ sự kiện theo thứ tự thời gian (stream global).
Tạo chỉ mục chính cho việc đọc lịch sử của một Aggregate theo thứ tự version tăng dần. Đây là chỉ mục quan trọng nhất để Rebuild State.
CREATE INDEX idx_events_aggregate_version ON events (aggregate_id, version ASC);
Kết quả mong đợi: Thông báo `CREATE INDEX`.
Tạo chỉ mục để truy vấn sự kiện theo loại sự kiện (event_type) khi cần tìm kiếm toàn cục hoặc xử lý lỗi.
CREATE INDEX idx_events_event_type ON events (event_type);
Kết quả mong đợi: Thông báo `CREATE INDEX`.
Tạo chỉ mục GIN cho trường metadata để hỗ trợ query nhanh các thuộc tính bên trong JSON.
CREATE INDEX idx_events_metadata ON events USING GIN (metadata);
Kết quả mong đợi: Thông báo `CREATE INDEX`.
Cấu hình lưu trữ Snapshot
Thiết kế bảng Snapshot
Snapshot là ảnh chụp trạng thái hiện tại của Aggregate tại một version cụ thể. Việc lưu Snapshot giúp giảm số lượng Event cần đọc và xử lý khi khôi phục trạng thái (Replay), từ đó tối ưu hiệu năng Query Side.
Bảng snapshots lưu trữ:
- aggregate_id: Khóa chính cùng với version.
- version: Phiên bản mà snapshot được tạo.
- state: Toàn bộ trạng thái của Aggregate tại thời điểm đó dưới dạng JSONB.
Chạy lệnh tạo bảng snapshots.
CREATE TABLE snapshots (
id BIGSERIAL PRIMARY KEY,
aggregate_id VARCHAR(255) NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
version INTEGER NOT NULL,
state JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_snapshot_per_version UNIQUE (aggregate_id, version)
);
Kết quả mong đợi: Thông báo `CREATE TABLE`.
Tạo chỉ mục cho Snapshot
Chỉ mục này cho phép tìm kiếm nhanh snapshot mới nhất của một Aggregate để bắt đầu quá trình Replay các Event mới hơn.
CREATE INDEX idx_snapshots_aggregate_id_version ON snapshots (aggregate_id, version DESC);
Kết quả mong đợi: Thông báo `CREATE INDEX`.
Script khởi tạo và Verify kết quả
Tạo script SQL khởi tạo toàn bộ Database
Để dễ dàng tái tạo môi trường (Idempotent), chúng ta sẽ xuất toàn bộ các lệnh trên vào một file SQL duy nhất.
Tạo file `/var/lib/postgresql/event_store_init.sql`.
cat > /var/lib/postgresql/event_store_init.sql
Kết quả mong đợi: File được tạo thành công không lỗi.
Thực thi script và kiểm tra kết quả
Chạy script khởi tạo thông qua psql. Đảm bảo bạn đang ở user postgres.
psql -f /var/lib/postgresql/event_store_init.sql
Kết quả mong đợi: Xuất hiện chuỗi các thông báo `CREATE DATABASE`, `CREATE TABLE`, `CREATE INDEX`, và `INSERT 0 1` (cho mỗi dòng dữ liệu mẫu).
Chạy lệnh kiểm tra để xác minh cấu trúc bảng và dữ liệu mẫu đã được lưu trữ đúng định dạng JSONB.
psql -d event_store_db -c "SELECT id, aggregate_id, event_type, version, data FROM events WHERE aggregate_id = 'order-123';"
Kết quả mong đợi: Hiển thị 2 dòng dữ liệu tương ứng với sự kiện OrderCreated và OrderShipped với version 1 và 2.
Kiểm tra bảng Snapshot để đảm bảo dữ liệu trạng thái đã được lưu.
psql -d event_store_db -c "SELECT aggregate_id, version, state FROM snapshots WHERE aggregate_id = 'order-123';"
Kết quả mong đợi: Hiển thị 1 dòng với version 2 và state chứa đầy đủ thông tin trạng thái hiện tại của Order.
Thoát khỏi psql và trở về user root để kết thúc bước thiết lập.
exit
Kết quả mong đợi: Prompt trở về `postgres@host`.
exit
Kết quả mong đợi: Prompt trở về `user@host`.
Điều hướng series:
Mục lục: Series: Triển khai Database CQRS với Event Sourcing và Ubuntu 24.04
« Phần 2: Khái niệm nền tảng: CQRS và Event Sourcing
Phần 4: Triển khai Command Side và xử lý Write Operations »