Table of Contents
1. Một số package hay dùng
1.1. Horizon
1.2. Passport
1.3. GraphQL
2. Race Conditions
Vấn đề này xảy ra khi có 2 hành động xảy ra đồng thời để cùng update vào 1 record table, nó sẽ làm sai lệch dữ liệu, gây ra các lỗi vô cùng nguy hiểm.
Ví dụ chúng ta thực hiện 1 hành động kiểm tra số dư, nếu số dư đủ chúng ta sẽ gửi cho khách hàng 1 mã thẻ điện thoại chưa được sử dụng, chúng ta có đoạn code đơn giản như sau:
DB::beginTransaction(); $account = Account::query()->where('id', $accountId)->first(); if(!$account instanceof Account){ response()->json([ 'error'=>'Account not found.' ]); } $order = Order::query()->where('id', $orderId)->first(); if(!$account instanceof Order){ response()->json([ 'error'=>'Order not found.' ]); } if($account->balance <= $order->amount){ response()->json([ 'error'=>'Balance is not enough.' ]); } $card = Card::query()->where('is_use', 0)->first(); if(!$card instanceof Card){ response()->json([ 'error'=>'Card is not enough.' ]); } $account->decrement('balance', $order->amount); $account->save(); $order->update([ 'status'=>'SUCCESS' ]); $card->update([ 'is_use'=>1 ]); DB::commit(); response()->json([ 'data'=>$card->toArray() ]);
Nhìn qua đoạn code trên không có vấn đề gì vì chúng ta đã kiểm tra các điều kiện đầy đủ. Tuy nhiên vì một lý do nào đó, tại cùng thời điểm có nhiều request xảy ra đồng thời thì sẽ phát sinh lỗi nghiêm trọng như sau: tại cùng một thời điểm các request đều có số dư hợp lệ là A, và khi đó chỉ với số dư A user có thể nhận được nhiều thẻ cào khác nhau.
Để xử lý trường hợp này, chúng ta có thể dùng cơ chế hàng đợi (queue) để đưa các yêu cầu trên vào xử lý lần lượt. Tuy nhiên nó gây ra tình trạng bất đồng bộ khi xử lý.
Và để xử lý tình huống này, chúng ta phải lock lại các bản ghi đã kiểm tra từ database, khi nào xử lý xong thì chúng ta mới open lock để xử lý tiếp. Và đây là đoạn code mẫu:
DB::beginTransaction(); $account = Account::query()->lockForUpdate()->where('id', $accountId)->first(); if(!$account instanceof Account){ response()->json([ 'error'=>'Account not found.' ]); } $order = Order::query()->where('id', $orderId)->first(); if(!$account instanceof Order){ response()->json([ 'error'=>'Order not found.' ]); } if($account->balance <= $order->amount){ response()->json([ 'error'=>'Balance is not enough.' ]); } $card = Card::query()->lockForUpdate()->where('is_use', 0)->first(); if(!$card instanceof Card){ response()->json([ 'error'=>'Card is not enough.' ]); } $account->decrement('balance', $order->amount); $account->save(); $order->update([ 'status'=>'SUCCESS' ]); $card->update([ 'is_use'=>1 ]); DB::commit();
3. Atomic Locks
Trong Laravel, Atomic Locks được sử dụng để đảm bảo rằng một đoạn mã nhất định chỉ được thực thi bởi một tiến trình duy nhất tại một thời điểm. Điều này giúp tránh các vấn đề về race conditions khi nhiều tiến trình có thể truy cập và thay đổi dữ liệu cùng một lúc.
Laravel cung cấp các cơ chế để sử dụng Atomic Locks thông qua các drivers khác nhau như Redis, Memcached, và Database.
Dưới đây là ví dụ về cách sử dụng Atomic Locks trong Laravel với Redis:
Cấu hình Redis: Trước tiên, bạn cần đảm bảo rằng Redis đã được cấu hình trong ứng dụng Laravel của bạn. Bạn có thể cấu hình Redis trong file .env và config/database.php.
Sử dụng Atomic Locks: Bạn có thể sử dụng phương thức lock của facade Cache để tạo một lock. Phương thức này trả về một đối tượng Illuminate\Cache\Lock mà bạn có thể sử dụng để thực hiện các hành động trên lock.
use Illuminate\Support\Facades\Cache; $lock = Cache::lock('foo', 10); // 'foo' là tên của lock, 10 là thời gian tồn tại của lock tính bằng giây if ($lock->get()) { // Thực hiện công việc cần lock bảo vệ ở đây // Giải phóng lock khi hoàn thành $lock->release(); } else { // Không thể lấy được lock echo 'Could not acquire lock'; }
Sử dụng lock với callback: Bạn cũng có thể sử dụng lock với một callback để tự động giải phóng lock sau khi hoàn thành công việc.
use Illuminate\Support\Facades\Cache; Cache::lock('foo', 10)->get(function () { // Thực hiện công việc cần lock bảo vệ ở đây });
Kiểm tra lock tồn tại: Bạn có thể kiểm tra xem một lock có đang tồn tại hay không bằng phương thức exists.
if (Cache::lock('foo', 10)->exists()) { echo 'Lock is currently held'; } else { echo 'Lock is available'; }
Releasing lock: Ngoài việc giải phóng lock thông qua phương thức release, bạn cũng có thể sử dụng forceRelease để giải phóng lock ngay lập tức mà không quan tâm đến việc nó có được giữ bởi tiến trình hiện tại hay không.
if (Cache::lock('foo', 10)->exists()) { echo 'Lock is currently held'; } else { echo 'Lock is available'; }
Atomic Locks rất hữu ích trong các tình huống như job processing, data synchronization, hoặc bất kỳ trường hợp nào cần đảm bảo rằng một đoạn mã chỉ được thực thi một lần tại một thời điểm.
Tham khảo: https://laravelmagazine.com/exploring-atomic-locks-in-laravel-enhancing-application-concurrency
4. Eager Loading
https://laravel.com/docs/7.x/eloquent-relationships#eager-loading
Khi chúng ta sử dụng Eloquent relationship, dữ liệu relation cần dùng chưa được truy vấn ngay mà được truy vấn tại thời điểm chúng ta sử dụng lần đầu tiên. Cách thực hiện này được gọi là “lazy loaded”.
Tuy nhiên chúng ta có thể yêu cầu lấy thông tin ngay từ thời điểm tạo câu truy vấn cha, cách này làm giảm bớt N + 1 câu truy vấn, gọi là “Eager Loading”.
Chúng ta xem 1 ví dụ cụ thể sau:
$books = App\Book::all(); foreach ($books as $book) { echo $book->author->name; }
Vòng lặp này sẽ thực hiện 1 truy vấn để lấy tất cả các danh sách trên bảng, một truy vấn khác để lấy thông tin tác giả cho mỗi cuốn sách. Ví dụ chung ta có 100 cuốn sách, đoạn code trên sẽ chạy 26 câu truy vấn, trong đó: 1 câu truy vấn cho cuốn sách gốc và 100 câu truy vấn bổ xung để truy vấn thông tin tác giả của mỗi cuốn sách.
Tuy nhiên chúng ta có thể giảm số lượng câu truy vấn từ 101 xuống chỉ còn 2 câu truy vấn bằngg cách chỉ định rõ mối quan hệ nào sẽ được truy vấn ngay bằng phương thức “with”:
$books = App\Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name; }
Đối với thao tác này, chỉ có hai truy vấn sẽ được thực hiện:
SELECT * FROM books SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ...)
Kiểm tra xem relation đã được load chưa bằng đoạn code sau:
if ($this->relationLoaded('author')) { //... }
5. Log các câu truy vấn
5.1. Log ngay sau khi thực hiện xong lệnh truy vấn
DB::enableQueryLog(); $users = DB::table('users')->select('name', 'email as user_email')->get(); dd(DB::getQueryLog());
5.2. Log tất cả các lệnh truy vấn
- app/Providers/AppServiceProvider.php
public function boot() { if (env('APP_DEBUG_QUERY')) { DB::listen(function ($query) { $log = new Log('query'); $log->info($query->sql, [ 'bindings' => $query->bindings, 'time' => $query->time ]); }); } }