Phân biệt mô hình CQRS với CRUD truyền thống
Trong mô hình CRUD (Create, Read, Update, Delete) truyền thống, một bảng dữ liệu duy nhất phục vụ cả thao tác ghi và đọc. Điều này dẫn đến xung đột khi nhiều người dùng truy cập đồng thời và làm phức tạp hóa việc tối ưu hóa riêng biệt cho từng loại thao tác.
Ngược lại, CQRS (Command Query Responsibility Segregation) tách biệt hoàn toàn trách nhiệm: Command Side chỉ xử lý việc thay đổi trạng thái (Write), trong khi Query Side chỉ phục vụ việc lấy dữ liệu (Read). Sự tách biệt này cho phép chúng ta tối ưu hóa từng phía độc lập, sử dụng cơ sở dữ liệu hoặc cấu trúc dữ liệu khác nhau cho từng mục đích.
Minh họa sự khác biệt qua cấu trúc thư mục
Để hình dung sự tách biệt này ngay từ khi thiết kế thư mục trên Ubuntu, chúng ta sẽ tạo cấu trúc phân tách rõ ràng thay vì gom chung vào một thư mục `app` duy nhất như trong CRUD.
Thực hiện lệnh tạo thư mục để mô phỏng kiến trúc tách biệt Command và Query:
mkdir -p /var/opt/myapp/cqrs/{commands,queries,event-store}
Kết quả mong đợi: Hệ thống tạo ra 3 thư mục con rõ ràng trong `/var/opt/myapp/cqrs`. Thư mục `commands` sẽ chứa logic ghi, `queries` chứa logic đọc, và `event-store` là nơi lưu trữ lịch sử sự kiện (Event Store) riêng biệt, không bị trộn lẫn với dữ liệu query.
Verify kết quả bằng cách liệt kê cây thư mục:
tree /var/opt/myapp/cqrs -L 1
Kết quả mong đợi: Hiển thị danh sách các thư mục `commands`, `queries`, `event-store` mà không có bất kỳ file nào khác nằm ngoài luồng kiến trúc này.
Nguyên lý hoạt động của Event Sourcing và lưu trữ lịch sử sự kiện
Event Sourcing là mô hình lưu trữ thay vì ghi đè trạng thái hiện tại (Current State). Mọi thay đổi của hệ thống được lưu dưới dạng một chuỗi các sự kiện (Events) theo thứ tự thời gian. Trạng thái hiện tại của một thực thể (Aggregate) được tính toán bằng cách "chơi lại" (replay) toàn bộ lịch sử sự kiện từ đầu.
Trong môi trường Linux server, chúng ta sẽ sử dụng file hệ thống hoặc database JSON để lưu trữ các event này dưới dạng plain text hoặc dòng JSON, đảm bảo tính minh bạch và khả năng audit cao.
Tạo cấu trúc Event Store mẫu
Bước đầu tiên là thiết lập nơi lưu trữ các sự kiện. Chúng ta sẽ tạo một file log mẫu chứa các sự kiện của một Aggregate (ví dụ: User) để minh họa cách dữ liệu được ghi nhận.
Tạo file event store đầu tiên và ghi vào 3 sự kiện mẫu (Created, Updated, Deleted):
cat > /var/opt/myapp/cqrs/event-store/users/1.json
Kết quả mong đợi: File `/var/opt/myapp/cqrs/event-store/users/1.json` được tạo thành công với nội dung JSON hợp lệ chứa 3 sự kiện theo thứ tự thời gian.
Xây dựng script Replay để tính toán trạng thái hiện tại
Để hiểu rõ nguyên lý Event Sourcing, chúng ta cần một script đơn giản đọc file event và tính toán trạng thái cuối cùng của User. Script này mô phỏng quá trình mà Query Side sẽ thực hiện khi cần hiển thị dữ liệu.
Tạo script shell `replay.sh` để đọc file event và in ra trạng thái cuối cùng:
cat > /var/opt/myapp/cqrs/scripts/replay.sh
Kết quả mong đợi: Script `replay.sh` được tạo và gán quyền thực thi. Script này sử dụng `jq` để duyệt qua mảng event và gộp dữ liệu lại thành một object trạng thái duy nhất.
Verify kết quả bằng cách chạy script với ID=1:
/var/opt/myapp/cqrs/scripts/replay.sh 1
Kết quả mong đợi: Terminal in ra trạng thái cuối cùng của user với email là `super_admin@example.com`, `is_locked` là `true` và `lock_reason` được ghi nhận, chứng tỏ quá trình replay đã tính toán đúng từ 3 sự kiện.
Vai trò của Command Side và Query Side trong kiến trúc
Trong kiến trúc CQRS, Command Side là nơi tiếp nhận yêu cầu thay đổi dữ liệu. Nó không trả về dữ liệu mà chỉ xác nhận việc sự kiện đã được ghi vào Event Store. Command Side chịu trách nhiệm xác thực (validation) và tạo ra sự kiện mới.
Query Side là nơi phục vụ các yêu cầu đọc dữ liệu. Nó không can thiệp vào Event Store mà đọc từ một Read Model (thường là bảng SQL hoặc cache) đã được đồng bộ hóa từ Event Store. Sự tách biệt này giúp Query Side có thể tối ưu hóa chỉ mục (indexing) và cấu trúc dữ liệu mà không ảnh hưởng đến tính toàn vẹn của Command Side.
Cấu hình Command Side: Xử lý Write Operation
Chúng ta sẽ mô phỏng Command Side bằng một script nhận dữ liệu và ghi thêm một sự kiện mới vào Event Store. Command Side chỉ quan tâm đến việc ghi log, không quan tâm đến việc dữ liệu hiển thị như thế nào.
Tạo script `command_handler.sh` để xử lý lệnh tạo sự kiện mới:
cat > /var/opt/myapp/cqrs/scripts/command_handler.sh "$TEMP_FILE"
mv "$TEMP_FILE" "$EVENT_FILE"
echo "Command executed successfully. Event ${EVENT_ID} persisted."
EOF
chmod +x /var/opt/myapp/cqrs/scripts/command_handler.sh
Kết quả mong đợi: Script `command_handler.sh` sẵn sàng. Nó đảm bảo tính nguyên tử khi ghi file bằng cách sử dụng file tạm và `mv`.
Verify kết quả bằng cách gửi một lệnh tạo sự kiện mới (ví dụ: UserUnlocked):
/var/opt/myapp/cqrs/scripts/command_handler.sh 1 "UserUnlocked" '{"reason": "Admin reset"}'
Kết quả mong đợi: Thông báo "Command executed successfully" được in ra. File `1.json` trong event-store sẽ có thêm 1 sự kiện mới ở cuối mảng.
Cấu hình Query Side: Xây dựng Read Model
Query Side sẽ không đọc trực tiếp file JSON raw như Command Side. Trong thực tế, Query Side sẽ có một cơ sở dữ liệu riêng (Read Model) được cập nhật bởi một quá trình đồng bộ. Ở đây, chúng ta mô phỏng Read Model bằng một file JSON đơn giản chứa trạng thái hiện tại, được cập nhật sau khi replay.
Tạo script `query_handler.sh` để đọc từ Read Model (mô phỏng) thay vì replay toàn bộ lịch sử:
cat > /var/opt/myapp/cqrs/scripts/query_handler.sh
Kết quả mong đợi: Script `query_handler.sh` được tạo. Nó chỉ đọc một file đơn giản, không cần tính toán lại lịch sử.
Để Query Side hoạt động, ta cần đồng bộ hóa dữ liệu từ Event Store sang Read Model. Chạy lệnh đồng bộ (mô phỏng quá trình projection):
mkdir -p /var/opt/myapp/cqrs/queries/users
/var/opt/myapp/cqrs/scripts/replay.sh 1 | jq '.' > /var/opt/myapp/cqrs/queries/users/1.json
Kết quả mong đợi: File `1.json` được tạo trong thư mục `queries`. Nội dung file này là trạng thái hiện tại đã tính toán xong.
Verify kết quả Query Side bằng cách gọi script query:
/var/opt/myapp/cqrs/scripts/query_handler.sh 1
Kết quả mong đợi: In ra trạng thái hiện tại của user ngay lập tức mà không có độ trễ tính toán, chứng tỏ sự tách biệt hiệu quả giữa Command (ghi log) và Query (đọc snapshot).
Lợi ích và thách thức khi áp dụng trên Linux server
Áp dụng CQRS và Event Sourcing trên Linux server mang lại lợi ích lớn về khả năng mở rộng (scalability) và khả năng kiểm toán (auditability). Vì dữ liệu được lưu dưới dạng file text hoặc dòng JSON, chúng ta có thể dễ dàng sử dụng các công cụ Linux như `grep`, `awk`, `sed` hoặc các công cụ log analysis như ELK Stack để phân tích lịch sử hệ thống mà không cần truy vấn database phức tạp.
Tuy nhiên, thách thức lớn nhất là việc quản lý kích thước file và hiệu năng khi số lượng sự kiện tăng lên hàng triệu. Nếu không có cơ chế Snapshoting (lưu trạng thái tại các mốc thời gian), việc replay toàn bộ lịch sử sẽ rất chậm. Ngoài ra, việc đồng bộ hóa giữa Command và Query cần được xử lý cẩn thận để tránh tình trạng dữ liệu không nhất quán (eventual consistency).
Verify hiệu năng và tính nhất quán
Chúng ta sẽ thực hiện một bài kiểm tra đơn giản để đảm bảo dữ liệu giữa Command Side (Event Store) và Query Side (Read Model) là nhất quán sau khi có sự thay đổi.
Thực hiện các bước: (1) Gửi một command mới, (2) Chạy lại quá trình đồng bộ (projection), (3) Kiểm tra xem Query Side có cập nhật đúng hay không.
# Bước 1: Gửi command mới
/var/opt/myapp/cqrs/scripts/command_handler.sh 1 "UserEmailUpdated" '{"new_email": "final_email@example.com"}'
# Bước 2: Đồng bộ hóa (Projection) từ Event Store sang Read Model
# Trong thực tế, bước này chạy tự động qua Event Bus, ở đây ta chạy thủ công
/var/opt/myapp/cqrs/scripts/replay.sh 1 | jq '.' > /var/opt/myapp/cqrs/queries/users/1.json
# Bước 3: Verify Query Side đã cập nhật email mới
/var/opt/myapp/cqrs/scripts/query_handler.sh 1 | jq '.email'
Kết quả mong đợi: Lệnh cuối cùng in ra `"final_email@example.com"`. Điều này chứng minh rằng quy trình CQRS + Event Sourcing đang hoạt động chính xác: Command ghi sự kiện -> Projection cập nhật Read Model -> Query đọc được trạng thái mới.
Kiểm tra kích thước file để đánh giá thách thức về lưu trữ:
ls -lh /var/opt/myapp/cqrs/event-store/users/1.json
Kết quả mong đợi: Hiển thị kích thước file tăng lên sau mỗi lần thêm sự kiện. Điều này nhắc nhở sysadmin cần có chiến lược lưu trữ và snapshoting khi hệ thống phát triển.
Đ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 1: Chuẩn bị môi trường Ubuntu 24.04 và công cụ cần thiết
Phần 3: Thiết kế Schema và lưu trữ Event Store »