PostgreSQL Bài 31: Hệ thống khóa chuyên sâu: Table Locks, Row Locks, Advisory Locks
Mặc dù kiến trúc MVCC (đã học ở Bài 29) đảm bảo "đọc không chặn ghi, ghi không chặn đọc", PostgreSQL vẫn duy trì một hệ thống khóa (Locking System) đồ sộ. Hệ thống này là lớp khiên bảo vệ cấu trúc vật lý của cơ sở dữ liệu khi chạy các lệnh DDL (thêm cột, xóa bảng, tạo index) và cung cấp công cụ khóa bi quan (Pessimistic Locking) cho các nghiệp vụ tài chính, kho hàng đụng độ cực cao.
Đặc biệt với các hệ thống backend chịu tải lớn (viết bằng Golang hay Laravel), việc nắm vững cơ chế SKIP LOCKED hoặc Advisory Locks sẽ giúp bạn thiết kế hàng đợi (Message Queue) hoặc khóa phân tán (Distributed Lock) trực tiếp trên PostgreSQL mà không cần dựng thêm Redis.
1. Table-Level Locks (Khóa cấp độ Bảng)
Mọi câu lệnh truy vấn khi chạm vào một bảng đều tự động xin một loại khóa cấp độ bảng để đảm bảo không ai có thể xóa hoặc sửa cấu trúc bảng đó trong lúc nó đang được đọc/ghi.
PostgreSQL định nghĩa 8 cấp độ khóa bảng. Dưới đây là 4 cấp độ quan trọng nhất bạn thường xuyên gặp phải trên Production:
| Tên khóa (Lock Mode) | Lệnh kích hoạt tiêu biểu | Xung đột với (Block) | Tác động thực tế |
|---|---|---|---|
| AccessShareLock | SELECT |
AccessExclusive |
Rất nhẹ. Cho phép tất cả các lệnh đọc/ghi khác diễn ra song song. Chỉ chặn lệnh xóa/sửa cấu trúc bảng. |
| RowExclusiveLock | INSERT, UPDATE, DELETE |
Share, AccessExclusive |
Cho phép đọc song song. Chặn các lệnh tạo Index thông thường (CREATE INDEX không có cờ CONCURRENTLY). |
| ShareUpdateExclusiveLock | VACUUM (không FULL), CREATE INDEX CONCURRENTLY |
Lệnh DDL, VACUUM khác |
Rất an toàn trên Production. Không chặn SELECT, INSERT, UPDATE, DELETE. |
| AccessExclusiveLock | DROP TABLE, ALTER TABLE, VACUUM FULL |
TẤT CẢ MỌI KHÓA | Cơn ác mộng Production! Chặn đứng hoàn toàn mọi câu lệnh đọc và ghi vào bảng. |
Nguyên tắc "Xếp hàng chết chùm" (Lock Queue Starvation): Giả sử bảng
ordersđang có hàng ngàn câu lệnhSELECTchạy liên tục. Bạn tung ra một lệnhALTER TABLE orders ADD COLUMN note TEXT;. LệnhALTERnày xin khóaAccessExclusiveLock.
Nó không thể chạy ngay vì phải chờ các lệnh
SELECTcũ nhả khóaAccessShareLock.Tệ hơn, một khi lệnh
ALTERđã bước vào hàng đợi xin khóa, toàn bộ các lệnhSELECThoặcINSERTđến sau nó đều bị chặn lại chờ lệnhALTERchạy xong. Hệ thống sập cục bộ chỉ trong vài giây.
2. Row-Level Locks (Khóa cấp độ Dòng) và Pessimistic Locking
Khi xử lý các nghiệp vụ nhạy cảm như trừ tiền ví điện tử hoặc trừ tồn kho (Inventory Deduction), bạn không thể để hai request đồng thời đọc cùng một số dư rồi cùng trừ tiền (race condition). Bạn cần khóa chủ động (Pessimistic Locking).
Cú pháp: SELECT ... FOR [MODE]
2.1. Bốn chế độ Khóa dòng
-
FOR UPDATE:-
Khóa mạnh nhất. Báo hiệu: "Tôi chuẩn bị UPDATE hoặc DELETE dòng này".
-
Chặn toàn bộ các giao dịch khác muốn
FOR UPDATE,FOR SHAREhoặcUPDATE/DELETEcùng dòng đó.
-
-
FOR NO KEY UPDATE(Khuyên dùng cho phần lớn trường hợp):-
Báo hiệu: "Tôi sẽ UPDATE dòng này, nhưng KHÔNG sửa giá trị của Khóa chính/Khóa unique".
-
Nó nhẹ hơn
FOR UPDATEvì vẫn cho phép các bảng khác kiểm tra khóa ngoại (Foreign Key) trỏ tới dòng này.
-
-
FOR SHARE:-
Báo hiệu: "Tôi cần đọc dòng này và đảm bảo không ai được phép sửa/xóa nó cho đến khi tôi xong việc".
-
Chặn
UPDATE/DELETE, nhưng cho phép các transaction khác cùngFOR SHARE.
-
-
KEY SHARE:- Yếu nhất, tự động được PostgreSQL sử dụng dưới nền khi nó cần kiểm tra Foreign Key.
2.2. Vũ khí tối thượng: NOWAIT và SKIP LOCKED
Nếu dòng dữ liệu đã bị khóa bởi Transaction A, khi Transaction B gọi SELECT ... FOR UPDATE, mặc định B sẽ bị treo (block) vô thời hạn chờ A nhả khóa.
Để tránh việc các Goroutine hoặc Worker bị treo nghẽn, hãy dùng các cờ phụ trợ:
-
NOWAIT: Nếu dòng đang bị khóa, PostgreSQL lập tức ném lỗi ngay không chờ đợi (ERROR: could not obtain lock on row). Backend có thể catch lỗi này và báo cho user: "Đơn hàng đang được xử lý, thử lại sau".SQL
SELECT balance FROM wallets WHERE user_id = 1 FOR UPDATE NOWAIT; -
SKIP LOCKED(Trái tim của Database Queue): Bỏ qua hoàn toàn các dòng đang bị khóa và chỉ trả về các dòng đang rảnh rỗi.SQL
-- Đây chính là câu query cốt lõi đằng sau driver 'database' của Laravel Queue! SELECT * FROM jobs WHERE status = 'PENDING' ORDER BY created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1;Nhờ
SKIP LOCKED, 100 worker có thể cùng query vào bảngjobsmà không bị đụng độ nhau (mỗi worker sẽ tự động lướt qua job mà worker khác đã khóa và lấy job tiếp theo).
3. Advisory Locks (Khóa tư vấn / Distributed Locks)
Hầu hết lập trình viên khi cần làm Distributed Lock (đảm bảo 1 tiến trình cronjob chạy duy nhất trên toàn cụm server, hoặc giới hạn rate limit) đều vội vã cài đặt Redis (Redlock).
Thực tế, PostgreSQL cung cấp Advisory Locks — hệ thống khóa ở tầng ứng dụng do PostgreSQL quản lý.
-
Nó không gắn với bất kỳ bảng hay dòng nào.
-
Chỉ đơn giản là một "cái tên" (hoặc ID 64-bit) được lưu trên RAM của PostgreSQL. Nó cực kỳ nhẹ và nhanh.
3.1. Phân loại Advisory Locks
-
Session-level (Cấp độ kết nối): Khóa được giữ cho đến khi bạn chủ động nhả ra (
pg_advisory_unlock) hoặc kết nối database bị đứt. -
Transaction-level (Cấp độ giao dịch): Tự động nhả khóa khi Transaction
COMMIThoặcROLLBACK. Không cần lệnh unlock thủ công (hậu tố_xact_).
3.2. Kịch bản ứng dụng: Chống chạy trùng Cronjob ở kiến trúc Microservices
Giả sử bạn có 3 pod backend (Go) cùng chạy. Cả 3 pod đều kích hoạt một cronjob quét dữ liệu vào lúc 00:00. Để đảm bảo chỉ đúng 1 pod được chạy job này:
Mã giả SQL điều phối bằng Advisory Lock:
SQL
-- 1. Định nghĩa một ID ngẫu nhiên đại diện cho job "DailyReport" (ví dụ: 123456789)
-- 2. Pod gọi hàm try_lock (Không block, trả về boolean ngay lập tức)
SELECT pg_try_advisory_lock(123456789);
-
Pod A gọi trước: Trả về
TRUE(Được cấp khóa) ──► Bắt đầu chạy logic quét dữ liệu. -
Pod B và Pod C gọi chậm hơn 1 mili-giây: Trả về
FALSE(Khóa đã có chủ) ──► Thoát ngay, không chạy cronjob nữa. -
Khi Pod A chạy xong, gọi lệnh giải phóng:
SQL
SELECT pg_advisory_unlock(123456789);(Nếu Pod A bị crash chết đột ngột, kết nối TCP đứt, PostgreSQL cũng tự động nhả khóa này ngay lập tức, hoàn toàn không bị kịch bản "dead lock" như khóa file hệ thống).
4. Tóm tắt & Bài tiếp theo
-
Table Locks: Bảo vệ cấu trúc DDL. Lệnh
ALTER TABLElà hung thần vớiAccessExclusiveLock, cần thận trọng khi chạy trên production. -
Row Locks:
FOR UPDATEchặn cập nhật, kết hợpSKIP LOCKEDđể xây dựng hàng đợi tốc độ cao (Queue/Outbox pattern) triệt tiêu tranh chấp. -
Advisory Locks: Giải pháp Distributed Lock hoàn hảo, nhẹ gọn, lưu trực tiếp trên RAM của Postgres, thay thế hoàn toàn nhu cầu dựng Redis cho các bài toán khóa ứng dụng phân tán đơn giản.
Bài 32 xem tiếp: Deadlock (Khóa chéo) và Công cụ chẩn đoán (Lock Monitoring) — khóa dữ liệu là con dao hai lưỡi. Chúng ta sẽ tìm hiểu nguyên nhân sinh ra Deadlock (
SQLSTATE 40P01), cách PostgreSQL tự động phát hiện và triệt tiêu Deadlock (Deadlock Detector), cùng bộ truy vấn SQL thực chiến để soi xem kết nối nào đang block kết nối nào qua bảngpg_locksvàpg_stat_activity.
All rights reserved