Bài 12: Kafka Transactions — Mảnh ghép cuối cùng cho Exactly-Once Semantics (EOS)
Ở phần trước, chúng ta đã thấy Idempotent Producer giải quyết rất tốt bài toán chống trùng lặp. Tuy nhiên, nó có một giới hạn lớn: nó chỉ đảm bảo Exactly-once trong phạm vi 1 Producer gửi tới 1 Partition của 1 Topic.
Trong thực tế, các ứng dụng (như Kafka Streams, microservices) thường hoạt động theo chu trình Consume-Transform-Produce:
- Đọc dữ liệu từ Topic A (Consume).
- Xử lý logic/Tính toán (Transform).
- Ghi kết quả sang Topic B (Produce).
- Xác nhận đã đọc xong ở Topic A bằng cách commit offset.
Bài toán: Điều gì xảy ra nếu service của bạn bị sập ngay sau bước 3, tức là đã ghi xong kết quả sang Topic B nhưng chưa kịp commit offset ở Topic A?
Khi service khởi động lại, nó sẽ đọc lại tin nhắn cũ từ Topic A và tiến hành ghi thêm một bản sao nữa sang Topic B. Dữ liệu bị nhân đôi! Idempotent Producer không thể cứu bạn ở đây vì khi restart, chu kỳ Produce là một session hoàn toàn mới.
Đây là lúc Kafka Transactions bước vào để hoàn thiện bức tranh.
1. Kafka Transactions là gì?
Được giới thiệu từ bản 0.11, Kafka Transactions API cho phép bạn gom nhóm nhiều thao tác — bao gồm việc ghi dữ liệu vào nhiều Topic/Partition khác nhau và cả việc commit offset — thành một khối Atomic (nguyên tử) duy nhất.
Triết lý của nó rất đơn giản: Hoặc tất cả các thao tác cùng thành công, hoặc không có gì thay đổi.
2. Các thành phần cốt lõi của hệ thống Giao dịch
Để quản lý luồng dữ liệu phức tạp này, Kafka sử dụng các cơ chế nội bộ sau:
transactional.id: Khác với Producer ID (bị reset mỗi khi ứng dụng khởi động lại),transactional.idlà một chuỗi định danh cố định do bạn tự cấu hình (ví dụ:order-processing-service-1). Nhờ ID này, Kafka nhận diện được Producer ngay cả khi nó vừa bị crash và restart, từ đó phục hồi hoặc hủy bỏ các giao dịch đang dang dở.- Transaction Coordinator: Một module chạy ngầm bên trong Broker, đóng vai trò như một "nhà cái" quản lý trạng thái của các giao dịch (
Ongoing,PrepareCommit,CompleteCommit...). - Transaction Log: Một topic nội bộ siêu nhẹ (
__transaction_state) dùng để Transaction Coordinator ghi lại nhật ký giao dịch, giúp hệ thống không bị quên trạng thái ngay cả khi Broker sập.
3. Vòng đời của một luồng Exactly-once
Để đảm bảo không có dữ liệu nào bị lọt hoặc lặp trong chu trình Consume-Transform-Produce, Kafka thực hiện các bước sau:
- Khởi tạo: Producer đăng ký
transactional.idvới Transaction Coordinator để lấy quyền điều hành. - Bắt đầu (Begin): Ứng dụng đánh dấu bắt đầu một transaction.
- Xử lý: Ứng dụng đọc dữ liệu từ Topic A và thực hiện tính toán.
- Ghi kết quả (Produce): Producer gửi kết quả sang Topic B. Các tin nhắn này được Broker lưu vào disk nhưng bị gắn cờ là "uncommitted" (chưa hoàn tất).
- Gửi Offset: Thay vì Consumer tự gọi lệnh commit offset như thông thường, Producer sẽ gửi thông tin offset của Topic A trực tiếp cho Transaction Coordinator. Điều này cực kỳ quan trọng vì nó gộp bước "đánh dấu đã đọc" vào chung một giao dịch với bước "ghi kết quả".
- Xác nhận (Commit/Abort):
- Thành công: Ứng dụng gọi lệnh
Commit. Coordinator ghi một markerCommitvào Topic B và chính thức cập nhật offset cho Topic A. - Lỗi (Crash/Exception): Ứng dụng (hoặc Coordinator khi bị timeout) gọi lệnh
Abort. Coordinator ghi markerAbort. Toàn bộ dữ liệu vừa ghi sẽ bị đánh dấu là đồ bỏ đi.
- Thành công: Ứng dụng gọi lệnh
4. Chốt chặn cuối cùng: Consumer Isolation Level
Dù Producer đã đóng gói dữ liệu vào Transaction rất cẩn thận, nhưng nếu Consumer ở đầu ra (đọc từ Topic B) cứ thấy có tin nhắn mới là lấy ra dùng, thì mọi nỗ lực đều vô nghĩa. Nó sẽ đọc nhầm cả những tin nhắn thuộc về các giao dịch đang dang dở hoặc đã bị hủy.
Để hoàn tất Exactly-once, Consumer đầu ra phải được cấu hình tham số:
isolation.level = read_committed(Mặc định của Kafka làread_uncommitted).
Với cấu hình này, Consumer sẽ tự động lọc và bỏ qua các tin nhắn thuộc về các giao dịch bị Abort, đồng thời chờ đợi các giao dịch đang mở. Nó chỉ trả về cho ứng dụng những tin nhắn đã có marker Commit.
🛠️ Tổng hợp cấu hình Exactly-once toàn tập
Nếu bạn đang xây dựng hệ thống tài chính hoặc ví điện tử, đây là combo cấu hình bắt buộc:
| Thành phần | Cấu hình | Tác dụng |
|---|---|---|
| Producer | enable.idempotence = true |
Bật cơ chế chống trùng lặp cơ bản. |
| Producer | transactional.id = "your-app-id" |
Kích hoạt Transactions, đảm bảo tính nguyên tử xuyên suốt quá trình restart. |
| Producer | acks = all |
Đảm bảo dữ liệu không bị mất ở mức phần cứng. |
| Consumer (Output) | isolation.level = read_committed |
Chỉ đọc các dữ liệu đã được xác nhận thành công trọn vẹn. |
All rights reserved
