Transaction trong Laravel — 1 Commit hay Nhiều Commit?

5 phút đọc

Bạn đã bao giờ tự hỏi: "Trong một business flow phức tạp, mình nên gói tất cả vào 1 transaction hay tách ra nhiều transaction?" — Nếu chọn sai, bạn có thể lock database quá lâu, hoặc tệ hơn là mất dữ liệu khi flow fail giữa chừng.

Bài viết này sẽ giúp bạn hiểu rõ 3 pattern transaction trong Laravel/MySQL, kèm code thực tế để bạn áp dụng ngay.


1. Một Transaction = Một COMMIT (Atomicity)

Đây là pattern cơ bản nhất — tất cả các bước phải thành công cùng nhau, hoặc rollback toàn bộ.

Khi nào dùng: Các bước có quan hệ chặt chẽ, không thể tồn tại độc lập. Ví dụ: chuyển tiền giữa 2 tài khoản.

DB::transaction(function () {
    // Bước 1: Trừ tiền người gửi
    Account::where('id', 1)->decrement('balance', 500);

    // Bước 2: Cộng tiền người nhận
    Account::where('id', 2)->increment('balance', 500);

    // Bước 3: Ghi log giao dịch
    TransferLog::create([...]);

    // → COMMIT 1 lần duy nhất cho cả 3 bước
    // → Nếu bất kỳ bước nào fail → ROLLBACK tất cả
});

2. Nhiều Transaction trong Một Business Flow

Khi flow có các bước độc lập về logic hoặc cần gọi external service (API thanh toán, gửi email...), bạn nên tách thành nhiều transaction.

Khi nào dùng: Flow có bước gọi external API, hoặc các bước có thể commit độc lập mà không phá vỡ data integrity.

Ví dụ thực tế: Flow đặt phòng → Thanh toán Stripe → Xác nhận

// === Transaction 1: Tạo booking ở trạng thái pending ===
$booking = DB::transaction(function () use ($roomId, $userId) {
    $room = Room::where('id', $roomId)->lockForUpdate()->first();
    if (!$room->isAvailable()) {
        throw new Exception('Room not available');
    }

    $room->update(['status' => 'reserved']);
    return Booking::create([
        'room_id' => $roomId,
        'user_id' => $userId,
        'status'  => 'pending',
    ]);
});
// → COMMIT #1 ✅ — Booking đã tồn tại trong DB

// === Gọi Stripe API (KHÔNG nằm trong transaction) ===
try {
    $charge = Stripe::charge($booking->total);
} catch (PaymentFailedException $e) {
    // Transaction 2: Cleanup nếu payment fail
    DB::transaction(function () use ($booking) {
        $booking->update(['status' => 'payment_failed']);
        Room::where('id', $booking->room_id)
            ->update(['status' => 'available']);
    });
    // → COMMIT #2 ✅
    throw $e;
}

// === Transaction 3: Confirm booking ===
DB::transaction(function () use ($booking, $charge) {
    $booking->update([
        'status'    => 'confirmed',
        'charge_id' => $charge->id,
    ]);
    Payment::create([...]);
});
// → COMMIT #3 ✅

3. Savepoint — Rollback Một Phần trong Transaction

MySQL hỗ trợ SAVEPOINT — cho phép bạn rollback một phần transaction mà không rollback toàn bộ. Hữu ích khi có bước phụ "nice to have" có thể fail mà không ảnh hưởng flow chính.

Khi nào dùng: Có bước optional (thêm quà tặng, gửi notification nội bộ...) mà nếu fail thì flow vẫn nên tiếp tục.

DB::transaction(function () {
    // Bước 1: Tạo order (bắt buộc)
    $order = Order::create([...]);

    // Đặt savepoint trước bước optional
    DB::statement('SAVEPOINT before_gift');

    try {
        // Bước 2: Thêm quà tặng (optional — có thể hết quà)
        GiftItem::create(['order_id' => $order->id, ...]);
    } catch (\Exception $e) {
        // Rollback CHỈ bước 2, giữ nguyên bước 1
        DB::statement('ROLLBACK TO SAVEPOINT before_gift');
        // Log: "Hết quà tặng, order vẫn được tạo"
    }

    // Bước 3: Ghi payment (bắt buộc)
    Payment::create(['order_id' => $order->id, ...]);

    // → COMMIT toàn bộ (bước 1 + bước 3, có thể có hoặc không bước 2)
});

Tổng kết — Chọn Pattern nào?

Tình huống Pattern Ví dụ
Các bước phải đúng cùng nhau 1 transaction, 1 commit Chuyển tiền A → B
Các bước có thể tách rời hoặc có external API Nhiều transaction, nhiều commit Booking → Stripe → Confirm
Bước phụ có thể fail mà không ảnh hưởng flow chính Savepoint Order + optional gift
gọi external API/service Tách transaction, KHÔNG bọc API trong transaction Payment gateway, email service

Bài viết liên quan

Đang cập nhật...