Đồng thời (concurrency) là nơi database vừa mạnh vừa dễ gây rắc rối. PostgreSQL dùng khóa (lock) để nhiều giao dịch cùng chạy mà không giẫm lên nhau — nhưng khóa cũng là nguồn gốc của những sự cố "truy vấn bỗng treo". Bài này mổ xẻ ba tầng khóa của PostgreSQL — hàng, bảng, và advisory — bằng cách cho hai session thật tranh nhau và đo bằng đồng hồ ai chặn ai.

Ba tầng khóa

Khóa hàng (row lock) là tầng mịn nhất: SELECT ... FOR UPDATE hoặc một UPDATE/DELETE tự khóa đúng những hàng bị đụng. Hai giao dịch sửa cùng một hàng phải nối đuôi; sửa hàng khác nhau thì chạy song song.

Khóa bảng (table lock) có 8 mức, từ nhẹ tới nặng. Mỗi thao tác lấy một mức: SELECT lấy ACCESS SHARE, INSERT/UPDATE/DELETE lấy ROW EXCLUSIVE (không chặn nhau ở mức bảng), còn DDL như ALTER TABLE/DROP/TRUNCATE/VACUUM FULL lấy ACCESS EXCLUSIVE — mức chặn tất cả, kể cả đọc.

Advisory lock khác hẳn: nó không gắn với bảng hay hàng nào, mà là khóa theo một số nguyên do ứng dụng tự chọn. Dùng để điều phối logic ở tầng ứng dụng (chống chạy job trùng, leader election, hàng đợi công việc).

-- 1. Khóa hàng
BEGIN; SELECT so_du FROM taikhoan WHERE id=1 FOR UPDATE;
-- 2. Khóa bảng (DDL)
ALTER TABLE taikhoan ADD COLUMN x int;   -- ACCESS EXCLUSIVE: chặn MỌI truy vấn
-- 3. Advisory
SELECT pg_advisory_lock(42);   -- khóa theo số nguyên tự chọn

Ảnh chụp đoạn mã SQL nền tối minh hoạ khóa hàng bảng và advisory ba tầng khóa của PostgreSQL, 1 khóa hàng row lock chỉ chặn thao tác trên cùng hàng BEGIN SELECT so_du FROM taikhoan WHERE id bằng 1 FOR UPDATE khóa hàng id 1 UPDATE DELETE cũng tự khóa hàng bị đụng session khác đụng cùng hàng chờ đụng hàng khác chạy song song, 2 khóa bảng 8 mức ACCESS EXCLUSIVE chặn cả đọc SELECT ACCESS SHARE INSERT UPDATE DELETE ROW EXCLUSIVE không chặn nhau ở mức bảng VACUUM CREATE INDEX CONCURRENTLY SHARE UPDATE EXCLUSIVE ALTER TABLE DROP TRUNCATE VACUUM FULL ACCESS EXCLUSIVE ALTER TABLE taikhoan ADD COLUMN x int ACCESS EXCLUSIVE chặn mọi truy vấn DDL trên bảng đang tải nặng có thể treo cả ứng dụng, 3 advisory lock khóa do ứng dụng tự định nghĩa SELECT pg_advisory_lock 42 khóa theo một số nguyên tự chọn làm việc độc quyền SELECT pg_advisory_unlock 42 không gắn với bảng hàng nào dùng cho chống chạy job trùng hàng đợi công việc điều phối leader logic khóa ở tầng ứng dụng pg_try_advisory_lock trả về ngay true false thay vì chờ, quan sát khóa SELECT locktype mode granted FROM pg_locks xem ai giữ chờ khóa gì

Hình 1: Ba tầng khóa của PostgreSQL. Khóa hàng (FOR UPDATE) chỉ chặn cùng hàng; khóa bảng 8 mức mà ACCESS EXCLUSIVE của DDL chặn cả đọc; advisory lock do ứng dụng tự định nghĩa theo số nguyên.

Đo thật: ai chặn ai

Tôi cho một session A giữ mỗi loại khóa trong 4 giây (chạy nền), rồi đo thời gian session khác phải chờ:

Ảnh chụp bảng kết quả đo thật nền tối ai chặn ai chờ 3 giây hay chạy ngay session A giữ khóa 4s ở nền đo thời gian session khác phải chờ PostgreSQL 16, 1 khóa hàng FOR UPDATE trên id 1 session B UPDATE id 1 cùng hàng chờ 3,02 giây bị chặn session C UPDATE id 2 hàng khác chờ 0,05 giây chạy ngay khóa ở mức hàng chỉ xung đột trên đúng hàng bị khóa, 2 khóa bảng ACCESS EXCLUSIVE như DDL session A LOCK TABLE ACCESS EXCLUSIVE giữ 4s session B chỉ SELECT count chờ 3,02 giây ACCESS EXCLUSIVE xung đột với cả ACCESS SHARE mà SELECT cần một ALTER TABLE có thể chặn cả những truy vấn chỉ đọc, 3 advisory lock khóa ứng dụng tự định nghĩa session B pg_advisory_lock 42 cùng khóa chờ 3,01 giây session C pg_advisory_lock 99 khóa khác chờ 0,05 giây không gắn bảng hàng khóa theo số nguyên do ứng dụng chọn, bảng loại khóa hàng FOR UPDATE cùng đối tượng chờ 3,02s đối tượng khác 0,05s dùng cho sửa đồng thời an toàn bảng ACCESS EXCL chặn cả SELECT DDL cẩn thận advisory chờ 3,01s 0,05s điều phối ứng dụng, bài học chính khóa hàng mịn chỉ chặn cùng hàng nền tảng của ghi đồng thời khóa bảng ACCESS EXCLUSIVE DDL thô chặn tất cả chạy vào giờ thấp điểm advisory điều phối logic ứng dụng mà không cần bảng khóa riêng

Hình 2: Đo thật. Khóa hàng: cùng hàng chờ 3,02s, hàng khác 0,05s. Khóa bảng ACCESS EXCLUSIVE: một SELECT thường cũng chờ 3,02s. Advisory: cùng khóa chờ 3,01s, khóa khác 0,05s.

  • Khóa hàng: session B UPDATE id=1 (cùng hàng đang bị FOR UPDATE) chờ 3,02 giây; session C UPDATE id=2 (hàng khác) chỉ 0,05 giây. Khóa ở mức hàng — chỉ xung đột trên đúng hàng bị khóa.
  • Khóa bảng ACCESS EXCLUSIVE: session A LOCK TABLE ... ACCESS EXCLUSIVE (như một DDL đang chạy), rồi session B chỉ SELECT count(*) cũng phải chờ 3,02 giây. Vì ACCESS EXCLUSIVE xung đột với cả ACCESS SHARE mà một SELECT bình thường cần. Đây là cái bẫy production kinh điển: một ALTER TABLE trên bảng đang tải nặng có thể chặn cả những truy vấn chỉ đọc, treo cả ứng dụng.
  • Advisory lock: session B pg_advisory_lock(42) (cùng khóa) chờ 3,01 giây; session C pg_advisory_lock(99) (khóa khác) chỉ 0,05 giây. Khóa theo số nguyên, không gắn với dữ liệu.

Khi nào dùng loại nào

Khóa hàng là nền tảng của ghi đồng thời an toàn. SELECT ... FOR UPDATE khi bạn đọc một hàng rồi sẽ cập nhật nó dựa trên giá trị vừa đọc (tránh lost update). PostgreSQL cũng có FOR UPDATE SKIP LOCKED — bỏ qua hàng đang bị khóa thay vì chờ, rất hữu ích cho hàng đợi công việc.

Khóa bảng phần lớn là tự động (bạn không gọi LOCK TABLE tay). Điều cần nhớ là mức khóa mà mỗi DDL lấy: ALTER TABLE thêm cột có DEFAULT hằng số (PG11+) hay CREATE INDEX CONCURRENTLY nhẹ hơn nhiều so với ALTER TABLE viết lại bảng. Luôn kiểm mức khóa của một DDL trước khi chạy trên production.

Advisory lock cho điều phối tầng ứng dụng mà không cần dựng bảng khóa riêng: đảm bảo chỉ một tiến trình chạy một job (pg_try_advisory_lock trả về ngay true/false thay vì chờ), leader election, hay khóa theo khóa nghiệp vụ. Nhớ unlock hoặc dùng bản xact (pg_advisory_xact_lock) tự nhả khi giao dịch kết thúc.

Đánh đổi cần cân nhắc

ACCESS EXCLUSIVE của DDL là mối nguy production. Một ALTER TABLE chờ lấy ACCESS EXCLUSIVE sẽ xếp hàng sau các truy vấn đang chạy — và trong lúc chờ, nó chặn mọi truy vấn mới (kể cả SELECT) vốn cần lấy khóa sau nó. Kết quả: một DDL tưởng nhanh có thể gây "lock queue" treo cả bảng. Đặt lock_timeout ngắn cho DDL, và chạy vào giờ thấp điểm.

Advisory lock dễ bị quên nhả. Advisory lock kiểu session tồn tại tới khi bạn unlock hoặc kết nối đóng. Trong môi trường có connection pooling, một kết nối tái dùng có thể giữ advisory lock cũ nếu code quên nhả — dùng pg_advisory_xact_lock (tự nhả cuối giao dịch) an toàn hơn.

Khóa hàng có thể leo thang thành deadlock. Hai giao dịch khóa hai hàng theo thứ tự ngược nhau sẽ deadlock; PostgreSQL tự phát hiện và hủy một giao dịch (deadlock detected). Tránh bằng cách luôn khóa các hàng theo cùng một thứ tự trong mọi giao dịch.

Ba ý mang về

  1. Khóa hàng chỉ chặn thao tác trên cùng hàng: đo thật, UPDATE cùng hàng đang FOR UPDATE chờ 3,02s nhưng hàng khác chỉ 0,05s — đây là nền tảng cho phép nhiều giao dịch ghi song song an toàn khi đụng hàng khác nhau.
  2. Khóa bảng ACCESS EXCLUSIVE của DDL chặn cả SELECT: đo thật, một SELECT count(*) thường cũng chờ 3,02s khi bảng bị giữ ACCESS EXCLUSIVE — nên một ALTER TABLE bất cẩn có thể treo cả ứng dụng; đặt lock_timeout và chạy giờ thấp điểm.
  3. Advisory lock là khóa ứng dụng tự định nghĩa theo số nguyên: cùng khóa chờ 3,01s, khóa khác 0,05s — dùng cho điều phối job/leader mà không cần bảng khóa riêng, ưu tiên bản xact tự nhả để tránh quên unlock trong pool.

Phần sau ta học cách chẩn đoán khi khóa gây treo: Phần sau mổ xẻ cách đọc pg_locks (và pg_stat_activity) để tìm truy vấn nào đang chặn truy vấn nào, và gỡ thế nào.