Làm việc với SQL hay PostgreSQL đủ lâu nhưng có bao giờ bạn đặt câu hỏi sẽ thế nào nếu như thực hiện một lệnh SELECT và DROP trên cùng một table cùng lúc? Lệnh nào sẽ chạy trước lệnh nào sẽ chạy sau, độ ưu tiên của các câu lệnh như thế nào hay là PostgreSQL dựa vào đâu để phân mức ưu tiên? Bài viết này hôm nay tôi xin phép trình bày cách mà PostgreSQL sử dụng Khoá (Locks) để giải quyết xung đột dữ liệu như thế nào nhé.
PostgreSQL cung cấp nhiều chế độ khóa khác nhau để kiểm soát quyền truy cập đồng thời vào dữ liệu trong bảng. Các khoá này có thể được sử dụng để can thiệp vào các tình huống của MVCC (Multi Version Concurrency Control).
Ngoài ra, hầu hết các lệnh PostgreSQL có được các khóa của các chế độ thích hợp để đảm bảo rằng các bảng được tham chiếu không bị xóa hoặc sửa đổi theo cách không mong muốn trong khi các lệnh đang được thực thi. Ví dụ như lệnh TRUNCATE không thể thực hiện đồng thời với các lệnh thao tác khác trên cùng một bảng như SELECT..., để làm được điều TRUNCATE được cấp phát cho một khoá gọi là ACCESS EXCLUSIVE.
Postgres có các cấp khoá sau đây:
Các cấp khoá mô tả miền xung đột của các lệnh PostgreSQL ở các cấp độ bảng (table), dòng (row) hay là trang (page)...
Tuy nhiên trong bài viết này tôi xin phép trình bày hai cấp khoá là Table và Row. Bạn đọc có thể tìm hiểu thêm các loại khoá tại trang tài liệu của PostgreSQL.
Là các khoá ở quy mô bảng, các khoá này được cấp phát tự động hoặc thông qua lệnh LOCK
.
Tự động có nghĩa là phụ thuộc vào lệnh SQL nào khi gọi thì bạn ngay lập tức sẽ được cấp pháp khoá đó. Dưới đây là một số câu lệnh quen thuộc và các khoá được cấp tự động tương ứng khi bạn gọi chúng.
Nhiều khoá và chi tiết hơn xem lại trang tài liệu của postgres.
Dưới đây là bảng thể hiện sự xung đột của các khoá với nhau.
Tôi có thể giải thích như sau: nhìn vào bảng chúng ta thấy khoá ACCESS SHARE sẽ xung đột với ACCESS EXCLUSIVE vì thế trong các transaction nếu lệnh nào đang được thực hiện trước (nắm khoá ACCESS SHARE) thì lệnh sau (nắm khoá ACCESS EXCLUSIVE) phải chờ cho đến khi transaction được hoàn thành mới có thể tiếp tục và ngược lại.
-- (1)
BEGIN;
-- (2)
SELECT * FROM users; -- ACCESS SHARE LOCKS
-- (4)
COMMIT;
-- (3)
TRUNCATE users; -- ACCESS EXCLUSIVE LOCKS
Hãy thử chạy các lệnh trên trên hai tiến trình khác nhau theo các bước (1) (2) (3) (4) để xem điều gì xảy ra nhé.
Các khoá cấp hàng được cấp phát bằng cách xác định tên của khoá trong câu truy vấn. Có 4 khoá là:
FOR UPDATE khiến các hàng được truy xuất bởi SELECT bị khóa như thể để cập nhật. Điều này giúp chúng không bị khóa, sửa đổi hoặc xóa bởi các transaction khác cho đến khi transaction hiện tại kết thúc. Nghĩa là, các transaction khác như DELETE hoặc UPDATE một trong số các hàng này sẽ bị chặn cho đến khi transaction hiện tại kết thúc.
Tương tự như Table-Level Locks, bảng trên mô tả xung đột giữa các khoá. Các khoá xung đột với nhau sẽ phải đợi cho đến khi khoá cấp phát trước được thực hiện xong thì mới đến lượt mình.
Ví dụ bạn phát triển một tính năng đổi quà, có một bảng quà chứa số lượng còn lại của quà muốn đổi và bảng lưu số điểm hợp lệ của bạn dùng để đổi quà. Khi đổi quà bạn cần lấy ra số lượng quà còn lại và số điểm của bạn, kiểm tra qua nhiều điều kiện khắc nghiệt và tốn rất nhiều thời gian nữa mới có thể đổi. Giả sử trong lúc yêu cầu thứ nhất đã lấy ra số lượng quà còn lại là 1 và có yêu cầu thứ hai ngay lập tức cũng lấy ra số lượng quà còn lại là 1 thì điều gì sẽ xảy ra nếu như cả hai tiếp tục đổi quà? Để ngăn chặn điều đó bạn có thể sử dụng Row-Level Locks, tức là khi một transaction đang lấy số lượng quà còn lại để bắt đầu validate thì transaction thứ hai cần chờ đợi cho đến khi transaction đầu kết thúc.
Deadlock xảy ra khi hai hoặc nhiều transaction giữ các khoá mà bên kia mong muốn. Ví dụ transaction thứ nhất có được khoá ACCESS EXCLUSIVE trên bảng A và sau đó muốn có tiếp khoá đó trên bảng B trong khi transaction thứ hai đã có khoá ACCESS EXCLUSIVE trên bảng B và bây giờ lại muốn có khoá đó trên bảng A thì lúc đó không có transaction nào có thể tiếp tục. PostgreSQL tự động phát hiện tình huống này và giải quyết bằng cách huỷ bỏ một trong các transaction có liên quan, transaction bị huỷ bỏ không thể đoán trước.
Deadlock cũng có thể xảy ra ở trong các khoá cấp hàng khi các transaction cũng giữ khoá của nhau.
Biện pháp bảo vệ tốt nhất chống lại các deadlock nói chung là tránh để cho các lệnh có được các khóa trên nhiều đối tượng (table/row) theo một thứ tự xác định.
Nếu transaction được thực hiện trên nhiều đối tượng liên quan, cách tốt nhất là làm sao cho thời gian thực hiện transaction càng ngắn càng tốt. Tránh giữ các transaction trong thời gian dài. Ví dụ cho một thực hành tồi là transaction sẽ được thực thi tiếp cho đến khi chờ đợi thông tin nhập của người dùng xong.
Bài viết trên đây tôi đã trình bày về cách mà PostgreSQL sử dụng khoá để quản lý quyền truy cập đồng thời vào dữ liệu trong bảng. Mặc dù là dành riêng cho PostgreSQL nhưng tư tưởng sử dụng khoá vẫn tồn lại trong các loại cơ sở dữ liệu SQL khác như MySQL, SQL Server... Nắm được cách hoạt động của khoá sẽ giúp bạn ứng biến với những tình huống trong việc phát triển ứng dụng cùng SQL.
Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.
Bình luận (1)