← Về danh sách
Kỹ nghệ & Quản lý phần mềm
#CQRS#이벤트 소싱#DDD#이벤트 기반 아키텍처#마이크로서비스#분산시스템
Cập nhật lần cuối · 2026-08-18

CQRS và Event Sourcing (Command Query Responsibility Segregation · Event Sourcing)

1. Tổng quan

A. Định nghĩa

CQRS (Command Query Responsibility Segregation — tách biệt trách nhiệm lệnh và truy vấn) là mẫu kiến trúc tách trách nhiệm đọc (Query) và ghi (Command) khỏi một mô hình dữ liệu duy nhất, nhờ đó áp dụng mô hình, luồng xử lý và chiến lược hiệu năng phù hợp cho từng mục đích.

Event Sourcing (lưu trữ theo sự kiện) là mẫu lưu bền (persistence) không chỉ cập nhật và lưu trạng thái hiện tại, mà ghi nối tiếp theo trình tự thời gian các sự kiện miền (domain event) đã làm thay đổi trạng thái, và khi cần sẽ tái dựng trạng thái bằng cách phát lại (replay) các sự kiện.

CQRS và Event Sourcing thường được dùng cùng nhau nhưng không phải là cùng một khái niệm. Cốt lõi của CQRS là tách biệt trách nhiệm giữa mô hình đọc và mô hình ghi; ngay cả khi dùng cơ sở dữ liệu quan hệ thông thường cho cả hai phía thì vẫn có thể là CQRS. Cốt lõi của Event Sourcing là lưu giữ các sự thật về thay đổi dữ liệu dưới dạng sự kiện append-only (chỉ ghi nối), và không bắt buộc phải tách đọc và ghi thành các mô hình riêng. Khi kết hợp hai mẫu, phía ghi kiểm tra quy tắc miền và ghi lại sự kiện, còn phía đọc chiếu (projection) các sự kiện để tạo ra các view tối ưu cho truy vấn.

Các hệ thống CRUD truyền thống quen với cách đọc và sửa hàng hiện tại của một bảng. Cách này hiệu quả khi nghiệp vụ đơn giản và tính nhất quán tức thời là quan trọng, nhưng mô hình sẽ trở nên phức tạp khi đặc tính tải đọc và ghi khác nhau nhiều hoặc khi bản thân lịch sử thay đổi là quan trọng. Ví dụ, nếu chỉ ghi đè trạng thái đơn hàng thành Đã giao hàng thì rất khó khôi phục ai đã đổi trạng thái, vào lúc nào, theo quy tắc nào, và trước đó đã có những sự kiện thanh toán và tồn kho nào. CQRS và Event Sourcing là những lựa chọn thiết kế lại các vấn đề này từ góc độ cách lưu trữ dữ liệu và mô hình nghiệp vụ.

B. Bối cảnh ra đời và sự cần thiết

Thứ nhất, mục tiêu tối ưu hóa của đọc và ghi khác nhau. Việc ghi phải tuân thủ các bất biến (invariant) và kiểm soát đồng thời, nên mô hình miền đã chuẩn hóa và giao dịch ngắn có thể có lợi. Ngược lại, việc đọc cần dữ liệu khác nhau cho từng màn hình, báo cáo, tìm kiếm, và để giảm chi phí join thì mô hình truy vấn phi chuẩn hóa có thể có lợi. Nếu đặt cả hai yêu cầu lên một mô hình, rất khó làm cho mọi màn hình truy vấn đều nhanh mà không phá vỡ các quy tắc ghi.

Thứ hai, khi quy mô và chức năng của dịch vụ tăng lên, nảy sinh nhu cầu lưu giữ ý nghĩa của các thay đổi trạng thái. Trong các lĩnh vực như giao dịch tài chính, luân chuyển tồn kho, tích điểm, thay đổi quyền hạn — nơi chỉ “giá trị hiện tại” thì khó giải thích cho kiểm toán, tranh chấp, quyết toán — thì sự thật và thứ tự của thay đổi trở thành tài sản quan trọng. Event Sourcing lưu các sự kiện làm bản gốc, cho phép tính lại trạng thái hoặc căn cứ phán đoán tại một thời điểm trong quá khứ. Tuy nhiên, mục đích không phải là biến mọi bảng thành sự kiện; cần đánh giá đồng thời giá trị nghiệp vụ của lịch sử thay đổi và chi phí vận hành.

Thứ ba, trong kiến trúc microservice và kiến trúc hướng sự kiện, cần liên kết lỏng các thay đổi trạng thái bên trong dịch vụ với các dịch vụ khác. Khi dịch vụ đơn hàng phát hành sự thật về việc tạo đơn hàng, các dịch vụ tồn kho, thanh toán, thông báo có thể tự xây dựng mô hình của riêng mình. Tuy nhiên, truyền tải bất đồng bộ tạo ra nhất quán trễ thay vì nhất quán tức thời, nên cần thiết kế cả cách giải thích và hiệu chỉnh sự chênh lệch giữa màn hình người dùng thấy và trạng thái cuối cùng.

2. Khái niệm và cấu trúc của CQRS

A. Cấu trúc logic

flowchart LR
    U[Người dùng · hệ thống bên ngoài] --> API[API·Application Layer]
    API --> CMD[Luồng Command]
    API --> QRY[Luồng Query]
    CMD --> CH[Command Handler]
    CH --> DM[Write Model·Domain Aggregate]
    DM --> WS[(Write Store)]
    QRY --> QH[Query Handler]
    QH --> RM[(Read Model·Materialized View)]
    WS -.Sự kiện thay đổi hoặc đồng bộ.-> PROJ[Projection·Cập nhật Read Model]
    PROJ --> RM

Command biểu diễn ý định thay đổi trạng thái của hệ thống. Thay vì chỉ thị trực tiếp các trường của kho lưu trữ như ThayĐổiSốLượngSảnPhẩm, nếu thiết kế thành các lệnh thể hiện hành vi nghiệp vụ như XácNhậnĐơnHàng, PhêDuyệtThanhToán, HủyĐặtChỗ thì dễ kiểm tra quy tắc miền tại một nơi. Lệnh có thể trả về thành công hay thất bại, nhưng những sự kiện nào đã xảy ra trong khi xử lý lệnh có thể được ghi riêng dưới dạng sự kiện.

Query không thay đổi trạng thái mà trả về biểu diễn cần thiết cho truy vấn. Mô hình truy vấn có thể chứa các DTO được kết hợp sẵn theo dạng mà màn hình hay báo cáo yêu cầu, và dù áp dụng chỉ mục, công cụ tìm kiếm, bộ nhớ đệm cho mô hình này thì cũng không xung đột trực tiếp với quy tắc toàn vẹn của mô hình ghi. Tuy nhiên, quy tắc Query không thay đổi dữ liệu không chỉ được tuân thủ về mặt kỹ thuật, mà còn bao gồm nguyên tắc thiết kế không chèn các thao tác ngầm như tăng bộ đếm hay cập nhật thời điểm truy cập cuối khi truy vấn.

CQRS được hiện thực hóa dưới nhiều dạng tùy theo mức độ tách biệt. Dạng yếu nhất là tách logic dịch vụ lệnh và dịch vụ truy vấn trong cùng một cơ sở dữ liệu. Dạng trung gian là ánh xạ cùng một dữ liệu nguồn sang hai mô hình, còn dạng mạnh là tách vật lý kho ghi và kho đọc rồi kết nối chúng bằng sự kiện hoặc thu thập dữ liệu thay đổi (CDC). Mức tách biệt càng cao thì khả năng mở rộng độc lập và tối ưu truy vấn càng tốt, nhưng gánh nặng triển khai, giám sát và nhất quán dữ liệu cũng càng lớn.

B. Vai trò của từng thành phần

Thành phần Trách nhiệm Trọng tâm thiết kế
Command Truyền đạt ý định thay đổi trạng thái Thuật ngữ nghiệp vụ, tính hợp lệ, tính lũy đẳng
Command Handler Điều phối lệnh và thiết lập ranh giới giao dịch Quyền hạn, xử lý trùng lặp, trả về lỗi
Write Model Thực thi bất biến và quy tắc miền Aggregate, đồng thời, tính nhất quán
Query Yêu cầu thông tin cần thiết Hợp đồng truy vấn, bộ lọc, phân trang
Read Model Cung cấp trạng thái tối ưu cho truy vấn Phi chuẩn hóa, chỉ mục, hiệu năng tìm kiếm
Projector Phản ánh thay đổi nguồn vào mô hình đọc Thứ tự, xử lý lại, chống trùng lặp
Event Store hoặc Write Store Lưu bền các thay đổi Tính nguyên tử, lưu giữ, phiên bản, sao lưu

Command Handler không chỉ là một controller đơn thuần. Nó phải kiểm tra quyền của bên gọi đã được xác thực, nạp Aggregate, xác định lệnh có hợp lệ ở phiên bản hiện tại hay không, và lưu các thay đổi thành công trong một ranh giới giao dịch duy nhất. Nếu thực hiện trực tiếp trong handler các tác vụ nằm ngoài giao dịch như thanh toán bên ngoài hay gửi tin nhắn, thứ tự giữa lưu thành công và gọi bên ngoài thành công có thể bị lệch, do đó cần Outbox hoặc Process Manager được trình bày ở phần sau.

Read Model không phải là bản sao của bản gốc mà là kết quả chiếu để trả lời một câu hỏi cụ thể. Màn hình danh sách đơn hàng có thể chỉ cần tóm tắt đơn hàng và trạng thái gần nhất theo khách hàng, còn màn hình quyết toán có thể cần rộng rãi thông tin thanh toán, hoàn tiền, thuế. Hai màn hình có thể có các mô hình đọc khác nhau, và nếu làm rõ tiền đề rằng mô hình đọc là dữ liệu dẫn xuất có thể tái tạo, thì dễ thay đổi lược đồ theo biến động nghiệp vụ.

C. Hiệu quả và giới hạn khi áp dụng CQRS

Khi tách đọc và ghi, lúc lưu lượng truy vấn tăng đột biến có thể chỉ mở rộng theo chiều ngang mô hình đọc. Có thể xây dựng các màn hình tập trung vào tìm kiếm, tổng hợp phù hợp với Elasticsearch hay kho lưu trữ dạng cột, trong khi phía lệnh dùng kho lưu trữ mạnh về giao dịch. Ngoài ra, mô hình ghi tập trung biểu diễn quy tắc miền còn mô hình truy vấn tập trung biểu diễn trải nghiệm người dùng, nên ý định của từng phần mã trở nên rõ ràng.

Tuy nhiên, bản thân việc tách không bảo đảm hiệu năng. Độ trễ projection, nút thắt của message broker, thiết kế chỉ mục của mô hình truy vấn, số vòng mạng có thể làm tăng tổng thời gian phản hồi. Nếu vận hành cả kho đọc và kho ghi, phải áp dụng sao lưu, khôi phục sự cố, thay đổi lược đồ, quyền truy cập cho hai hệ thống. Vì vậy, trước hết phải đo lường xem nút thắt của hệ thống hiện tại là do sự gắn kết mô hình hay chỉ là vấn đề chỉ mục, truy vấn, bộ nhớ đệm.

3. Nguyên lý hoạt động của Event Sourcing

A. Quản lý trạng thái lấy sự kiện làm trung tâm

sequenceDiagram
    participant C as Client
    participant H as Command Handler
    participant A as Aggregate
    participant E as Event Store
    participant B as Event Bus
    participant P as Projector
    participant R as Read Store
    C->>H: Command (ý định nghiệp vụ)
    H->>E: Truy vấn Event Stream hiện có
    E-->>A: Phát lại sự kiện quá khứ
    H->>A: Kiểm tra Command · thay đổi trạng thái
    A-->>H: Tạo Domain Event
    H->>E: Append nguyên tử sự kiện mới
    E->>B: Phát hành sự kiện đã lưu
    B->>P: Chuyển giao sự kiện
    P->>R: Cập nhật mô hình đọc
    R-->>C: Kết quả Query

Trong Event Sourcing, thay vì chỉ lưu kết quả số dư hiện tại = 100,000 won, ta lưu dòng các sự thật đã làm thay đổi trạng thái như nạp 150,000 won, rút 50,000 won. Vì sự kiện là sự thật đã xảy ra, thường dùng tên ở thì quá khứ và coi nó là đối tượng bất biến, không sửa nội dung sau khi lưu. Các sự kiện của cùng một Aggregate có thứ tự, và khi dựng lại Aggregate sẽ áp dụng theo đúng thứ tự đó.

Kho sự kiện khác với một hàng đợi thông điệp đơn thuần. Hàng đợi có thể xóa thông điệp đã tiêu thụ hoặc có phạm vi bảo đảm chuyển giao khác, còn kho sự kiện với tư cách là bản gốc nghiệp vụ cần lưu giữ dài hạn, phát lại, quản lý phiên bản và truy vấn. Ngược lại, nếu biến kho sự kiện thành nơi lưu trữ vô thời hạn cho mọi thông điệp tích hợp thì các vấn đề thông tin cá nhân và chi phí sẽ tăng lên, nên cần tách mục đích lưu giữ của sự kiện nghiệp vụ, sự kiện tích hợp và log kỹ thuật.

B. Aggregate và luồng sự kiện

Aggregate là nhóm đối tượng miền phải cùng kiểm tra quy tắc nhất quán cho một lệnh. Trong Aggregate đơn hàng có thể kiểm tra nguyên tử mối quan hệ giữa trạng thái đơn hàng và các dòng đơn hàng, nhưng nếu gộp toàn bộ đơn hàng, tồn kho, thanh toán thành một Aggregate khổng lồ thì nút thắt đồng thời và sự gắn kết dịch vụ sẽ tăng lên. Ranh giới Aggregate được xác định không theo ranh giới bảng cơ sở dữ liệu mà theo bất biến nghiệp vụ và ranh giới giao dịch.

Mỗi Aggregate có thể có một luồng sự kiện theo định danh. Khi có lệnh mới, đọc luồng tương ứng để dựng trạng thái hiện tại, rồi so sánh phiên bản hiện tại với phiên bản mà lệnh kỳ vọng để áp dụng kiểm soát đồng thời lạc quan (optimistic concurrency control). Khi hai người dùng cùng đặt một ghế, lần append đầu tiên phải tăng phiên bản và lần append thứ hai phải bị từ chối thì mới ngăn được đặt chỗ trùng. Khi thử lại, nếu kiểm tra cùng một ID lệnh thì cũng có thể giảm xử lý trùng lặp do timeout mạng.

C. Các yếu tố thiết kế sự kiện

Yếu tố Ý nghĩa Ví dụ
Event ID Định danh toàn cục của sự kiện UUID
Aggregate ID Đối tượng nghiệp vụ mà sự kiện thuộc về order-2026-0818-001
Stream Version Số thứ tự bên trong Aggregate 17
Event Type Loại sự thật nghiệp vụ đã xảy ra OrderConfirmed
Occurred At Thời điểm xảy ra theo nghiệp vụ Thời điểm có kèm múi giờ chuẩn
Payload Dữ liệu để phát lại sự thật Số tiền, tiền tệ, định danh sản phẩm
Metadata Thông tin truy vết, bảo mật, tương quan correlation ID, actor

Payload của sự kiện phải chứa các sự thật cần cho việc phát lại chứ không phải “ảnh chụp lưu giá trị hiện tại”. Ví dụ, nếu tính lại số tiền đơn hàng bằng giá sản phẩm hiện tại thì kết quả của các đơn hàng cũ có thể thay đổi sau khi giá thay đổi, nên phải lưu vào sự kiện đơn giá, thuế, phiên bản chính sách giảm giá tại thời điểm đặt hàng. Ngược lại, nếu nhân bản thông tin nhạy cảm như số định danh công dân vào mọi sự kiện thì khó tuân thủ chính sách xóa và lưu giữ, nên cần áp dụng token hóa, tách tham chiếu, mã hóa, tối thiểu hóa trường dữ liệu.

D. Phát lại và snapshot

Khi số lượng sự kiện tăng lên, chi phí phát lại toàn bộ luồng từ đầu mỗi lần sẽ lớn. Snapshot là kỹ thuật tối ưu hóa lưu trạng thái Aggregate đã phát lại đến một phiên bản nhất định, rồi chỉ áp dụng các sự kiện sau đó để giảm thời gian nạp. Snapshot không phải là sự thật gốc mà là bộ đệm dẫn xuất, nên khi bị hỏng hoặc cũ phải có thể tạo lại từ sự kiện. Lưu kèm thời điểm tạo snapshot và phiên bản sự kiện đã áp dụng, đồng thời kiểm tra để snapshot không vượt trước luồng hiện tại.

Phát lại sự kiện còn được dùng để tạo mô hình đọc mới. Có thể đưa các sự kiện hiện có theo thứ tự vào Projector để tạo lại chỉ mục tìm kiếm hay màn hình kiểm toán. Khi đó, nếu phiên bản mã của Projector đang vận hành và Projector dùng để phát lại khác nhau thì kết quả có thể khác nhau, nên phải ghi lại phiên bản projection và phiên bản lược đồ sự kiện. Phát lại có thể làm tăng lưu lượng vận hành và tải kho lưu trữ, vì vậy cần thiết kế nhóm consumer riêng, giới hạn tốc độ, checkpoint, kho lưu sự kiện thất bại.

4. Vận hành kết hợp CQRS và Event Sourcing

A. Luồng xử lý tổng thể

Khi client gửi lệnh XácNhậnĐơnHàng, tầng API thực hiện kiểm tra định dạng và xác thực. Command Handler kiểm tra lệnh có trùng lặp không và quyền hạn, sau đó đọc luồng sự kiện của Aggregate đơn hàng. Aggregate kiểm tra từ trạng thái hiện tại xem đã giữ tồn kho chưa, trạng thái thanh toán, đơn hàng có hết hạn không, và nếu đủ điều kiện thì tạo sự kiện OrderConfirmed. Kho sự kiện kiểm tra phiên bản kỳ vọng rồi append nguyên tử sự kiện, và chỉ chuyển các sự kiện thành công sang các projection tiếp theo.

Projector nhận sự kiện và cập nhật một hoặc nhiều Read Model như danh sách đơn hàng, màn hình khách hàng, view quyết toán. Vì mỗi Read Model có thể diễn giải cùng một sự kiện theo cách khác nhau, không cần gượng ép phản ánh yêu cầu của từng màn hình vào mô hình ghi. Truy vấn được xử lý tại Read Model, và khi ngay sau lệnh mô hình truy vấn chưa được cập nhật, cung cấp cho người dùng trạng thái đang xử lý, phiên bản, hướng dẫn truy vấn lại.

B. Nhất quán và xử lý sự cố

Đặc tính tiêu biểu của cấu trúc kết hợp CQRS và Event Sourcing là tính nhất quán cuối cùng (eventual consistency) giữa bản gốc ghi và projection đọc. Ngay khi append sự kiện thành công thì bản gốc nghiệp vụ đã thay đổi, nhưng do trễ mạng hay trễ Projector, trạng thái cũ có thể tạm thời còn trên màn hình. Nếu che giấu độ trễ này, người dùng có thể hiểu nhầm là thanh toán thất bại hoặc lặp lại cùng một lệnh, vì thế an toàn hơn khi cung cấp một mô hình trạng thái riêng để truy vấn ID yêu cầu và trạng thái xử lý.

Projector thường được hiện thực lũy đẳng với tiền đề chuyển giao ít nhất một lần (at-least-once). Lưu event ID và phiên bản Aggregate để bỏ qua các sự kiện đã xử lý, và bảo đảm rằng nếu thất bại giữa chừng khi cập nhật một phần thì xử lý lại cùng sự kiện vẫn cho kết quả cuối cùng giống nhau. Với các luồng coi trọng thứ tự, bảo đảm thứ tự theo từng Aggregate ID, còn các sự kiện đến sai thứ tự thì chuyển vào hàng đợi tạm giữ hoặc thử lại sau khi kiểm tra phiên bản hiện tại.

Trong tích hợp với hệ thống bên ngoài, mẫu Outbox rất hữu ích. Ghi dữ liệu nghiệp vụ và thông điệp cần phát hành trong cùng một giao dịch cục bộ, rồi một bộ phát hành riêng chuyển sang message broker, nhờ đó giảm được vấn đề ghi kép (dữ liệu đã lưu nhưng sự kiện chưa được phát hành). Tuy nhiên, nếu bản thân kho sự kiện đã gắn nguyên tử với chức năng phát hành thì Outbox riêng có thể bị trùng lặp, vì vậy cần kiểm tra trước bảo đảm chuyển giao và đặc tính vận hành của kho lưu trữ.

Sự kiện thất bại được lưu giữ để có thể truy vết nguyên nhân. Nếu thử lại vô điều kiện, dữ liệu sai hay sự kiện không hợp lệ có thể làm tắc hàng đợi xử lý, nên cần phân biệt số lần thử lại, backoff, hàng đợi cách ly, quy trình xử lý lại của người vận hành. Khi xử lý lại, không sửa sự kiện gốc mà phát hành sự kiện hiệu chỉnh mới để duy trì khả năng kiểm toán.

C. Phiên bản sự kiện và tiến hóa lược đồ

Vì sự kiện được lưu giữ trong thời gian dài, không được giả định rằng cấu trúc lớp hôm nay sẽ vẫn đọc được nguyên vẹn vào ngày mai. Khi thêm trường, thêm dưới dạng trường tùy chọn để consumer hiện có có thể bỏ qua; khi đổi ý nghĩa của trường, phân biệt bằng loại sự kiện mới hoặc phiên bản tường minh. Nếu sửa hàng loạt toàn bộ sự kiện hiện có thì có thể phá vỡ tính bất biến của sự thật quá khứ và khả năng truy vết kiểm toán.

Upcasting sự kiện là phương pháp chuyển đổi sự kiện quá khứ sang định dạng hiện tại tại thời điểm đọc, còn migration là phương pháp chuyển đổi và lưu sang định dạng sự kiện mới hoặc luồng mới. Upcasting dễ bảo toàn bản gốc nhưng mã chuyển đổi tích tụ dần, còn migration có thể đơn giản hóa việc tiêu thụ nhưng đòi hỏi xử lý lại và kiểm chứng quy mô lớn. Quy tắc tương thích phải bao gồm thứ tự triển khai Producer và Consumer, trường bắt buộc, bổ sung enum, chính sách xóa.

5. So sánh CRUD truyền thống · CQRS · Event Sourcing

A. Khác biệt giữa các mẫu

Hạng mục CRUD truyền thống CQRS Event Sourcing Kết hợp CQRS và Event Sourcing
Dữ liệu nguồn Hàng trạng thái hiện tại Trạng thái hiện tại tùy cách hiện thực Luồng sự kiện bất biến Luồng sự kiện và projection
Mô hình đọc · ghi Phần lớn giống nhau Tách biệt Không nhất thiết tách Tách biệt rõ ràng
Khôi phục lịch sử Cần log kiểm toán riêng Thiết kế riêng Có sẵn một cách tự nhiên Được nhờ phát lại sự kiện
Tính nhất quán Dễ đạt nhất quán mạnh Chọn đồng bộ · bất đồng bộ Tùy cách phát lại · chiếu Ghi mạnh, đọc có thể nhất quán cuối cùng
Độ phức tạp Thấp~trung bình Trung bình~cao Cao Có thể cao nhất
Tình huống phù hợp Nghiệp vụ đơn giản · truy vấn tức thời Nghiệp vụ có đặc tính đọc/ghi khác nhau Nghiệp vụ lấy sự thật thay đổi và phát lại làm cốt lõi Nghiệp vụ tải cao · kiểm toán · nhiều view

Như bảng cho thấy, CQRS không phải là khái niệm cấp trên bao hàm Event Sourcing. CQRS là sự tách biệt mô hình và trách nhiệm, còn Event Sourcing là lựa chọn phương thức lưu bền, nên phải đánh giá hai mẫu như hai trục độc lập. Ví dụ, hệ thống lưu trạng thái hiện tại vào DB ghi và sao chép sang DB truy vấn là CQRS nhưng có thể không phải Event Sourcing. Ngược lại, hệ thống phát lại luồng sự kiện để tạo ra một đối tượng trạng thái hiện tại duy nhất là Event Sourcing nhưng có thể không tách mô hình đọc · ghi.

B. Xử lý đồng bộ và xử lý bất đồng bộ

CQRS đồng bộ là cách sau khi xử lý lệnh sẽ cập nhật luôn mô hình đọc trong cùng yêu cầu và trả ngay kết quả mới nhất cho người dùng. Cách này hiện thực đơn giản và trải nghiệm người dùng dễ dự đoán, nhưng khi có nhiều mô hình đọc hoặc có hệ thống bên ngoài tham gia thì thời gian xử lý lệnh kéo dài. Ngoài ra, phải quyết định chính sách xem thất bại cập nhật mô hình đọc là thất bại lệnh hay sẽ hiệu chỉnh sau.

CQRS bất đồng bộ lưu sự kiện rồi Projector cập nhật mô hình đọc một cách riêng biệt. Cách này làm phản hồi ghi nhanh và có thể mở rộng độc lập các consumer, nhưng phải quản lý độ trễ, trùng lặp, thứ tự, xử lý lại sự kiện và sự chênh lệch trạng thái người dùng thấy. Trong hầu hết nghiệp vụ, thay vì biến mọi luồng thành bất đồng bộ, thực tế hơn là tách các lệnh cần nhất quán tức thời mạnh khỏi các projection phụ có thể chấp nhận độ trễ.

C. Tiêu chí quyết định áp dụng

Câu hỏi đánh giá Tín hiệu nên xem xét áp dụng Tín hiệu nên hoãn áp dụng
Tải đọc và ghi có khác nhau không Truy vấn nhiều hơn ghi rất nhiều hoặc mẫu truy vấn đa dạng Tải và mô hình đơn giản
Lịch sử thay đổi có phải tài sản nghiệp vụ không Kiểm toán · tranh chấp · truy vấn theo thời điểm là quan trọng Chỉ cần trạng thái hiện tại
Quy tắc miền có phức tạp không Bất biến Aggregate và ý định lệnh rõ ràng Chủ yếu đăng ký · truy vấn đơn giản
Có chấp nhận được nhất quán cuối cùng không Cho phép trạng thái đang xử lý và thử lại Nhất quán tức thời là cốt lõi như thanh toán · tồn kho
Năng lực vận hành đã sẵn sàng chưa Có hệ thống messaging · khả năng quan sát · xử lý lại Thiếu nhân lực sao lưu · giám sát

Tiêu chí này giúp ưu tiên rủi ro nghiệp vụ hơn là trào lưu công nghệ. Ví dụ, dù có nhiều màn hình đọc, nếu có thể giải quyết bằng chỉ mục và bộ nhớ đệm đơn giản thì không cần áp dụng CQRS. Ngược lại, nếu trong tranh chấp với khách hàng cần tái hiện các quyết định trong quá khứ và chủ thể thay đổi, chi phí bổ sung của Event Sourcing có thể được bù đắp bởi giá trị kiểm toán.

6. Tình huống áp dụng

A. Tình huống đặt chỗ ngồi và xử lý đơn hàng

Trong hệ thống đặt chỗ ngồi buổi hòa nhạc, ChọnGhế và XácNhậnĐặtChỗ được mô hình hóa thành các lệnh khác nhau. Command Handler xác nhận đặt chỗ kiểm tra phiên bản hiện tại của Aggregate ghế và thời điểm hết hạn giữ chỗ, và từ chối lệnh nếu người dùng khác đã đặt trước. Nếu thành công, lần lượt ghi các sự kiện như SeatHeld và ReservationConfirmed, và mô hình truy vấn ghế chiếu ghế đó thành Đã đặt.

Màn hình tình trạng ghế được rất nhiều người dùng truy vấn nên có thể đặt mô hình đọc trên bộ nhớ đệm hay kho tìm kiếm. Tuy nhiên, khả năng mua cuối cùng không được chỉ dựa vào giá trị màn hình trong bộ nhớ đệm mà phải quyết định bằng Aggregate và kiểm tra phiên bản ở phía lệnh. Dù màn hình tạm thời hiển thị Còn trống, nếu thông báo rõ cho người dùng chính sách rằng có thể thất bại tại thời điểm xác nhận, và gợi ý ghế thay thế khi thất bại, thì có thể giảm bất tiện của nhất quán cuối cùng.

B. Tình huống giao dịch tài chính và kiểm toán

Dịch vụ ví tài chính có thể lưu nạp tiền, rút tiền, chuyển khoản, thu phí dưới dạng sự kiện. Read Model số dư hiện tại là kết quả phản ánh các sự kiện theo thứ tự, còn số dư theo ngày, sao kê giao dịch, tổng hợp rủi ro có thể do các Projector khác nhau tạo ra. Người kiểm toán có thể phát lại sự kiện đến một thời điểm cụ thể để tái hiện số dư, và truy vết chủ thể, luồng phê duyệt, correlation ID của giao dịch.

Trong giao dịch tài chính, xử lý trùng lặp là chết người, nên ID lệnh và ID giao dịch bên ngoài được quản lý làm khóa lũy đẳng. Việc lưu sự kiện không có nghĩa là lời gọi API ngân hàng bên ngoài đã thành công, nên tích hợp bên ngoài được tách ra bằng máy trạng thái và chính sách thử lại. Khi cần hiệu chỉnh, không sửa số tiền của sự kiện rút tiền hiện có mà ghi một sự thật mới có hiệu ứng ngược lại như WithdrawalReversed thì sổ cái và vết kiểm toán mới khớp nhau.

C. Tình huống logistics và view phân tích

Trong hệ thống logistics, các sự kiện NhậpKhoHàng, HoànTấtLấyHàng, HoànTấtXếpHàng, XuấtPhátGiaoHàng, HoànTấtGiaoHàng có thể cấu thành luồng của Aggregate giao hàng. Màn hình vận hành hiển thị nhanh trạng thái giao hàng hiện tại và thời gian đến dự kiến, còn màn hình phân tích tổng hợp sự kiện theo khu vực, đơn vị vận chuyển, nguyên nhân chậm trễ. Thay vì xử lý mọi màn hình bằng một bảng đơn hàng đã chuẩn hóa, nếu tạo mô hình đọc theo từng nghiệp vụ thì các yêu cầu truy vấn ít ảnh hưởng lẫn nhau hơn.

Khi đó, nếu xử lý sai thứ tự và trùng lặp của sự kiện thì trạng thái giao hàng có thể quay lui về bước trước. Projector kiểm tra phiên bản đã xử lý cuối cùng theo từng Aggregate ID và các chuyển trạng thái được phép, còn sự kiện đến muộn thì tạm giữ hoặc chuyển sang quy trình hiệu chỉnh. Trong các trường hợp thứ tự thời gian làm thay đổi ý nghĩa nghiệp vụ, như đổi địa chỉ sau khi giao hàng xong, lưu cả thời điểm xảy ra và thời điểm nhận sự kiện để tách bạch căn cứ phán đoán.

7. Quy trình áp dụng và chiến lược kiểm thử

A. Áp dụng theo từng giai đoạn

  1. Chọn nghiệp vụ ứng viên: Bắt đầu từ các Aggregate giới hạn, nơi lịch sử thay đổi, nhiều truy vấn, quy tắc miền có giá trị thực tế.
  2. Khảo sát luồng hiện tại: Liệt kê lệnh, thay đổi trạng thái, tích hợp bên ngoài, yêu cầu kiểm toán, SLA truy vấn và cách xử lý sự cố.
  3. Định nghĩa ngôn ngữ sự kiện: Thống nhất Event Type và Payload dựa trên các sự thật ở thì quá khứ vốn đã được dùng trong nghiệp vụ.
  4. Xây dựng mô hình ghi: Hiện thực trước ranh giới Aggregate, bất biến, phiên bản đồng thời, tính lũy đẳng của lệnh.
  5. Bảo đảm lưu nguyên tử: Xác nhận ranh giới giao dịch giữa append sự kiện và kiểm tra phiên bản.
  6. Chiếu từ một mô hình đọc trước: Chọn một màn hình thiết yếu cho vận hành và kiểm chứng độ trễ, trùng lặp, xử lý lại.
  7. Khả năng quan sát và tự động hóa vận hành: Đưa độ trễ sự kiện, consumer lag, hàng đợi thất bại, số lần xử lý lại, lỗi lược đồ lên dashboard.
  8. Mở rộng dần dần: Mở rộng các sự kiện đã ổn định sang mô hình đọc khác · dịch vụ bên ngoài, và quản lý phiên bản hợp đồng của từng consumer.

Thí điểm nên bắt đầu từ nghiệp vụ có giá trị lịch sử và có thể hiệu chỉnh, thay vì những vùng có chi phí thất bại lớn như toàn bộ thanh toán, thì an toàn hơn. Tuy nhiên, vì thí điểm phải là bản thu nhỏ của đặc tính vận hành thực tế, cần cố ý thử nghiệm cả độ trễ thông điệp, trùng lặp, khởi động lại, thay đổi lược đồ. Thay vì loại bỏ CRUD hiện có một lần, cách thực tế hơn là ghi song song sự kiện và trạng thái hiện có, so sánh kiểm chứng mô hình đọc rồi chuyển dần lưu lượng.

B. Hạng mục kiểm thử

Lĩnh vực kiểm thử Nội dung kiểm chứng
Kiểm thử miền Bất biến theo lệnh, chuyển trạng thái được phép, sự kiện được tạo
Kiểm thử phát lại Cùng một luồng sự kiện có tạo ra cùng trạng thái Aggregate không
Kiểm thử đồng thời Xung đột phiên bản kỳ vọng và xử lý lệnh trùng lặp
Kiểm thử projection Kết quả cuối cùng sau thứ tự · trùng lặp · khởi động lại sự kiện
Kiểm thử hợp đồng Tương thích giữa lược đồ sự kiện và Consumer
Kiểm thử sự cố Trễ broker, sự cố kho lưu trữ, xử lý lại hàng đợi thất bại
Kiểm thử hiệu năng Thông lượng append, thời gian phát lại, độ trễ truy vấn, lag
Kiểm thử bảo mật Quyền truy cập sự kiện, che thông tin nhạy cảm, log kiểm toán

Kiểm thử phát lại quan trọng hơn việc chỉ nâng độ bao phủ mã. Nếu phát lại sự kiện quá khứ bằng mã miền phiên bản mới mà kết quả khác đi, đó là tín hiệu cần có chính sách về phiên bản lược đồ, upcasting, thay đổi quy tắc miền. Kiểm thử projection phải bảo đảm chuyển cùng một sự kiện hai lần cho kết quả giống như xử lý một lần, và xác nhận trạng thái cuối cùng hội tụ dù thất bại giữa chừng rồi khởi động lại.

8. Chuyên sâu: Liên kết với kiến trúc hướng sự kiện

CQRS và Event Sourcing kết hợp tốt với kiến trúc hướng sự kiện, nhưng không phải cứ phát hành sự kiện là trở thành Event Sourcing. Sự kiện tích hợp là hợp đồng để thông báo cho hệ thống khác, sự kiện miền biểu diễn sự thật nghiệp vụ bên trong Aggregate, còn sự kiện Event Sourcing là bản ghi lưu bền để phát lại trạng thái nguồn. Nếu chia sẻ vô điều kiện ba loại sự kiện này với cùng tên và Payload, thay đổi mô hình nội bộ có thể lan thành thay đổi hợp đồng bên ngoài. Đặt một ranh giới ánh xạ riêng từ sự kiện miền nội bộ sang sự kiện tích hợp bên ngoài sẽ giúp giảm mức độ gắn kết.

Microsoft Azure Architecture Center mô tả CQRS là mẫu tách thao tác đọc · ghi thành các mô hình riêng, và giải thích rằng khi kết hợp với Event Sourcing thì kho sự kiện là nguồn ghi và mô hình đọc được tạo từ sự kiện (Microsoft CQRS Pattern, Microsoft Event Sourcing Pattern). Martin Fowler cũng phân biệt rằng bản thân CQRS là sự tách biệt mô hình đọc và ghi, không tất yếu đồng nhất với Event Sourcing (Martin Fowler CQRS, Martin Fowler Event Sourcing). Sự phân biệt này là căn cứ để trong bài làm của Kỹ sư chuyên nghiệp (Professional Engineer) không học thuộc hai mẫu như một cặp đơn thuần, mà giải thích tách bạch mục đích áp dụng và chi phí.

Trong thực tiễn, càng dùng luồng sự kiện làm sổ cái dài hạn của tổ chức thì quản trị dữ liệu càng quan trọng. Phải gắn chủ sở hữu, thời hạn lưu giữ, quyền truy cập, khóa mã hóa, việc có chứa thông tin cá nhân hay không, thủ tục pháp lý xóa · che dữ liệu của sự kiện với hệ thống phân loại dữ liệu. Nguyên tắc sự kiện là bất biến không có nghĩa là phải lưu giữ thông tin cá nhân mãi mãi, nên cần thiết kế các chính sách như tách định danh, hủy khóa mã hóa, xóa mô hình dẫn xuất khi hết thời hạn lưu giữ.

9. Những điểm cần cân nhắc và hàm ý

A. Ưu tiên sự phù hợp với nghiệp vụ

CQRS và Event Sourcing không phải là công nghệ loại bỏ độ phức tạp mà là công nghệ chuyển độ phức tạp thành mô hình và quy trình vận hành tường minh. Nếu áp dụng cho nghiệp vụ có đọc và ghi đơn giản, chỉ làm tăng chi phí vận hành kho lưu trữ, thông điệp, projection và tăng gánh nặng hiểu biết của đội ngũ. Phải đồng thời đặt ra tiêu chí định lượng và định tính để xét xem lịch sử, khả năng tái hiện, nhiều view, mở rộng độc lập có thực sự gắn với giá trị kinh doanh hay không.

B. Làm rõ ranh giới nhất quán

Phải phân biệt theo từng nghiệp vụ giữa nhất quán mạnh của nguồn ghi và nhất quán cuối cùng của mô hình đọc. Phê duyệt thanh toán, trừ tồn kho, giữ ghế ưu tiên tính nguyên tử và kiểm soát đồng thời ở phía lệnh, còn tìm kiếm, thống kê, thông báo có thể chấp nhận độ trễ và xử lý lại. Thay vì cố gộp mọi dữ liệu vào một giao dịch toàn cục, hãy thiết kế ranh giới bằng Aggregate và các quy trình bù trừ · hiệu chỉnh.

C. Bảo đảm khả năng vận hành

Phải quản lý độ trễ sự kiện, consumer lag, hàng đợi thất bại, lỗi lược đồ, thời gian phát lại như các chỉ số mức dịch vụ. Nếu không có giám sát, khó biết được mô hình đọc có ở trạng thái cũ không, sự kiện có bị mất không, Projector có thất bại lặp lại không. Phản ánh quyền và phê duyệt xử lý lại, sao lưu checkpoint, quy trình phát lại mô hình đọc khi sự cố vào sổ tay vận hành và công cụ tự động hóa.

D. Bảo vệ dữ liệu và kiểm toán

Vì sự kiện được lưu giữ lâu dài, có thể cần mức bảo vệ cao hơn log thông thường. Không đưa trực tiếp thông tin nhạy cảm vào Payload sự kiện mà áp dụng thu thập tối thiểu, token hóa, mã hóa trường, tách quyền truy cập. Khả năng truy vết cho mục đích kiểm toán có thể xung đột với nghĩa vụ xóa · lưu giữ thông tin cá nhân, nên cần tách chính sách lưu giữ của sổ cái, thông tin định danh, mô hình truy vấn dẫn xuất và thống nhất với bộ phận pháp chế · phụ trách thông tin cá nhân.

E. Hợp đồng có khả năng tiến hóa

Vì sự kiện kết nối producer với nhiều consumer, cần đặt quy tắc tương thích lược đồ để việc refactor của một đội không buộc phải triển khai toàn bộ. Quản lý việc thêm · xóa · đổi nghĩa trường, đặt tên sự kiện, nâng phiên bản, thời điểm loại bỏ bằng kiểm thử hợp đồng và registry. Khi sự kiện hiện có không chứa thông tin mà consumer mới cần, an toàn hơn là định nghĩa sự kiện mới hoặc sự kiện tích hợp riêng thay vì thay đổi ý nghĩa của sự kiện hiện có.

F. Hàm ý dưới góc nhìn Kỹ sư chuyên nghiệp (Professional Engineer)

Kỹ sư chuyên nghiệp phải đưa vào bài làm không chỉ ưu điểm rời rạc “tách ra thì mở rộng được” mà cả chi phí nhất quán trễ, trùng lặp, xử lý lại, quản trị dữ liệu phát sinh do việc tách. Bản ghi quyết định kiến trúc (ADR) cần ghi lại mục tiêu nghiệp vụ, thuộc tính chất lượng, so sánh phương án, phạm vi áp dụng, chiến lược chuyển đổi, phương án khôi phục khi thất bại. Khi hệ thống hướng sự kiện và nền tảng dữ liệu ngày càng liên kết, chất lượng · hợp đồng · bảo mật của sự kiện nguồn trở thành nền tảng chung cho phân tích và vận hành, vì vậy cần góc nhìn xem xét đồng thời thiết kế ứng dụng và quản trị dữ liệu.

Tài liệu tham khảo


Tóm tắt một câu: CQRS tách biệt trách nhiệm đọc · ghi, còn Event Sourcing lưu giữ sự thật về thay đổi trạng thái làm nguồn gốc, vì vậy khi kết hợp hai mẫu phải thiết kế đồng thời không chỉ khả năng mở rộng và tái hiện mà cả nhất quán cuối cùng, độ phức tạp vận hành và bảo vệ dữ liệu.