Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 20 — Event-Driven Architecture & Message Queue

Technical BA Masterclass Bài 20/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn đang viết spec cho một hệ thống đặt hàng. Khách bấm "Thanh toán", và sau đó hàng loạt việc phải xảy ra: trừ tồn kho, gửi email xác nhận, ghi điểm tích lũy, đẩy đơn xuống kho vận chuyển, cập nhật báo cáo doanh thu. Nếu bạn để service "Đặt hàng" gọi trực tiếp từng service kia một cách tuần tự, thì chuyện gì xảy ra khi service "Gửi email" bị treo 5 giây? Khách hàng phải ngồi chờ 5 giây đó, dù việc gửi email chẳng liên quan gì đến việc đơn hàng đã được ghi nhận hay chưa. Tệ hơn, nếu service tích điểm sập, cả luồng đặt hàng có thể sập theo.

Đây chính là vấn đề mà Event-Driven Architecture (EDA) — kiến trúc hướng sự kiện — sinh ra để giải quyết. Thay vì các service gọi nhau trực tiếp như gọi điện thoại (phải có người nghe máy ngay), chúng giao tiếp bằng cách "thông báo có chuyện vừa xảy ra" lên một trung gian, rồi ai quan tâm thì tự xử lý.

Vì sao một Technical BA cần hiểu điều này? Bởi vì khi bạn viết spec cho hệ thống hiện đại, bạn sẽ liên tục gặp các quyết định kiểu: "Phần này nên gọi đồng bộ hay bắn event?", "Nếu event bị mất thì sao?", "Khách hàng có chấp nhận dữ liệu cập nhật chậm vài giây không?". Những câu hỏi này không phải của riêng developer — chúng quyết định trải nghiệm người dùng và yêu cầu nghiệp vụ. Một BA không hiểu EDA sẽ viết ra spec đòi hỏi "real-time tức thì" ở chỗ không cần, hoặc bỏ sót yêu cầu xử lý lỗi ở chỗ rất cần. Bài này giúp bạn nói cùng ngôn ngữ với architect và viết spec đúng bản chất.

Khái niệm cốt lõi

Event là gì?

Event (sự kiện) là một bản ghi bất biến mô tả "một việc đã xảy ra trong quá khứ". Tên event luôn ở thì quá khứ: OrderPlaced (đơn đã đặt), PaymentCompleted (thanh toán đã hoàn tất), UserRegistered (người dùng đã đăng ký). Đây là điểm khác biệt tư duy quan trọng so với cách gọi lệnh truyền thống.

Khi bạn gọi API kiểu RPC (Remote Procedure Call), bạn ra lệnh: "Hãy trừ tồn kho đi". Người gọi biết chính xác ai sẽ thực thi và chờ kết quả. Còn với event, bạn chỉ thông báo: "Đơn hàng số 12345 vừa được đặt". Người phát event không cần biết — và không nên biết — ai sẽ nghe và làm gì với thông tin đó. Đây gọi là loose coupling (liên kết lỏng): các service ít phụ thuộc vào nhau.

Message Broker — bưu điện trung tâm

Để các event đi từ nơi phát đến nơi nhận, ta cần một trung gian gọi là message broker (môi giới tin nhắn). Hãy hình dung nó như một bưu điện: người gửi thả thư vào, bưu điện giữ thư an toàn và chuyển đến đúng hộp thư người nhận, kể cả khi người nhận đang đi vắng. Các broker phổ biến gồm Kafka, RabbitMQ, AWS SNS/SQS, Google Pub/Sub, Azure Service Bus.

Vai trò của broker là gì? Thứ nhất, đệm (buffer): nếu service nhận đang bận, message xếp hàng chờ chứ không bị mất. Thứ hai, đảm bảo giao nhận: broker lưu message cho đến khi có bên xử lý xong. Thứ ba, tách rời thời gian: bên phát và bên nhận không cần online cùng lúc.

Hai pattern nền tảng: Pub/Sub và Message Queue

Đây là hai mô hình bạn phải phân biệt được, vì chúng phục vụ mục đích nghiệp vụ khác nhau.

1. Publish/Subscribe (Pub/Sub) — một-thông-báo-nhiều-người-nghe.

Một service publish (phát) một event lên một topic (chủ đề). Mọi service đã subscribe (đăng ký) topic đó đều nhận được một bản sao. Ví dụ event OrderPlaced được phát ra, thì service Kho, service Email, service Tích điểm — mỗi bên nhận một bản và xử lý độc lập. Đây giống như đài phát thanh: một lần phát sóng, mọi cái radio đang bật đều nghe được.

Pub/Sub phù hợp khi một sự kiện kéo theo nhiều hệ quả độc lập, và bạn muốn dễ dàng thêm bên nghe mới mà không sửa bên phát. Khi sếp yêu cầu "thêm tính năng gửi SMS khi đặt hàng", bạn chỉ cần tạo service mới subscribe vào topic OrderPlaced, không động đến service Đặt hàng.

2. Message Queue (hàng đợi) — một-thông-báo-một-người-xử-lý.

Message được đẩy vào một queue, và chỉ một consumer (bên tiêu thụ) trong nhóm lấy ra xử lý. Nếu bạn có 5 worker cùng đọc một queue, mỗi message chỉ được giao cho 1 worker. Mô hình này lý tưởng để phân tải công việc: 1000 đơn hàng cần xử lý ảnh, bạn bật 10 worker, mỗi worker gắp một phần, làm xong gắp tiếp.

Sự khác biệt cốt lõi: Pub/Sub là broadcast (ai cũng nghe), Queue là work distribution (chia việc, mỗi việc một người). Trên thực tế Kafka kết hợp cả hai qua khái niệm consumer group: nhiều group khác nhau đều nhận đủ event (giống Pub/Sub), nhưng trong cùng một group thì mỗi message chỉ một consumer xử lý (giống Queue).

Một vài khái niệm phải nắm để viết spec

  • At-least-once delivery: broker đảm bảo message được giao ít nhất một lần — nghĩa là có thể giao trùng. Đây là mặc định phổ biến nhất. Hệ quả: consumer phải xử lý được tình huống nhận trùng (vấn đề idempotency, sẽ học sâu ở Bài 29).
  • Eventual consistency (nhất quán cuối cùng): dữ liệu giữa các service không đồng bộ tức thì mà "cuối cùng sẽ giống nhau" sau một khoảng trễ. Điểm tích lũy có thể hiện sau vài giây chứ không ngay lập tức.
  • Dead Letter Queue (DLQ): hàng đợi chứa những message xử lý thất bại nhiều lần, để đội vận hành xem xét sau thay vì làm tắc nghẽn hệ thống.

Tình huống thực tế

Ví dụ 1 — Tiki và bài toán "nút Thanh toán bị treo"

Một sàn thương mại điện tử lớn của Việt Nam (tạm gọi theo mô hình Tiki) ban đầu xây luồng đặt hàng kiểu đồng bộ: service Đặt hàng gọi tuần tự 6 service con. Vào ngày sale lớn 12/12, lượng đơn tăng đột biến. Service Gửi email kết nối tới nhà cung cấp email bên thứ ba bị chậm, mỗi lần gọi mất 3–4 giây. Vì luồng đồng bộ, mỗi đơn hàng bị treo chờ email xong mới trả kết quả về khách. Kết quả: thời gian xác nhận đơn tăng từ 800ms lên hơn 5 giây, nhiều khách bấm lại nhiều lần tạo đơn trùng, tỷ lệ bỏ giỏ hàng tăng vọt.

Diễn giải: Đội kỹ thuật tái thiết kế theo EDA. Service Đặt hàng chỉ làm đúng việc cốt lõi — ghi đơn vào database — rồi publish event OrderPlaced lên Kafka và trả kết quả về khách ngay (dưới 500ms). Các service Email, Tích điểm, Kho, Phân tích đều subscribe topic này và xử lý bất đồng bộ. Email gửi chậm vài giây cũng không ảnh hưởng đến trải nghiệm bấm "Đặt hàng".

Bài học cho BA: Khi viết spec, hãy phân biệt việc nào đồng bộ bắt buộc (khách phải thấy kết quả ngay: tạo đơn, kiểm tra tồn kho khả dụng) với việc nào có thể bất đồng bộ (gửi email, tích điểm, cập nhật báo cáo). Trong tài liệu yêu cầu, hãy ghi rõ: "Email xác nhận được gửi trong vòng 60 giây sau khi đặt hàng" thay vì mặc định "gửi ngay tức thì". Một câu chữ này quyết định cả kiến trúc.

Ví dụ 2 — Ngân hàng số và sự cố ghi nợ hai lần

Một ngân hàng số tại Đông Nam Á dùng message queue để xử lý giao dịch chuyển tiền nội bộ. Một message DebitAccount (trừ tiền tài khoản) được đẩy vào queue. Do mạng chập chờn, consumer xử lý xong nhưng chưa kịp báo "đã nhận" (acknowledge) cho broker, nên broker tưởng thất bại và giao lại message đó lần nữa. Hậu quả: một tài khoản bị trừ tiền hai lần cho cùng một giao dịch — một sự cố nghiêm trọng trong ngành ngân hàng.

Diễn giải: Đây chính là mặt trái của cơ chế at-least-once delivery. Đội kỹ thuật khắc phục bằng cách thêm transaction_id duy nhất cho mỗi giao dịch; consumer trước khi trừ tiền sẽ kiểm tra "đã từng xử lý transaction_id này chưa?". Nếu rồi thì bỏ qua. Đây là nguyên tắc idempotency.

Bài học cho BA: Khi viết spec cho luồng dùng message queue, luôn đặt câu hỏi: "Nếu message này bị xử lý hai lần thì điều gì xảy ra?". Với nghiệp vụ tài chính, kế toán, tồn kho — hậu quả của xử lý trùng là rất nặng. Bạn cần ghi vào spec một business key duy nhất để hệ thống nhận diện trùng lặp (mã giao dịch, mã đơn). Đây là loại yêu cầu mà chỉ BA hiểu nghiệp vụ mới chỉ ra được, developer không tự đoán ra.

Ví dụ 3 — Grab và việc tách rời các đội phát triển

Một nền tảng gọi xe trong khu vực (mô hình Grab) có hàng chục đội phát triển khác nhau: đội Chuyến đi, đội Thanh toán, đội Khuyến mãi, đội Chống gian lận, đội Phân tích dữ liệu. Khi một chuyến xe hoàn thành, có ít nhất 8 đội quan tâm đến sự kiện đó vì các lý do hoàn toàn khác nhau.

Diễn giải: Nếu đội Chuyến đi phải gọi trực tiếp tới 8 service kia, mỗi lần một đội mới muốn dùng dữ liệu chuyến đi, đội Chuyến đi lại phải sửa code và deploy lại — một cơn ác mộng về phối hợp. Thay vào đó, đội Chuyến đi chỉ publish một event RideCompleted chứa đầy đủ thông tin. Đội Chống gian lận subscribe để phát hiện mẫu bất thường; đội Khuyến mãi subscribe để cộng tiến độ thử thách "đi 10 chuyến nhận quà"; đội Phân tích subscribe để cập nhật dashboard. Mỗi đội tự chủ hoàn toàn.

Bài học cho BA: EDA không chỉ là quyết định kỹ thuật — nó ảnh hưởng cách tổ chức đội và tốc độ ra tính năng. Khi bạn viết spec cho một event, hãy thiết kế nó như một "hợp đồng dữ liệu" (data contract) đầy đủ và bền vững, vì nhiều đội khác sẽ phụ thuộc vào nó. Hãy hỏi: "Những ai có thể quan tâm đến sự kiện này trong tương lai?" và đưa đủ trường dữ liệu cần thiết vào event ngay từ đầu.

Hướng dẫn từng bước

Khi bạn được giao spec một luồng nghiệp vụ có khả năng dùng EDA, hãy đi theo các bước sau.

Bước 1 — Vẽ ra chuỗi hệ quả của một hành động. Bắt đầu từ một hành động nghiệp vụ ("khách đặt hàng") và liệt kê mọi việc cần xảy ra sau đó. Đây là nguyên liệu để nhận ra đâu là các event.

Bước 2 — Phân loại đồng bộ vs bất đồng bộ. Với mỗi việc, hỏi: "Khách hàng có cần thấy kết quả này ngay trên màn hình không?". Nếu có → đồng bộ. Nếu không → ứng viên cho xử lý bất đồng bộ qua event. Ghi rõ phân loại này trong tài liệu.

Bước 3 — Đặt tên event theo thì quá khứ. Dùng quy ước DanhTừ + ĐộngTừ quá khứ: InvoiceIssued, ShipmentDelivered. Tránh đặt tên kiểu mệnh lệnh như SendEmail — đó là command, không phải event.

Bước 4 — Thiết kế payload (nội dung) của event. Liệt kê các trường dữ liệu event mang theo. Nguyên tắc: đủ để bên nhận xử lý mà không cần gọi ngược lại hỏi thêm, nhưng không nhồi nhét dữ liệu nhạy cảm không cần thiết. Luôn có: ID duy nhất của event, business key (mã đơn/giao dịch), thời điểm phát, phiên bản schema.

Bước 5 — Chọn pattern: Pub/Sub hay Queue. Sự kiện có nhiều bên nghe độc lập? → Pub/Sub. Một loại công việc cần chia tải cho nhiều worker? → Queue.

Bước 6 — Định nghĩa yêu cầu xử lý lỗi. Trả lời trong spec: Nếu xử lý thất bại thì thử lại bao nhiêu lần? Sau đó đưa vào DLQ? Ai theo dõi DLQ? Nếu nhận message trùng thì xử lý ra sao (business key nào để chống trùng)?

Bước 7 — Ghi rõ yêu cầu về độ trễ chấp nhận được. Thay vì "real-time", viết cụ thể: "Điểm tích lũy phản ánh trong vòng 2 phút", "Báo cáo doanh thu cập nhật mỗi 5 phút". Đây là cách diễn đạt eventual consistency dưới ngôn ngữ nghiệp vụ.

Lỗi thường gặp & mẹo

Lỗi 1 — Nhầm event với command. Viết event tên ChargeCustomer (hãy trừ tiền khách) là sai tư duy — đó là một lệnh gửi đích danh. Event đúng phải là OrderPlaced, còn việc trừ tiền là phản ứng của service thanh toán. Mẹo: nếu tên có động từ mệnh lệnh, dừng lại xem xét.

Lỗi 2 — Mặc định mọi thứ phải "real-time tức thì". Nhiều BA viết "hệ thống phải cập nhật ngay lập tức" ở mọi nơi, vô tình ép kiến trúc phải đồng bộ và đắt đỏ. Mẹo: với mỗi yêu cầu cập nhật, hãy hỏi nghiệp vụ "trễ 5 giây có sao không?". Phần lớn câu trả lời là không sao, và điều đó mở đường cho EDA.

Lỗi 3 — Quên xử lý message trùng và message lỗi. Spec chỉ mô tả "đường hạnh phúc" (happy path) mà bỏ qua: message đến trùng, đến sai thứ tự, hoặc xử lý thất bại. Mẹo: với mỗi event, luôn viết một mục "Xử lý ngoại lệ" trả lời ba câu hỏi: trùng thì sao, lỗi thì sao, mất thì sao.

Lỗi 4 — Coi thứ tự message là điều hiển nhiên. Nhiều người tưởng event luôn đến đúng thứ tự phát. Thực tế nhiều broker chỉ đảm bảo thứ tự trong phạm vi hẹp (ví dụ Kafka chỉ đảm bảo trong một partition). Mẹo: nếu nghiệp vụ phụ thuộc thứ tự (ví dụ "đặt hàng" phải trước "hủy hàng"), hãy nêu rõ yêu cầu này để architect chọn cách partition phù hợp.

Lỗi 5 — Lạm dụng EDA cho mọi thứ. EDA tăng độ phức tạp vận hành và gỡ lỗi. Một thao tác cần kết quả tức thì và mang tính giao dịch chặt chẽ (như kiểm tra số dư trước khi cho rút tiền) thì gọi đồng bộ vẫn đúng hơn. Mẹo: EDA hợp với "việc xảy ra sau và độc lập", không phải mọi tương tác.

Mẹo vàng: Khi mô tả một event trong spec, hãy viết câu "Khi [X xảy ra], hệ thống phát event [Y], và các bên [A, B, C] phản ứng bằng cách [...]". Cấu trúc câu này buộc bạn làm rõ cả nguồn, nội dung lẫn hệ quả.

Bài tập thực hành

Bài tập 1 — Phân loại luồng. Cho luồng "Khách hủy đơn hàng đã thanh toán". Hãy liệt kê tất cả việc cần xảy ra sau đó (hoàn tiền, trả tồn kho, gửi thông báo, thu hồi điểm đã cộng, cập nhật báo cáo...). Với mỗi việc, phân loại đồng bộ hay bất đồng bộ, và giải thích lý do dựa trên trải nghiệm khách hàng.

Bài tập 2 — Thiết kế event. Viết đặc tả cho event OrderCancelled: đặt tên đúng quy ước, liệt kê các trường payload (kèm business key chống trùng), chọn Pub/Sub hay Queue và giải thích, rồi viết mục "Xử lý ngoại lệ" trả lời ba câu hỏi trùng/lỗi/mất.

Bài tập 3 — Phản biện kiến trúc. Một developer đề xuất: "Khi khách đăng ký, ta bắn event UserRegistered rồi service Xác thực OTP subscribe để gửi mã, khách chờ trên màn hình." Hãy chỉ ra vấn đề trong thiết kế này (gợi ý: OTP là việc khách cần thấy kết quả ngay) và đề xuất cách sửa.

Bài tập 4 — Viết yêu cầu độ trễ. Lấy một nghiệp vụ trong công ty bạn có dùng cập nhật chậm chấp nhận được (ví dụ dashboard, điểm thưởng, thông báo). Viết lại yêu cầu từ "cập nhật ngay" thành một câu định lượng độ trễ tối đa, và lý giải con số đó với nghiệp vụ.

Tóm tắt

Event-Driven Architecture là cách các service giao tiếp bằng việc thông báo sự kiện đã xảy ra lên một message broker (Kafka, RabbitMQ, SNS/SQS) thay vì gọi nhau trực tiếp. Điều này tạo ra loose coupling: bên phát không cần biết ai nghe, giúp hệ thống chịu tải tốt hơn, dễ mở rộng và cho phép các đội phát triển độc lập.

Hai pattern nền tảng cần phân biệt: Pub/Sub (một event, nhiều bên nghe — dùng khi một sự kiện kéo theo nhiều hệ quả độc lập) và Message Queue (một message, một bên xử lý — dùng để chia tải công việc). Đổi lại sự linh hoạt, EDA mang theo các đặc tính phải xử lý cẩn thận: giao nhận trùng (at-least-once), nhất quán cuối cùng (eventual consistency), và rủi ro về thứ tự message.

Với vai trò Technical BA, giá trị bạn mang lại không phải là chọn broker — đó là việc architect. Giá trị của bạn là: phân biệt việc nào đồng bộ/bất đồng bộ dựa trên trải nghiệm khách hàng, đặt tên và thiết kế payload event như một hợp đồng dữ liệu bền vững, chỉ ra business key để chống xử lý trùng, và diễn đạt eventual consistency thành các yêu cầu độ trễ định lượng mà nghiệp vụ hiểu được. Hãy luôn ghi nhớ ba câu hỏi vàng cho mỗi event: nếu trùng thì sao, nếu lỗi thì sao, nếu mất thì sao. Trả lời được ba câu đó, spec của bạn đã vượt xa phần lớn BA chỉ biết mô tả "đường hạnh phúc".