Menu
ESC

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

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

Đang tải...

Bài 41 — Integration Design — Async & Batch

Technical BA Masterclass Bài 41/60

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

Ở Bài 40, chúng ta đã đi qua tích hợp đồng bộ (synchronous): hệ thống A gọi hệ thống B, đứng chờ, nhận kết quả ngay rồi đi tiếp. Nó đơn giản, dễ debug, nhưng có một điểm yếu chết người: nếu B chậm hoặc sập, A cũng chết theo. Trong thực tế doanh nghiệp Việt Nam — nơi một giao dịch chuyển khoản có thể phải đi qua corebanking, hệ thống chống gian lận, cổng SMS, và đối tác bên thứ ba — bạn không thể bắt người dùng đứng chờ tất cả các bước đó hoàn tất.

Đây chính là lý do bài này tồn tại. Tích hợp bất đồng bộ (async) và xử lý theo lô (batch) là hai vũ khí giúp hệ thống chịu tải lớn, sống sót khi một thành phần bị chậm, và xử lý khối lượng dữ liệu khổng lồ mà không làm sập giao diện người dùng. Là một Technical BA, bạn không cần code message queue, nhưng bạn PHẢI biết khi nào nên đề xuất async thay vì sync, phải viết spec thế nào để dev hiểu đúng, và phải lường trước những cạm bẫy như mất tin nhắn, xử lý trùng, hay tin nhắn đến sai thứ tự.

Nếu bạn viết spec một luồng tích hợp mà không phân biệt được async với batch, hậu quả không chỉ là code sai — mà là tiền chạy sai tài khoản, báo cáo cuối ngày lệch số, hoặc khách hàng nhận hai lần tin nhắn trừ tiền. Bài này sẽ trang bị cho bạn tư duy để tránh những thảm họa đó.

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

Async là gì và khác sync ở đâu

Trong mô hình đồng bộ, bên gọi (caller) chờ phản hồi trước khi tiếp tục. Trong mô hình bất đồng bộ, bên gọi gửi yêu cầu đi rồi đi làm việc khác ngay lập tức; kết quả sẽ quay lại sau qua một cơ chế khác (callback, webhook, hoặc đọc từ hàng đợi). Bản chất của async là tách rời (decouple) theo thời gian — bên gửi và bên nhận không cần cùng có mặt và rảnh tại cùng một thời điểm.

Hãy hình dung sự khác biệt: gọi điện thoại là sync (cả hai phải cùng online, cùng nói chuyện), còn nhắn tin Zalo là async (bạn gửi xong làm việc khác, người kia đọc và trả lời lúc nào rảnh).

Message Queue — mô hình point-to-point

Đây là mô hình async phổ biến nhất. Một Producer (bên sản xuất tin nhắn) đẩy tin vào một Queue (hàng đợi), và một Consumer (bên tiêu thụ) lấy tin ra xử lý.

Producer  →  [ Queue ]  →  Consumer

Đặc điểm cốt lõi của point-to-point: mỗi tin nhắn chỉ được một consumer xử lý. Nếu bạn có 5 consumer cùng nối vào một queue để chia tải, thì một tin chỉ rơi vào tay đúng một trong số đó, không bị xử lý lặp. Các công nghệ thường gặp: RabbitMQ, AWS SQS, ActiveMQ.

Lợi ích lớn nhất: Producer không cần Consumer online. Nếu Consumer đang bảo trì, tin nhắn nằm yên trong queue chờ đó. Khi Consumer sống lại, nó xử lý nốt phần tồn đọng. Queue đóng vai trò "bộ giảm xóc" (buffer) — Producer có thể bắn 10.000 tin trong 1 giây, còn Consumer cứ thong thả xử lý 500 tin/giây mà không ai sập.

Pub/Sub — mô hình publish/subscribe

Ngược với point-to-point, mô hình Pub/Sub cho phép một tin nhắn đến nhiều bên nhận cùng lúc. Producer (gọi là Publisher) đẩy tin vào một Topic, và mọi Subscriber đã đăng ký topic đó đều nhận được bản sao của tin.

                    ┌→ Subscriber A (gửi email)
Publisher → [Topic] ┼→ Subscriber B (cập nhật điểm thưởng)
                    └→ Subscriber C (ghi log phân tích)

Ví dụ: khi có sự kiện "đơn hàng đã thanh toán", hệ thống email cần biết để gửi hóa đơn, hệ thống loyalty cần biết để cộng điểm, hệ thống kho cần biết để trừ tồn. Với Pub/Sub, Publisher chỉ phát một lần, ba hệ thống cùng nhận. Công nghệ: AWS SNS, Google Pub/Sub, Kafka (sẽ học sâu ở Bài 21). Với BA, điều quan trọng cần nắm: Pub/Sub giúp thêm consumer mới mà không động vào producer — muốn thêm hệ thống chống gian lận lắng nghe sự kiện thanh toán? Chỉ cần subscribe thêm, producer không đổi một dòng code.

Batch — xử lý theo lô

Batch processing là gom một khối lượng lớn bản ghi và xử lý cùng lúc theo định kỳ, thay vì xử lý từng cái ngay khi nó đến. Đặc trưng: chạy theo lịch (cuối ngày, mỗi đêm 2h sáng, cuối tháng), khối lượng lớn, không yêu cầu kết quả tức thời.

Ví dụ kinh điển ở Việt Nam: đối soát (reconciliation) cuối ngày giữa ngân hàng và ví điện tử. Cả ngày, từng giao dịch chạy real-time; nhưng đến nửa đêm, hệ thống chạy một job batch gom toàn bộ giao dịch trong ngày, so khớp với file đối tác gửi sang, phát hiện lệch và sinh báo cáo. Tương tự: tính lãi tiết kiệm cuối ngày, sinh sao kê cuối tháng, chấm công tính lương.

Ba trục để phân biệt — async (message queue/pubsub) thường xử lý gần real-time, từng tin một, theo sự kiện; batch xử lý theo lịch, khối lớn, gom cùng lúc. Một hệ thống thật thường dùng cả ba.

Tình huống thực tế

Ví dụ 1: MoMo và bài toán gửi thông báo khi tải đỉnh

Bối cảnh: Một ví điện tử lớn tại Việt Nam (tạm gọi theo mô hình MoMo) chạy chiến dịch lì xì Tết. Vào đúng giao thừa, hàng triệu người cùng mở ứng dụng nhận lì xì. Mỗi giao dịch nhận lì xì cần: cộng tiền vào ví, gửi push notification, gửi SMS, ghi log, cập nhật bảng xếp hạng "lì xì nhiều nhất".

Nếu làm sync: server phải chờ cổng SMS phản hồi mới trả kết quả cho người dùng. Mà cổng SMS của nhà mạng lúc giao thừa thì nghẽn cứng, mỗi tin mất 3-5 giây. Người dùng bấm "Nhận lì xì" và màn hình quay vòng vòng 5 giây — trải nghiệm thảm họa, và server tích tụ hàng trăm nghìn request đang chờ rồi sập.

Giải pháp async: việc cộng tiền vào ví làm sync (vì người dùng cần thấy số dư tăng ngay), nhưng SMS, push, ghi log, cập nhật bảng xếp hạng được đẩy vào message queue. Người dùng thấy "Nhận lì xì thành công" trong 200ms. Các consumer ở phía sau thong thả gửi SMS — có thể chậm 30 giây cũng không sao, vì đó không phải thứ người dùng đứng chờ.

Bài học rút ra: Là BA, hãy tách biệt việc gì người dùng cần thấy ngay (sync)việc gì có thể xử lý sau lưng (async). Đây là quyết định thiết kế quan trọng nhất, và nó thuộc về nghiệp vụ chứ không chỉ kỹ thuật.

Ví dụ 2: Tiki và Pub/Sub cho sự kiện "đặt hàng thành công"

Bối cảnh: Một sàn thương mại điện tử (mô hình Tiki) ban đầu thiết kế: khi đơn hàng được tạo, code gọi tuần tự dịch vụ kho, dịch vụ thanh toán, dịch vụ email, dịch vụ điểm thưởng — tất cả sync, nối đuôi nhau. Mỗi lần marketing muốn thêm một hành động mới (ví dụ: gửi sự kiện sang hệ thống phân tích để đo lường), dev phải sửa code lõi của luồng đặt hàng, test lại toàn bộ, rủi ro cao.

Giải pháp: chuyển sang Pub/Sub. Luồng đặt hàng chỉ publish một sự kiện OrderPlaced lên topic. Các đội tự subscribe theo nhu cầu: đội kho subscribe để trừ tồn, đội CRM subscribe để cộng điểm, đội data subscribe để đổ vào kho dữ liệu. Khi đội marketing muốn thêm consumer mới cho chiến dịch, họ chỉ subscribe thêm — không ai phải đụng vào code đặt hàng.

Kết quả con số: thời gian đưa một tính năng "lắng nghe đơn hàng" mới từ ý tưởng đến production giảm từ trung bình 2 tuần (phải sửa luồng lõi, qua nhiều vòng review) xuống còn 2-3 ngày. Bài học: Pub/Sub không chỉ là kỹ thuật, nó là đòn bẩy tổ chức — cho phép nhiều đội làm việc độc lập trên cùng một luồng nghiệp vụ.

Ví dụ 3: Ngân hàng và job batch đối soát cuối ngày

Bối cảnh: Một ngân hàng tầm trung tại Việt Nam tích hợp với một ví điện tử để cho phép nạp/rút tiền. Suốt cả ngày, mỗi giao dịch nạp/rút chạy real-time qua API đồng bộ. Nhưng tiền thật chỉ "chốt sổ" thông qua một file đối soát mà hai bên trao đổi vào cuối ngày.

Quy trình batch: 23h59, hệ thống ví xuất một file chứa toàn bộ giao dịch trong ngày (giả sử 850.000 dòng) đẩy lên SFTP. Lúc 1h sáng, job batch của ngân hàng tải file về, đối chiếu từng dòng với sổ cái nội bộ, tìm ra các giao dịch lệch (ví có ghi nhưng ngân hàng không, hoặc ngược lại), sinh báo cáo chênh lệch gửi cho đội vận hành xử lý tay vào sáng hôm sau.

Một sự cố thực tế: một đêm, file đối soát đến trễ vì mạng SFTP gặp lỗi, job batch chạy lúc 1h sáng nhưng file chưa có, nên nó... chạy trên file của ngày hôm trước (job không kiểm tra ngày của file). Kết quả là báo cáo lệch hàng trăm giao dịch, đội vận hành hoảng loạn cả buổi sáng trước khi phát hiện file sai. Bài học cho BA: khi viết spec batch, bạn PHẢI đặc tả rõ điều kiện đầu vào (file của đúng ngày nào, định dạng ra sao), xử lý khi đầu vào thiếu/trễ (chờ bao lâu, báo động cho ai), và cơ chế chạy lại khi job thất bại. Đây là những thứ dev hay quên nếu BA không nhắc.

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

Khi bạn được giao thiết kế một luồng tích hợp, đây là cách tiếp cận:

Bước 1 — Phân loại tính cấp thiết của từng tác vụ. Liệt kê mọi việc cần làm trong luồng. Với mỗi việc, hỏi: "Người dùng/người gọi có cần kết quả của việc này NGAY không?" Nếu có → sync. Nếu không → ứng viên cho async.

Bước 2 — Chọn mô hình async phù hợp. Nếu mỗi tin chỉ cần đúng một bên xử lý (ví dụ: gửi một email) → message queue point-to-point. Nếu một sự kiện cần nhiều bên cùng phản ứng (ví dụ: "đơn đã thanh toán") → Pub/Sub. Nếu việc không cần real-time và xử lý hàng loạt theo lịch → batch.

Bước 3 — Đặc tả message/event. Với async, hãy mô tả rõ cấu trúc tin nhắn: tên sự kiện, các trường dữ liệu, kiểu dữ liệu, trường nào bắt buộc. Đặc biệt nên có một trường ID duy nhất cho mỗi message để hỗ trợ chống trùng (sẽ học sâu ở Bài 29 — Idempotency).

Bước 4 — Đặc tả hành vi khi lỗi. Điều gì xảy ra nếu consumer xử lý thất bại? Tin nhắn được thử lại (retry) bao nhiêu lần? Sau khi hết lần thử thì đi đâu — thường là Dead Letter Queue (DLQ), một hàng đợi riêng chứa các tin "chết" để con người xem xét. BA cần định nghĩa rõ điều này.

Bước 5 — Với batch, đặc tả lịch và ranh giới dữ liệu. Job chạy lúc nào? Xử lý dữ liệu của khoảng thời gian nào (ví dụ: từ 00:00 đến 23:59 hôm trước)? Điều gì xảy ra nếu job chạy quá giờ và đè lên lần chạy kế tiếp? Có chạy lại được không nếu thất bại giữa chừng?

Bước 6 — Vẽ sequence diagram cho luồng async. Mô tả rõ producer đẩy tin lúc nào, consumer nhận lúc nào, và kết quả quay lại người dùng qua kênh nào (vì async, kết quả không quay lại theo đường gọi ban đầu). Điều này giúp dev và tester hiểu đúng.

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

Lỗi 1 — Tưởng async là "gửi xong là xong". Async không có nghĩa là không cần biết kết quả. Bạn vẫn phải thiết kế cách báo cho người dùng biết việc đã hoàn tất (qua webhook, push, hoặc polling). Spec thiếu phần này khiến người dùng "treo" trong trạng thái không biết đơn của mình tới đâu.

Lỗi 2 — Quên chống xử lý trùng. Trong hệ thống async, một tin nhắn có thể được giao nhiều hơn một lần (at-least-once delivery — đây là hành vi mặc định của hầu hết message queue). Nếu consumer "cộng tiền" mà xử lý tin hai lần, khách hàng được cộng tiền hai lần. Mẹo: luôn yêu cầu mỗi message có ID duy nhất và consumer kiểm tra "đã xử lý ID này chưa".

Lỗi 3 — Giả định tin nhắn đến đúng thứ tự. Nhiều queue không đảm bảo thứ tự. Nếu "cập nhật địa chỉ" đến trước "tạo khách hàng", consumer sẽ lỗi. Hãy hỏi rõ team kỹ thuật queue có đảm bảo ordering không, và đặc tả cách xử lý khi sai thứ tự.

Lỗi 4 — Batch không xử lý dữ liệu thiếu/trễ. Như ví dụ ngân hàng ở trên — luôn đặc tả điều kiện file đầu vào và hành vi khi thiếu.

Mẹo lớn: Đừng dùng async cho mọi thứ chỉ vì nó "ngầu". Async thêm độ phức tạp — khó debug, khó theo dõi, khó test. Chỉ dùng khi có lý do nghiệp vụ rõ ràng: tải lớn, cần tách rời, hoặc tác vụ chậm không nên chặn người dùng.

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

Tình huống: Bạn là Technical BA cho một ứng dụng giao đồ ăn (mô hình ShopeeFood/GrabFood). Khi khách bấm "Đặt món", hệ thống cần: (1) trừ tiền/xác nhận thanh toán, (2) gửi đơn tới nhà hàng, (3) gửi push cho tài xế gần đó, (4) gửi SMS xác nhận cho khách, (5) ghi nhận giao dịch vào hệ thống phân tích doanh thu, (6) cuối ngày tổng hợp doanh thu từng nhà hàng để đối soát.

Yêu cầu:

  • Phân loại mỗi tác vụ (1)–(6) là sync, async (queue/pubsub), hay batch. Giải thích lý do cho từng cái.
  • Với tác vụ "ghi nhận vào hệ thống phân tích", hãy giải thích vì sao Pub/Sub có thể phù hợp hơn point-to-point.
  • Viết đặc tả ngắn cho job batch ở mục (6): chạy lúc nào, xử lý dữ liệu khoảng nào, điều gì xảy ra nếu thất bại.
  • Nêu một rủi ro "xử lý trùng" có thể xảy ra trong luồng này và cách bạn đề xuất phòng tránh trong spec.
Hãy viết câu trả lời như một mục trong tài liệu thiết kế tích hợp thật. So sánh với cách bạn từng nghĩ trước khi học bài này.

Tóm tắt

Tích hợp bất đồng bộ và batch là công cụ giúp hệ thống chịu tải, sống sót khi một thành phần chậm, và xử lý khối lượng lớn. Message queue (point-to-point) đưa tin từ một producer tới đúng một consumer, tách rời theo thời gian — dùng RabbitMQ, AWS SQS. Pub/Sub phát một sự kiện tới nhiều subscriber cùng lúc, cho phép thêm consumer mới mà không động vào producer — đòn bẩy để các đội làm việc độc lập. Batch gom khối lớn xử lý theo lịch, điển hình là đối soát và báo cáo cuối ngày/cuối tháng.

Với vai trò Technical BA, ba quyết định cốt lõi bạn phải làm chủ là: tách biệt việc gì cần sync và việc gì có thể async; chọn đúng mô hình (queue vs pubsub vs batch); và đặc tả đầy đủ các tình huống lỗi — xử lý trùng, sai thứ tự, retry, dead letter queue, và dữ liệu đầu vào thiếu/trễ. Những thứ này nghe có vẻ kỹ thuật, nhưng hậu quả của chúng — tiền sai, báo cáo lệch, khách phàn nàn — luôn là vấn đề nghiệp vụ. Một spec async tốt không phải là spec mô tả "happy path", mà là spec lường trước những gì xảy ra khi mọi thứ không như ý.