Locking & Deadlock là gì? Cơ chế khóa và bế tắc trong Database

Trong một ứng dụng Backend, nhiều request có thể truy cập và thay đổi dữ liệu trong database cùng một thời điểm. Nếu không kiểm soát việc truy cập đồng thời phù hợp, các thao tác có thể xung đột hoặc khiến dữ liệu không còn nhất quán.

Để quản lý những tình huống này, database sử dụng các cơ chế như Locking (khóa). Tuy nhiên, khi nhiều transaction cùng giữ và chờ những khóa mà transaction khác đang nắm giữ, hệ thống có thể gặp Deadlock (bế tắc).

Hiểu rõ hai khái niệm này giúp Backend Developer thiết kế transaction an toàn hơn, kiểm soát truy cập dữ liệu đồng thời và xử lý các vấn đề hiệu năng trong những ứng dụng có nhiều request.

1. Locking là gì?

Locking là cơ chế mà database sử dụng để kiểm soát việc truy cập hoặc thay đổi tài nguyên dữ liệu khi nhiều transaction hoạt động đồng thời.

Một transaction có thể cần khóa dữ liệu trước khi thực hiện một thao tác để tránh xung đột với các transaction khác. Nếu transaction khác yêu cầu một khóa không tương thích trên cùng tài nguyên, nó có thể phải chờ cho đến khi khóa được giải phóng.

Ví dụ, một cửa hàng chỉ còn một sản phẩm trong kho:

id name stock
1 Keyboard 1

Hai khách hàng cùng gửi request đặt mua sản phẩm gần như đồng thời. Nếu cả hai request đều đọc số lượng tồn kho là 1 trước khi cập nhật, hệ thống có thể xử lý sai nếu logic kiểm tra và cập nhật không được thiết kế để an toàn khi chạy đồng thời.

Locking giúp database điều phối các thao tác xung đột để những transaction liên quan không tùy ý thay đổi cùng dữ liệu tại cùng thời điểm.

Locking hoạt động như thế nào?

Trong PostgreSQL, có thể sử dụng SELECT ... FOR UPDATE bên trong transaction để khóa các hàng được chọn, ngăn những transaction khác lấy khóa xung đột trên các hàng đó cho đến khi transaction hiện tại kết thúc.

Ví dụ:

BEGIN;

SELECT stock
FROM products
WHERE id = 1
FOR UPDATE;

-- Kiểm tra số lượng tồn kho.
-- Nếu còn hàng, thực hiện cập nhật.

UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock > 0;

COMMIT;

Khi transaction thực hiện SELECT ... FOR UPDATE, PostgreSQL khóa hàng phù hợp. Một transaction khác muốn lấy khóa xung đột trên hàng đó thường phải chờ.

Tuy nhiên, chỉ khóa dữ liệu chưa đủ để bảo đảm nghiệp vụ đặt hàng chính xác. Ứng dụng vẫn cần kiểm tra số lượng tồn kho và kết quả cập nhật. Điều kiện stock > 0 giúp tránh giảm số lượng xuống giá trị âm.

Đây là một ví dụ về row-level locking (khóa hàng) trong PostgreSQL.

2. Các loại Lock thường gặp

Database có thể sử dụng nhiều loại khóa khác nhau tùy theo hệ quản trị và thao tác đang thực hiện.

Shared Lock — Khóa chia sẻ

Shared lock cho phép nhiều thao tác giữ khóa đọc tương thích cùng lúc, nhưng có thể xung đột với những thao tác cần khóa ghi.

Exclusive Lock — Khóa độc quyền

Exclusive lock được sử dụng để bảo vệ tài nguyên khỏi những thao tác xung đột. Trong thời gian khóa được giữ, các transaction khác có thể phải chờ nếu yêu cầu khóa không tương thích.

Row-level Lock — Khóa hàng

Khóa ở mức hàng dữ liệu, cho phép các transaction thao tác trên những hàng khác nhau mà không nhất thiết phải chặn nhau.

Ví dụ, hai transaction cập nhật hai sản phẩm có ID khác nhau thường có thể tiến hành đồng thời nếu không có xung đột khác.

Table-level Lock — Khóa bảng

Khóa ở mức bảng, ảnh hưởng đến phạm vi dữ liệu rộng hơn. PostgreSQL sử dụng nhiều chế độ khóa bảng khác nhau, không phải tất cả đều chặn mọi thao tác đọc và ghi.

Cần lưu ý rằng cách phân loại và hành vi của lock phụ thuộc vào hệ quản trị database. Những ví dụ trên không có nghĩa mọi database đều sử dụng cùng một cơ chế khóa.

3. Deadlock là gì?

Deadlock là tình trạng hai hoặc nhiều transaction chờ những tài nguyên đang bị transaction khác giữ, tạo thành một chu trình chờ khiến các transaction liên quan không thể tiếp tục theo cách thông thường.

Ví dụ, có hai tài khoản ngân hàng là A và B. Hai transaction đồng thời thực hiện chuyển tiền theo hai hướng ngược nhau.

  • Transaction 1 muốn chuyển tiền từ A sang B.
  • Transaction 2 muốn chuyển tiền từ B sang A.

Giả sử hai transaction khóa các hàng theo thứ tự khác nhau:

  1. Transaction 1 khóa hàng tài khoản A.
  2. Transaction 2 khóa hàng tài khoản B.
  3. Transaction 1 yêu cầu khóa hàng B nhưng phải chờ Transaction 2.
  4. Transaction 2 yêu cầu khóa hàng A nhưng phải chờ Transaction 1.

Lúc này, Transaction 1 chờ Transaction 2, còn Transaction 2 chờ Transaction 1. Đây là một deadlock.

Minh họa Deadlock

Transaction 1                  Transaction 2
      |                              |
  Khóa tài khoản A              Khóa tài khoản B
      |                              |
  Chờ khóa B  ---------------->      |
      |                              |
      |       <----------------    Chờ khóa A
      |                              |
      +---------- Deadlock ----------+

Trong PostgreSQL, hệ thống có cơ chế phát hiện deadlock. Khi phát hiện chu trình chờ, PostgreSQL sẽ hủy một transaction để phá vỡ bế tắc, cho phép những transaction còn lại tiếp tục khi các điều kiện khóa đã được giải phóng.

Ứng dụng cần xử lý lỗi từ database và có thể thử lại toàn bộ transaction bị hủy nếu thao tác đó an toàn để retry.

4. Phân biệt Locking và Deadlock

Tiêu chí Locking Deadlock
Bản chất Cơ chế kiểm soát truy cập dữ liệu Tình trạng các transaction chờ khóa lẫn nhau theo chu trình
Mục đích hoặc nguyên nhân Quản lý các thao tác có khả năng xung đột Phát sinh khi các transaction giữ và chờ tài nguyên của nhau
Có phải lỗi không? Không, đây là cơ chế bình thường của database Là tình huống cần được phát hiện và xử lý
Hậu quả Transaction khác có thể phải chờ khóa Database thường phải hủy một transaction để phá vỡ chu trình
Cách xử lý Chọn phạm vi và thời gian giữ khóa phù hợp Giảm khả năng tạo chu trình chờ, xử lý lỗi và retry khi phù hợp

Điều quan trọng là không phải mọi tình trạng chờ lock đều là deadlock. Nếu Transaction 2 chờ Transaction 1 hoàn thành rồi tiếp tục, đó chỉ là chờ khóa thông thường. Deadlock xuất hiện khi tồn tại một chu trình chờ giữa các transaction.

5. Nguyên nhân và cách hạn chế Deadlock

Deadlock thường xuất hiện khi các transaction truy cập nhiều tài nguyên theo thứ tự khác nhau hoặc giữ khóa trong thời gian dài.

Một số nguyên tắc giúp hạn chế tình trạng này:

5.1. Truy cập dữ liệu theo cùng một thứ tự

Nếu nhiều transaction cần khóa các tài khoản A và B, hãy thiết kế để chúng luôn khóa A trước rồi mới khóa B, hoặc theo thứ tự ID thống nhất.

Điều này giúp giảm nguy cơ các transaction giữ những khóa khác nhau rồi chờ khóa của nhau.

5.2. Giữ transaction ngắn

Không nên giữ transaction mở lâu hơn mức cần thiết. Những thao tác như gọi API bên ngoài hoặc chờ người dùng nhập dữ liệu nên được thực hiện ngoài transaction khi có thể.

Transaction kéo dài có thể khiến khóa được giữ lâu hơn và làm tăng thời gian chờ của các request khác.

5.3. Chỉ khóa dữ liệu cần thiết

Tránh khóa quá nhiều hàng hoặc bảng khi chỉ cần thay đổi một phần nhỏ dữ liệu. Phạm vi khóa rộng có thể làm tăng khả năng xung đột giữa các transaction.

5.4. Xử lý lỗi và retry

Khi database hủy transaction do deadlock, ứng dụng có thể thử lại toàn bộ transaction sau một khoảng chờ ngắn, với số lần thử có giới hạn.

Việc retry cần được thiết kế cẩn thận để tránh thực hiện lặp lại các tác động bên ngoài database hoặc gây ra giao dịch trùng lặp.

5.5. Theo dõi truy vấn và log

Khi deadlock xảy ra thường xuyên, cần kiểm tra log database, những transaction liên quan và các tài nguyên mà chúng đang khóa hoặc chờ.

Việc tìm nguyên nhân cụ thể giúp xác định có cần thay đổi thứ tự truy cập dữ liệu, thiết kế transaction hay tối ưu truy vấn hay không.

6. Locking và Deadlock trong dự án Backend thực tế

Trong một hệ thống Backend, nhiều request có thể cùng thực hiện thao tác trên database. Các chức năng như đặt hàng, cập nhật tồn kho, chuyển tiền hoặc xử lý thanh toán thường cần đặc biệt chú ý đến truy cập đồng thời.

Ví dụ, API đặt hàng có thể thực hiện những bước sau:

  1. Nhận request đặt hàng từ client.
  2. Bắt đầu transaction.
  3. Kiểm tra và khóa dữ liệu tồn kho cần thiết.
  4. Tạo đơn hàng và cập nhật số lượng sản phẩm.
  5. Commit transaction nếu các thao tác thành công; rollback khi xảy ra lỗi.

Nếu các transaction được thiết kế không phù hợp, hệ thống có thể gặp xung đột khóa, thời gian chờ kéo dài hoặc deadlock.

Backend Developer cần hiểu cách database quản lý khóa để chọn giải pháp phù hợp, thay vì chỉ tập trung vào việc viết các câu truy vấn hoặc gọi ORM.

7. Kết luận

Locking là cơ chế quan trọng giúp database kiểm soát những thao tác truy cập và thay đổi dữ liệu đồng thời. Deadlock là tình trạng bế tắc có thể xảy ra khi các transaction giữ và chờ những khóa của nhau theo một chu trình.

Trong phát triển Backend, mục tiêu không phải là loại bỏ mọi lock, bởi locking là một phần cần thiết để quản lý dữ liệu đồng thời. Thay vào đó, lập trình viên cần thiết kế transaction hợp lý, giảm thời gian giữ khóa, thống nhất thứ tự truy cập tài nguyên và xử lý lỗi deadlock an toàn.

Hiểu được mối quan hệ giữa locking, deadlock và transaction giúp Backend Developer xây dựng những hệ thống có khả năng xử lý nhiều request đồng thời mà vẫn duy trì tính nhất quán của dữ liệu.

Nguồn tham khảo

Bài viết khác

Isolation Level

Isolation Level là gì? Các mức độ cô lập trong Database Trong một ứng dụng Backend, nhiều request có thể truy cập và thay đổi dữ liệu trong database cùng một thời điểm. Khi nhiều transaction chạy đồng thời, kết quả của một transaction có thể bị ảnh hưởng bởi những thao tác của transaction […]

ORM

ORM là gì? (Object-Relational Mapping) Trong quá trình phát triển Backend, ứng dụng thường xuyên cần tương tác với database để lưu trữ, truy vấn, cập nhật và xóa dữ liệu. Một cách phổ biến để thực hiện những thao tác này là viết SQL trực tiếp. Tuy nhiên, khi dự án phát triển, việc […]

N+1 Query Problem

N+1 Query Problem là gì? Trong quá trình phát triển Backend, ứng dụng thường cần truy xuất dữ liệu từ database để trả về cho client. Một API có thể cần lấy danh sách đơn hàng cùng thông tin khách hàng tương ứng. Tuy nhiên, nếu ứng dụng truy vấn dữ liệu liên quan riêng […]

Database Indexes

Database Indexes là gì? Trong Backend, database không chỉ cần lưu trữ dữ liệu mà còn phải tìm kiếm và truy xuất dữ liệu hiệu quả. Khi số lượng bản ghi tăng lên, những truy vấn đơn giản như tìm một người dùng theo email hoặc tìm sản phẩm theo tên cũng có thể mất […]

Transactions & ACID

Transactions & ACID là gì? Trong Backend, một request gửi đến API có thể yêu cầu hệ thống thực hiện nhiều thao tác trên database. Chẳng hạn, khi khách hàng đặt hàng, hệ thống có thể cần tạo đơn hàng, lưu chi tiết đơn hàng và cập nhật số lượng tồn kho. Nếu một thao […]

Database Normalization là gì? Tìm hiểu 1NF, 2NF, 3NF và Denormalization

1. Database Normalization là gì? Database Normalization (chuẩn hóa cơ sở dữ liệu) là quá trình tổ chức dữ liệu thành các bảng và thiết lập mối quan hệ phù hợp nhằm hạn chế dữ liệu trùng lặp không cần thiết, đồng thời giảm các vấn đề có thể xảy ra khi thêm, sửa hoặc […]

Leave a Reply

Your email address will not be published. Required fields are marked *