Database Transactions trong Laravel

LAMP Jan 25, 2021

Problem:  Trong một giao dịch chuyển 1 tỷ đồng trong ngân hàng từ tài khoản A sang tài khoản B. Sẽ có ít nhất 2 hoạt động sẽ diễn ra đó là trừ 1 tỷ đồng trong tài khoản A và cộng 1 tỷ đồng trong tài khoản B. Vấn đề xảy ra, giả sử nếu hoạt động trừ 1 tỷ đồng tại tài khoản A thành công nhưng khi hoạt động cộng 1 tỷ đồng vào tài B gặp lỗi không thể thực hiện. Khi đó số tiền 1 tỷ sẽ mất.

Solution: Phương án giải quyết, phải đảm bảo rằng 2 hoạt động cùng được thực hiện thành công, nếu có bất kỳ hoạt động nào trong chuỗi hoạt động gặp lỗi thì tất cả các hoạt động đều được hủy, nếu các hoạt động phía trước đã thành công thì sẽ được rollback. Trong Laravel, phương thức transaction của Facade DB sẽ thực hiện công việc đó.

Vậy transaction là gì ?
Transaction là một tiến trình xử lý có xác định điểm đầu và điểm cuối, được chia nhỏ thành các operation, tiến trình được thực thi một cách tuần tự và độc lập các operation đó theo nguyên tắc hoặc tất cả đều thành công hoặc một operation thất bại thì toàn bộ tiến trình thất bại. Nếu việc thực thi một operation nào đó bị fail đồng nghĩa với việc dữ liệu phải rollback trạng thái ban đầu.

Sử dụng transaction trong Laravel
Bạn có thể sử dụng phương thức transaction của facade DB để thực thi một tập các lệnh trong một database transaction. Nếu một ngoại lệ được ném ra trong transaction Closure, transaction sẽ tự động rollback. Nếu Closure thực thi thành công, transaction sẽ tự động commit. Bạn không cần phải lo lắng về việc thực hiện thủ công thao tác rollback hoặc commit khi sử dụng phương thức transaction:

use Illuminate\Support\Facades\DB;

DB::transaction(function()
{
    $newAcct = Account::create([
        'accountname' => Input::get('accountname')
    ]);

    $newUser = User::create([
        'username' => Input::get('username'),
        'account_id' => $newAcct->id,
    ]);
});

Vậy làm thế nào để biết được code được rollback hoặc commit trong transaction?
Bạn có thể đi sâu vào DB::transaction() trong Illuminate\Database\Connection

 public function transaction(Closure $callback)
    {
            $this->beginTransaction();

            // We'll simply execute the given callback within a try / catch block
            // and if we catch any exception we can rollback the transaction
            // so that none of the changes are persisted to the database.
            try
            {
                    $result = $callback($this);

                    $this->commit();
            }

            // If we catch an exception, we will roll back so nothing gets messed
            // up in the database. Then we'll re-throw the exception so it can
            // be handled how the developer sees fit for their applications.
            catch (\Exception $e)
            {
                    $this->rollBack();

                    throw $e;
            }

            return $result;
    }

Rất đơn giản, nếu một ngoại lệ của bất kỳ loại nào được đưa ra trong quá trình đóng, thì giao dịch sẽ được khôi phục lại. Điều này có nghĩa là nếu có một lỗi SQL , thì giao dịch sẽ được rollback. Tuy nhiên, điều này có nghĩa là chúng ta có thể ném các ngoại lệ của riêng mình để khôi phục một giao dịch. Ví dụ như sau:

DB::transaction(function()
{
    $newAcct = Account::create([
        'accountname' => Input::get('accountname')
    ]);

    $newUser = User::create([
        'username' => Input::get('username'),
        'account_id' => $newAcct->id,
    ]);

    if( !$newUser )
    {
        throw new \Exception('User not created for account');
    }
});

Xử lý Deadlocks
Phương thức transaction  chấp nhận đối số thứ hai tùy chọn xác định số lần giao dịch phải được thử lại khi xảy ra deadlock. Khi những nỗ lực này đã hết, một ngoại lệ sẽ được ném ra

DB::transaction(function()
{
    $newAcct = Account::create([
        'accountname' => Input::get('accountname')
    ]);

    $newUser = User::create([
        'username' => Input::get('username'),
        'account_id' => $newAcct->id,
    ]);
}, 5);

Tự xử lý transaction
Nếu bạn muốn tự thực hiện một transaction và quản lý việc rollBack hay commit, bạn có thể sử dụng phương thức beginTransaction() của facade DB:

DB::beginTransaction();

Sử dụng phương thức rollBack() để huỷ một transaction:

DB::rollBack();

Cuối cùng, bạn sử dụng phương thức commit() để hoàn tất một transaction:

DB::commit();

Tóm lại, trong trường hợp bạn cần 'exit' một transaction thông qua code theo cách thủ công (có thể là thông qua một ngoại lệ hoặc chỉ đơn giản là kiểm tra trạng thái lỗi), bạn không nên sử dụng  DB::transaction() mà thay vào đó hãy sử dụng  DB::beginTransaction()  và DB::commit() /  DB::rollBack()

DB::beginTransaction();

try {
    DB::insert(...);
    DB::insert(...);
    DB::insert(...);

    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
}

Rủi ro khi thực thi transaction
Có ba loại rủi ro chính khiến việc thực thi một transaction có thể bị fail.
1. Việc thực thi operation bị hỏng: rõ ràng việc này sẽ dẫn tới transaction bị hỏng. Điều này đã được quy định rõ trong định nghĩa về transaction.
2. Vấn đề về phần cứng và mạng: việc phần cứng hoặc mạng có vấn đề trong lúc đang thực thi transaction sẽ dẫn đến tiến trình xử lý thất bại.
3. Các vấn đề với dữ liệu dùng chung: Đây là vấn đề khó nhất.
Dữ liệu là một tài nguyên dùng chung, nếu như có nhiều tiến trình xử lý đồng thời thực hiện các phép trên dữ liệu sẽ xảy ra những rủi ro: write-write, write-read,… việc dữ liệu ghi cùng lúc dẫn tới hỏng dữ liệu hoặc dữ liệu đọc ra không đồng nhất với dữ liệu mới ghi vào

Tổng kết
- Việc sử dụng transaction cần phải hiểu ý nghĩa và không nên lạm dụng.
- Khi thực hiện transaction cần phải rất thận trọng và phải có đánh giá tổng thể xem việc thực thi đó có bị xung đột nhau hay không.
=> Nên nhớ DEADLOCK luôn rình rập xung quanh.

Tài liệu tham khảo
1. https://laravel.com/docs/8.x/database#handling-deadlocks
2. https://fideloper.com/laravel-database-transactions

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.