Series Cấu trúc Modules trong Laravel | Bài 4: Nâng tầm kiến trúc - Tích hợp Message Broker (Kafka/RabbitMQ) và bài toán Dữ liệu mồ côi
Chào anh em, tiếp nối Bài 3, chúng ta đã biết cách tách rời Module Order và Module Payment bằng Event-Driven. Khi khách mua vé thành công, một Event nội bộ được bắn ra để các module khác (như AFC - Automatic Fare Collection) lắng nghe và đồng bộ dữ liệu xuống trạm.
Nhưng hãy tưởng tượng kịch bản này: Khách hàng quẹt thẻ, Module Payment trừ tiền thành công và bắn Event. Ngay lúc đó, đường truyền mạng xuống thiết bị bán vé tự động (TVM) tại trạm bị đứt. Thiết bị không nhận được lệnh tạo vé. Khách mất tiền nhưng không qua được cổng. Lúc này, trên hệ thống sinh ra một "Giao dịch mồ côi" (Orphan Transaction) - dữ liệu giữa server trung tâm và thiết bị ngoại vi bị lệch pha nghiêm trọng.
Để giải quyết triệt để những ca "khó đẻ" này, đồng thời chuẩn bị cho việc tách hệ thống thành các Microservices độc lập (ví dụ backend AFC viết bằng Golang, web bán hàng viết bằng PHP), chúng ta phải đưa Message Broker (RabbitMQ hoặc Kafka) vào giữa các Modules.
1. Tại sao Event nội bộ của Laravel là chưa đủ?
- Mất mát dữ liệu: Event mặc định của Laravel chạy trên RAM (Sync). Quá trình xử lý lỗi thì mất luôn data. Dù dùng Queue (như Database hay Redis) của Laravel, nó vẫn thiếu các cơ chế chịu lỗi (fault-tolerance) nâng cao.
- Rào cản ngôn ngữ: Event của Laravel chỉ có mã PHP mới hiểu. Nếu bạn có một module thiết bị trạm viết bằng C++ hay hệ thống AI phân tích hành vi viết bằng Python, chúng không thể "lắng nghe" Event của Laravel được.
2. RabbitMQ / Kafka: Người vận chuyển không bao giờ ngủ
Thay vì Module Order gọi trực tiếp Module AFC hoặc bắn Event nội bộ, nó sẽ ném gói tin (Message) vào một hệ thống trung gian gọi là Message Broker (RabbitMQ hoặc Kafka).
- Module Order đóng vai trò là Publisher (Người gửi).
- Message Broker giữ gói tin trong Queue / Topic cực kỳ an toàn. Nó cam kết không bao giờ làm mất tin nhắn kể cả khi server sập.
- Module AFC (hoặc bất kỳ service ngoại vi nào) đóng vai trò là Consumer (Người nhận), liên tục lấy tin nhắn về xử lý. Nếu mạng chập chờn hoặc thiết bị offline, tin nhắn vẫn nằm chờ trong Broker cho đến khi thiết bị online trở lại để đồng bộ.
3. Thực hành: Thay máu Laravel Event bằng RabbitMQ
Để tích hợp RabbitMQ vào Laravel Modules, cách nhanh nhất là sử dụng package vladimir-yuldashev/laravel-queue-rabbitmq. Nó cho phép chúng ta dùng hàm dispatch() quen thuộc của Laravel nhưng data sẽ bay thẳng vào RabbitMQ.
Bước 1: Khai báo Job đẩy dữ liệu lên Broker Trong Module Order, thay vì dùng Event nội bộ, ta tạo một Queued Job.
// Modules/Order/Jobs/PublishTicketToBroker.php
namespace Modules\Order\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class PublishTicketToBroker implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $ticketData;
public function __construct($ticketData)
{
$this->ticketData = $ticketData;
}
public function handle()
{
// Khi Job này chạy, nội dung của nó thực chất đã nằm an toàn trên RabbitMQ.
// Bạn không cần viết code kết nối RabbitMQ ở đây, Laravel Queue Driver sẽ lo.
}
}
Bước 2: Cấu hình Queue Driver
Sửa file .env:
QUEUE_CONNECTION=rabbitmq
RABBITMQ_HOST=127.0.0.1
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_QUEUE=afc_sync_queue
Bước 3: Dispatch Job khi có giao dịch mới
// Modules/Order/Http/Controllers/OrderController.php
public function store(Request $request)
{
DB::beginTransaction();
try {
$ticket = Order::create($request->all());
// Trừ tiền...
// Đẩy thẳng vào RabbitMQ để các hệ thống khác (C++, Golang...) tự lấy về xử lý
\Modules\Order\Jobs\PublishTicketToBroker::dispatch($ticket)->onQueue('afc_sync_queue');
DB::commit();
return response()->json(['msg' => 'Giao dịch thành công, chờ đồng bộ trạm']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => 'Lỗi giao dịch'], 500);
}
}
4. Xử lý triệt để Dữ liệu mồ côi (Orphan Transactions) với Outbox Pattern
Đoạn code trên vẫn có một lỗ hổng chí mạng: Chuyện gì xảy ra nếu DB::commit() thành công nhưng ngay lúc gọi dispatch() thì RabbitMQ bị sập? Tiền đã trừ, nhưng lệnh không được gửi đi. Giao dịch mồ côi lại xuất hiện!
Để giải quyết, các kỹ sư hệ thống dùng Transactional Outbox Pattern:
- Cùng trong một Transaction của Database, ta lưu giao dịch vào bảng orders VÀ lưu nội dung cần gửi vào một bảng outbox_messages.
- Do lưu cùng lúc vào chung 1 database relational (MySQL/PostgreSQL), tính toàn vẹn (ACID) được đảm bảo: 1 là ăn cả, 2 là rollback tất cả.
- Có một worker (hoặc Cronjob/Debezium) chạy ngầm độc lập, liên tục quét bảng outbox_messages và đẩy chúng lên Kafka/RabbitMQ một cách an toàn.
Bằng cách này, ứng dụng PHP của bạn không bao giờ phải chờ đợi mạng hay lo lắng về việc Broker sập giữa chừng. Dữ liệu luôn nhất quán 100%.
Tổng kết Series
Qua 4 bài viết, chúng ta đã biến một project Laravel nguyên khối, lộn xộn thành một cỗ máy Modular tinh xảo, giao tiếp lỏng lẻo và có sức chịu đựng cao trước những biến cố hệ thống.
Từ đây, nếu muốn bóc tách Module AFC ra thành một Microservice riêng, bạn chỉ việc đem bộ code đó qua server mới. Luồng giao tiếp vẫn không suy suyển vì mọi thứ đã chạy qua cầu nối trung gian là Message Broker.
Hi vọng series này cung cấp đủ góc nhìn từ mức Code (Framework) cho đến mức System Design để anh em tự tin chiến đấu với các dự án siêu to khổng lồ. Hẹn gặp lại anh em ở những bài viết kỹ thuật tiếp theo!
All Rights Reserved