Pessimistic Locking trong Laravel

LAMP Oct 19, 2020

Problem: Đã bao giờ bạn gặp bài toán có 1 sản phẩm với số lượng tồn kho là 1 mà lại có 2 customer order cùng một lúc chưa. Vậy thì ai sẽ là người order thành công hay không ai cả.

Solution: Để giải quyết bài toán trên nói riêng (hay quản lý việc truy cập dữ liệu dùng chung nói chung) mình đã sử dụng locking. Ở bài viết này mình sẽ giới thiệu về 1 cơ chế locking cơ bản là Pessimistic.

Vậy pessimistic lock là gì ?

Pessimistic dịch nghĩa là bi quan.
Cơ chế của pessimistic locking là sẽ khóa record ngay khi người dùng đầu tiên truy cập vào dữ liệu đó, tất cả những người dùng khác sẽ phải chờ cho đến khi tiến trình cập nhật dữ liệu của người dùng lock đầu tiên hoàn thành. Để dễ hiểu hơn thì nó đơn giản như 1 ổ khoá chỉ có duy nhất 1 chìa khoá. Chỉ có người duy nhất nắm giữ chìa khoá đó mới có thể truy cập vào nhà và sử dụng các dịch vụ trong nhà. Những người khác sẽ không thể truy cập trái phép vào nhà nếu không được sự cho phép của chủ nhà.

Pessimistic lock trong laravel thì sao ?

Query builder cũng bao gồm các hàm để giúp bạn thực hiện "pessimistic locking" trên cú pháp select. Để chạy cú pháp với một "shared lock", bạn có thể sử dụng phương thức sharedLock trên truy vấn. Một shared lock ngăn chặn các row được select bị thay đổi cho tới khi transaction được commit

DB::table('users')->where('votes', '>', 100)->sharedLock()->get();

Ngoài ra, bạn có thể sử dụng phương thức lockForUpdate. Một "for update" lock ngăn việc thay đổi hoặc bị selected bởi các shared lock khác.

DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();

Ví dụ

a. lockForUpdate

public function store(createOrderRequest $request)
{
  	$products = $request->get('products');
  	$productIds = collect($products)->pluck('id')->toArray();
    
    DB::beginTransaction();
    $listProduct = Product::whereIn('id', $productIds)->lockForUpdate()->get();
    try {
          	$foreach ($products as $productData) {
	          	$product = $listProduct->where('id', $productData->id)->first();
	          	if(empty($product)) {
	          		return //responseErrors
	          	}

	          	if($product->in_stock_quantity - $productData->quantity < 0) {
	          		return //responseErrors
	          	}
	          	// ....
	          }
          // ....
        DB::commit();
        return //responseOk
    } catch (\Exception $e) {
        DB::rollBack();
         return //responseErrors
    }
}

Gỉa sử 2 customer lần lượt là A và B. A nắm được chìa khóa trước thì B phải đợi cho tới khi A hoàn thành quá trình mua hàng (có thể không thành công). Và nếu như A mua hàng thành công thì giá trị hàng tồn kho cho mặt hàng đó sẽ được update bằng  0. Bây giờ B sẽ thấy rằng mặt hàng đã hết hàng và không thể làm gì với mặt hàng đó.

b. Shared Lock
- Shared lock là khi có nhiều hơn 1 transaction được cấp quyền đọc một bản ghi nhất định.
- Khi một transactions có shared lock của 1 record.
- Sau đó, 1 transaction khác có thể yêu cầu (và lấy) shared lock của record đó. Tất cả các transactions sử dụng record này thì chỉ có quyền đọc mà không thể thay đổi chúng (chỉnh sửa hoặc xóa) cho tới khi shared lock được giải phóng.
Ví dụ như việc bạn muốn xem danh sách sản phẩm trước thời điểm query

Product::select('id', 'name', 'in_stock_quantity')
            ->sharedLock()
            ->get();

Tổng kết

Pessimistic lock là khi bạn khóa record  để sử dụng độc quyền cho đến khi bạn hoàn thành nó. Tuy nhiên, người ta nói "cái gì mà chả có hai mặt của nó" và pessimistic lock cũng vậy. Khi sử dụng pessimistic lock, bạn nên cân nhắc đến việc xử lý timeout cho các long transaction để tránh deadlock.

Tài liệu tham khảo
1. https://laravel.com/docs/8.x/queries#pessimistic-locking
2. https://medium.com/@aslrousta/pessimistic-vs-optimistic-locking-in-laravel-264ec0b1ba2

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.