Menu
ESC

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

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

Đang tải...

Event-driven architecture: Kafka, RabbitMQ high level

API and Technical Fundamentals cho BA Bài 39/60

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

Hãy tưởng tượng bạn là BA cho một sàn thương mại điện tử. Khi khách bấm "Đặt hàng", có cả chục việc phải xảy ra: trừ kho, tạo đơn vận chuyển, gửi email xác nhận, cộng điểm thành viên, ghi nhận doanh thu cho báo cáo, đẩy thông báo cho nhà bán. Nếu mọi việc này phải xảy ra ngay lập tức, lần lượt, trong cùng một request — thì chỉ cần dịch vụ tính điểm thành viên bị chậm là cả nút "Đặt hàng" treo cứng, khách bỏ giỏ hàng.

Đây chính là vấn đề mà Event-Driven Architecture (kiến trúc hướng sự kiện) sinh ra để giải quyết. Thay vì dịch vụ A gọi thẳng dịch vụ B, C, D và chờ tất cả trả lời, dịch vụ A chỉ cần "thông báo" rằng "Đơn hàng vừa được tạo" rồi đi tiếp. Những dịch vụ quan tâm sẽ tự lắng nghe và xử lý phần việc của mình.

Là một BA, bạn không cần code Kafka hay RabbitMQ. Nhưng bạn sẽ liên tục ngồi họp với team kỹ thuật, đọc tài liệu thiết kế hệ thống, viết yêu cầu cho các tính năng "khi X xảy ra thì Y phải làm gì". Nếu bạn không phân biệt được khái niệm event với command, không hiểu vì sao đội kỹ thuật chọn message queue, không nắm được hệ quả của việc xử lý bất đồng bộ với trải nghiệm người dùng — bạn sẽ viết ra những yêu cầu mâu thuẫn với kiến trúc, hoặc bỏ sót những kịch bản lỗi quan trọng. Bài này giúp bạn nói cùng ngôn ngữ với kỹ sư hệ thống ở mức "high level" đúng nghĩa: đủ sâu để ra quyết định, không sa vào chi tiết triển khai.

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

Event là gì, khác Command ra sao

Cốt lõi của tư duy hướng sự kiện nằm ở một sự phân biệt nhỏ nhưng rất quan trọng.

Command (mệnh lệnh) là yêu cầu ai đó làm một việc gì đó. Ví dụ: "Hãy trừ kho 1 sản phẩm", "Hãy gửi email cho khách". Command có người gửi cụ thể, người nhận cụ thể, và người gửi thường mong đợi một kết quả trả về. Đây chính là cách REST API truyền thống hoạt động — bạn gọi POST /orders, bạn chờ phản hồi.

Event (sự kiện) là lời thông báo rằng một việc gì đó đã xảy ra rồi. Ví dụ: "Đơn hàng #12345 đã được tạo", "Khoản thanh toán đã thành công". Lưu ý cách dùng thì quá khứ — event mô tả một sự thật đã thành hình. Người phát ra event không quan tâm ai sẽ xử lý, cũng không chờ đợi câu trả lời. Nó chỉ kể lại một sự kiện và đi tiếp.

Sự khác biệt này dẫn tới hệ quả lớn về mặt thiết kế. Với command, dịch vụ phát lệnh phải biết dịch vụ nào sẽ nhận — nó bị ràng buộc chặt (tight coupling). Với event, dịch vụ phát ra hoàn toàn không cần biết ai đang lắng nghe — đây là loose coupling (kết nối lỏng). Bạn có thể thêm một dịch vụ mới lắng nghe event "Đơn hàng đã tạo" mà không phải sửa một dòng nào ở dịch vụ tạo đơn.

Producer, Consumer và Broker

Trong kiến trúc hướng sự kiện có ba vai trò bạn cần thuộc lòng:

  • Producer (hay Publisher): dịch vụ tạo ra và phát đi event. Ví dụ Order Service phát event "OrderCreated".
  • Consumer (hay Subscriber): dịch vụ lắng nghe và xử lý event. Ví dụ Inventory Service nghe "OrderCreated" để trừ kho.
  • Broker (hay Message Bus): hạ tầng trung gian đứng giữa, nhận event từ producer và chuyển tới consumer. Đây chính là vai trò của KafkaRabbitMQ.
Broker quan trọng ở chỗ: nó tách rời producer và consumer cả về không gian lẫn thời gian. Producer phát event xong là xong, dù consumer đang bận, đang bảo trì, hay tạm sập — event vẫn nằm an toàn trong broker chờ được xử lý. Đây gọi là khả năng "đệm" (buffering) và đảm bảo không mất tin (durability).

Kafka và RabbitMQ — hai triết lý khác nhau

Đây là phần BA hay được hỏi: "Dùng Kafka hay RabbitMQ?". Bạn không cần biết cấu hình, nhưng nên nắm tinh thần.

RabbitMQ là một message broker kiểu truyền thống, hoạt động giống một bưu điện thông minh. Nó nhận tin nhắn, định tuyến tới đúng hàng đợi (queue), và khi consumer xử lý xong thì tin nhắn được xóa khỏi queue. RabbitMQ rất mạnh ở việc định tuyến phức tạp (routing), phân phối công việc cho nhiều worker, và phù hợp với các tác vụ kiểu "giao việc rồi quên" (task queue). Hãy nghĩ tới: gửi email, xử lý ảnh, tạo file PDF hóa đơn.

Kafka lại là một distributed event log — nó giống một cuốn nhật ký khổng lồ ghi lại mọi sự kiện theo thứ tự thời gian, và event không bị xóa sau khi đọc (nó được giữ lại theo thời gian cấu hình, ví dụ 7 ngày hoặc vĩnh viễn). Nhiều consumer khác nhau có thể đọc lại cùng một dòng sự kiện ở những thời điểm khác nhau, thậm chí "tua lại từ đầu". Kafka được sinh ra cho khối lượng dữ liệu cực lớn (hàng triệu event mỗi giây), cho các tình huống nhiều hệ thống cùng tiêu thụ một luồng dữ liệu: phân tích real-time, đồng bộ dữ liệu giữa nhiều service, ghi log hành vi người dùng.

Một cách ví von dễ nhớ: RabbitMQ là danh sách việc cần làm — làm xong thì gạch đi. Kafka là cuốn băng ghi hình — phát lại bao nhiêu lần cũng được, ai muốn xem cũng được.

Sync vs Async — gốc rễ của mọi quyết định

Kiến trúc hướng sự kiện gắn liền với xử lý bất đồng bộ (asynchronous). Trong mô hình đồng bộ (synchronous), bên gọi phải chờ bên kia xong mới làm tiếp — như gọi điện thoại, hai bên phải online cùng lúc. Trong mô hình bất đồng bộ, bên gọi gửi đi rồi làm việc khác ngay — như nhắn tin Zalo, bạn gửi xong là xong, người kia trả lời lúc nào tùy họ.

Event-driven về bản chất là async. Hệ quả: hệ thống chịu tải tốt hơn, các dịch vụ ít phụ thuộc nhau hơn, một dịch vụ sập không kéo sập cả dây chuyền. Nhưng cái giá phải trả là sự phức tạp về tính nhất quán: kết quả không xuất hiện ngay lập tức (eventual consistency — nhất quán sau cùng), và việc theo dõi một luồng nghiệp vụ đi qua nhiều service trở nên khó hơn nhiều.

Tình huống thực tế

Tình huống 1: Tiki và nút "Đặt hàng" trong ngày sale 11/11

Hãy hình dung một sàn như Tiki vào đợt sale lớn. Vào giờ flash sale, hệ thống nhận tới 8.000–10.000 đơn mỗi phút. Nếu mỗi đơn hàng phải xử lý tuần tự: trừ kho → tính phí ship → cộng điểm Astra → gửi email → đẩy đơn cho nhà bán → ghi nhận doanh thu, và mỗi bước mất 200ms, thì tổng cộng một đơn mất hơn 1 giây chỉ riêng phần backend, chưa kể nếu một dịch vụ (ví dụ tính điểm) bị nghẽn thì cả nút "Đặt hàng" treo.

Diễn giải: Với kiến trúc hướng sự kiện, khi khách bấm đặt hàng, Order Service chỉ làm hai việc tối thiểu — kiểm tra giỏ hàng hợp lệ và lưu đơn — rồi phát một event "OrderPlaced" lên Kafka và lập tức trả về cho khách màn hình "Đặt hàng thành công". Tất cả 6 việc còn lại trở thành các consumer độc lập, mỗi cái tự đọc event và xử lý theo nhịp riêng. Dịch vụ gửi email có chậm 5 giây cũng không ảnh hưởng gì tới trải nghiệm khách.

Bài học rút ra: Việc tách các tác vụ "không cần ngay" ra khỏi luồng đồng bộ chính là cách hệ thống chịu được tải đột biến. Là BA, khi viết yêu cầu cho tính năng đặt hàng, bạn cần chủ động xác định việc nào bắt buộc xong trước khi báo thành công (kiểm tra tồn kho, trừ tiền) và việc nào có thể xử lý sau (email, điểm thưởng). Câu hỏi này định hình cả kiến trúc.

Tình huống 2: Ví điện tử MoMo và thanh toán hóa đơn điện

Giả sử bạn là BA cho luồng "thanh toán hóa đơn điện" của một ví như MoMo. Khi người dùng trả tiền điện, ví phải gọi sang hệ thống của công ty điện lực (EVN) để ghi nhận. Nhưng hệ thống đối tác đôi khi phản hồi chậm, đôi khi quá tải vào cuối tháng khi ai cũng đóng tiền điện.

Diễn giải: Nếu thiết kế đồng bộ — ví chờ EVN xác nhận rồi mới báo người dùng — thì vào ngày cao điểm, người dùng sẽ thấy màn hình quay vòng vòng rồi báo lỗi, dù tiền đã bị trừ. Cách làm hướng sự kiện: ví trừ tiền, ghi nhận giao dịch ở trạng thái "đang xử lý", phát event "BillPaymentRequested" vào một message queue (rất hợp với RabbitMQ vì đây là tác vụ kiểu giao-việc), và báo người dùng "Giao dịch đang được xử lý". Một worker đọc queue, gọi EVN với cơ chế thử lại (retry) nếu đối tác chậm. Khi EVN xác nhận, hệ thống phát tiếp event "BillPaymentConfirmed" để cập nhật trạng thái và gửi thông báo "Thanh toán thành công" cho người dùng.

Bài học rút ra: Async cho phép hệ thống kiên nhẫn với đối tác bên ngoài thay vì sập theo họ. Nhưng nó buộc bạn — người viết yêu cầu — phải thiết kế thêm các trạng thái trung gian ("đang xử lý") và các kịch bản thất bại ("nếu EVN không phản hồi sau 3 lần thử thì hoàn tiền và báo thất bại"). Đây là phần BA hay quên, và nó quan trọng hơn cả luồng thành công.

Tình huống 3: Grab và việc nhiều đội cùng cần một luồng dữ liệu

Tại một nền tảng như Grab, mỗi chuyến xe hoàn thành sinh ra một event "TripCompleted". Điều thú vị là rất nhiều đội khác nhau cần dữ liệu này: đội thanh toán cần để trừ tiền, đội tài chính cần để đối soát với tài xế, đội khuyến mãi cần để cộng điểm GrabRewards, đội data science cần để phân tích nhu cầu theo khu vực, đội chống gian lận cần để phát hiện chuyến bất thường.

Diễn giải: Đây chính là tình huống Kafka tỏa sáng. Event "TripCompleted" được phát một lần lên một "topic" (chủ đề) trong Kafka. Năm đội nói trên trở thành năm nhóm consumer độc lập, mỗi nhóm đọc cùng dòng sự kiện đó theo cách riêng mà không giẫm chân nhau. Khi mai này có thêm đội thứ sáu — chẳng hạn đội bảo hiểm chuyến đi — họ chỉ cần "đăng ký" đọc topic mà không cần đội tài xế sửa bất cứ thứ gì. Vì Kafka giữ lại lịch sử event, đội mới còn có thể "tua lại" dữ liệu nhiều ngày trước để khởi tạo hệ thống của mình.

Bài học rút ra: Khi một sự kiện nghiệp vụ có nhiều bên cùng quan tâm và danh sách các bên còn mở rộng theo thời gian, kiến trúc hướng sự kiện với một event log như Kafka giúp tổ chức mở rộng mà không vỡ trận. Là BA, nhận diện được "đây là một sự kiện nhiều bên quan tâm" sẽ giúp bạn đề xuất đúng hướng kiến trúc ngay từ đầu.

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

Dưới đây là cách một BA tiếp cận khi gặp một bài toán có thể cần kiến trúc hướng sự kiện.

  • Xác định các "sự kiện nghiệp vụ" trong miền của bạn. Liệt kê những việc đã xảy ra mà hệ thống cần phản ứng, đặt tên ở thì quá khứ: "Đơn hàng đã tạo", "Thanh toán đã thành công", "Người dùng đã đăng ký", "Hàng đã giao". Đây là bước "event storming" đơn giản hóa.
  • Phân loại việc xử lý: cần ngay hay xử lý sau. Với mỗi sự kiện, hỏi: việc nào bắt buộc phải hoàn tất trước khi báo kết quả cho người dùng? Việc nào có thể chạy nền? Ranh giới này quyết định cái gì nằm trong luồng đồng bộ, cái gì đẩy ra event.
  • Xác định ai phát, ai nghe. Vẽ ra: dịch vụ nào là producer của event này, những dịch vụ nào là consumer. Đừng quên hỏi "trong tương lai còn ai có thể cần nghe?" — nếu danh sách dài và mở, đó là tín hiệu nghiêng về mô hình event log.
  • Định nghĩa cấu trúc dữ liệu của event (payload). Event nên mang đủ thông tin để consumer xử lý mà không phải gọi ngược lại hỏi thêm. Ví dụ event "OrderPlaced" nên có order ID, danh sách sản phẩm, tổng tiền, ID khách hàng. Bạn viết phần này y như viết một JSON schema cho API.
  • Thiết kế các kịch bản thất bại. Đây là phần sống còn. Hỏi: Nếu consumer xử lý lỗi thì sao? Có thử lại không, thử mấy lần? Nếu một event bị xử lý hai lần thì có gây hậu quả không (ví dụ cộng điểm hai lần)? Nếu cộng hai lần là sai, hệ thống cần cơ chế chống trùng (idempotency).
  • Thống nhất với team kỹ thuật về trải nghiệm "nhất quán sau cùng". Vì kết quả không hiện ngay, bạn cần định nghĩa giao diện trong khoảng thời gian chờ: hiển thị trạng thái "đang xử lý" thế nào, bao lâu thì coi là thất bại, có thông báo đẩy khi xong không.

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

Lỗi 1 — Nhầm event với command. Nhiều BA viết yêu cầu kiểu "phát event để bắt dịch vụ kho trừ hàng". Câu này sai tinh thần: nếu bạn bắt một dịch vụ cụ thể làm việc cụ thể, đó là command trá hình. Event đúng nghĩa là "Đơn hàng đã tạo" — còn việc kho phản ứng thế nào là chuyện của kho. Đặt tên event ở thì quá khứ giúp bạn tránh lỗi này.

Lỗi 2 — Quên rằng event có thể đến hai lần. Trong hệ thống phân tán, một event có thể được gửi lặp (do retry, do lỗi mạng). Nếu consumer "cộng điểm thành viên" mà xử lý hai lần, khách được cộng đôi điểm. Mẹo: luôn hỏi đội kỹ thuật "consumer này đã idempotent chưa?" với mọi tác vụ liên quan tiền bạc, tồn kho, điểm thưởng.

Lỗi 3 — Lạm dụng async cho việc cần đồng bộ. Không phải mọi thứ đều nên hướng sự kiện. Việc kiểm tra mật khẩu khi đăng nhập, kiểm tra số dư trước khi trừ tiền — những việc người dùng cần biết kết quả ngay — phải đồng bộ. Async chỉ dành cho việc "làm sau cũng được".

Lỗi 4 — Bỏ quên thứ tự sự kiện. Trong một số nghiệp vụ, thứ tự rất quan trọng: "Tài khoản đã tạo" phải xử lý trước "Tài khoản đã nâng cấp". Hãy nêu rõ trong yêu cầu khi thứ tự là bắt buộc, vì đảm bảo thứ tự trong hệ phân tán không hề miễn phí.

Mẹo lớn: Khi mô tả luồng hướng sự kiện trong tài liệu, hãy vẽ sơ đồ với các mũi tên một chiều từ producer tới broker tới các consumer, ghi rõ tên event ở thì quá khứ trên mỗi mũi tên. Một sơ đồ như vậy nói rõ ý đồ hơn ba trang chữ.

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

  • Phân biệt event và command. Với mỗi câu sau, xác định đó là event hay command, và viết lại cho đúng kiểu event (thì quá khứ): (a) "Gửi SMS xác nhận cho khách", (b) "Khách đã hủy đơn hàng", (c) "Trừ 50.000đ trong ví", (d) "Tài xế đã nhận chuyến".
  • Thiết kế luồng event cho đăng ký khóa học. Bạn là BA cho một nền tảng học trực tuyến. Khi học viên đăng ký một khóa học có trả phí, hãy: liệt kê các sự kiện nghiệp vụ phát sinh, xác định việc nào cần xử lý đồng bộ và việc nào async, và liệt kê các consumer khả dĩ cho event "EnrollmentCompleted".
  • Chọn Kafka hay RabbitMQ. Cho hai tình huống, hãy chọn và giải thích ngắn gọn: (a) một hàng đợi gửi email marketing cho 100.000 người, mỗi worker gửi một email rồi xong; (b) một luồng sự kiện "người dùng click sản phẩm" mà cả đội đề xuất, đội phân tích và đội quảng cáo đều cần đọc.
  • Viết kịch bản thất bại. Với tình huống MoMo thanh toán hóa đơn điện ở trên, viết ra ít nhất ba kịch bản lỗi và cách hệ thống nên xử lý mỗi kịch bản (gợi ý: EVN không phản hồi, EVN báo lỗi sai hóa đơn, hệ thống ví bị sập giữa chừng sau khi đã trừ tiền).

Tóm tắt

Kiến trúc hướng sự kiện thay đổi cách các dịch vụ giao tiếp: thay vì ra lệnh cho nhau (command, tight coupling), chúng thông báo những việc đã xảy ra (event, loose coupling) thông qua một broker trung gian. Ba vai trò cốt lõi là producer (phát event), consumer (lắng nghe và xử lý) và broker (Kafka hoặc RabbitMQ) đứng giữa làm nhiệm vụ trung chuyển và lưu đệm.

RabbitMQ giống danh sách việc cần làm — hợp với giao việc cho worker, xử lý xong là xóa; Kafka giống cuốn nhật ký event giữ lại lịch sử — hợp với khối lượng lớn và nhiều bên cùng tiêu thụ một luồng dữ liệu. Cả hai đều gắn với tư duy bất đồng bộ (async): hệ thống chịu tải tốt và ít phụ thuộc nhau hơn, đổi lại là sự nhất quán sau cùng và độ phức tạp cao hơn trong việc theo dõi luồng nghiệp vụ.

Với vai trò BA, giá trị của bạn không nằm ở việc cấu hình Kafka, mà ở chỗ: nhận diện đúng các sự kiện nghiệp vụ, phân định việc nào cần đồng bộ và việc nào async, đặt câu hỏi đúng về idempotency và kịch bản thất bại, và mô tả rõ trải nghiệm người dùng trong khoảng thời gian chờ. Nắm vững những điều này, bạn sẽ viết được những yêu cầu ăn khớp với kiến trúc thực tế thay vì mâu thuẫn với nó.