Bài trước cho thấy khóa có thể làm một truy vấn chờ. Khi điều đó xảy ra trên production — một truy vấn "treo" không rõ lý do — bạn cần chẩn đoán ngay: ai đang chặn ai, và làm sao gỡ. PostgreSQL cung cấp pg_locks và hàm pg_blocking_pids() cho đúng việc đó. Bài này dựng một sự cố chặn thật rồi chạy các truy vấn chẩn đoán để bạn có sẵn "bộ đồ nghề" khi cần.
Bối cảnh: một truy vấn đang treo
Tôi dựng tình huống: session A UPDATE hàng id=1 rồi giữ giao dịch mở (chưa commit); session B cố UPDATE cùng hàng id=1 nên bị chặn. Giờ session B đang "treo" và ta cần tìm ra vì sao.
Bốn bước chẩn đoán
Bước 1 — tìm ai đang chờ khóa qua pg_stat_activity. Cột wait_event_type='Lock' là dấu hiệu một truy vấn đang bị chặn bởi khóa:
SELECT pid, state, wait_event_type, wait_event, query
FROM pg_stat_activity WHERE wait_event_type='Lock';
Bước 2 — pg_blocking_pids(), cách hiện đại và gọn nhất. Hàm này trả về mảng các PID đang chặn một PID cho trước:
SELECT pid, pg_blocking_pids(pid) AS bi_chan_boi
FROM pg_stat_activity WHERE wait_event_type='Lock';

Hình 1: Năm bước chẩn đoán khóa chờ — tìm kẻ đang chờ qua pg_stat_activity, dùng pg_blocking_pids tìm kẻ chặn, dựng cây chặn đầy đủ, đọc pg_locks thô (granted=false), và gỡ bằng pg_cancel_backend/pg_terminate_backend.
Bước 3 — cây chặn đầy đủ, truy vấn thực dụng nhất khi gỡ sự cố. Nó nối pg_stat_activity với chính nó qua pg_blocking_pids() để hiện cả truy vấn bị chặn lẫn kẻ chặn, kèm thời gian đã chờ:
SELECT blocked.pid, blocked.query AS truy_van_bi_chan,
blocking.pid, blocking.query AS truy_van_chan,
now()-blocked.query_start AS cho
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type='Lock';
Bước 4 — pg_locks thô, khi cần chi tiết mức khóa: cột granted=false chỉ ra kẻ đang chờ.
Đo thật: kết quả

Hình 2: Kết quả thật. pg_stat_activity: PID 43494 wait_event_type=Lock. pg_blocking_pids(43494)={43487}. Cây chặn: 43494 bị chặn bởi 43487, đã chờ 15 giây. pg_locks: kẻ chờ có dòng ShareLock trên transactionid với granted=f.
Đọc các kết quả:
pg_stat_activity: PID 43487 đangPgSleep(giữ khóa), PID 43494 cówait_event_type=Lock— đây là kẻ đang chờ.pg_blocking_pids(43494)= {43487} — trả lời trực tiếp: 43494 bị chặn bởi 43487.- Cây chặn:
43494(UPDATE ... sl+5) bị chặn bởi43487(BEGIN; UPDATE ... sl+1), đã chờ 15 giây. Đây là bức tranh đầy đủ nhất để quyết định gỡ. pg_locksthô: cả hai PID đều giữRowExclusiveLockmức relation vớigranted=t(khóa bảng của hai writer không chặn nhau). Dòng quan trọng: PID 43494 xinShareLocktrêntransactionidvớigranted=f— đây chính là cách một cuộc chờ khóa hàng hiện ra trongpg_locks: kẻ chờ xin khóa transaction id của kẻ giữ để đợi giao dịch đó kết thúc.
Gỡ sự cố
Khi đã biết PID kẻ chặn, có hai cách gỡ:
SELECT pg_cancel_backend(43487); -- hủy truy vấn hiện tại của kẻ chặn (nhẹ)
SELECT pg_terminate_backend(43487); -- ngắt hẳn kết nối của kẻ chặn (mạnh tay)
Ưu tiên pg_cancel_backend — nó chỉ hủy truy vấn đang chạy, giao dịch rollback, kẻ chặn nhả khóa và kẻ chờ chạy tiếp. Chỉ dùng pg_terminate_backend khi cancel không đủ (ví dụ kẻ chặn đang idle in transaction — không có truy vấn để hủy, phải ngắt kết nối).
Đánh đổi cần cân nhắc
idle in transaction là thủ phạm phổ biến nhất. Nhiều sự cố khóa không phải do truy vấn chạy lâu, mà do một giao dịch mở rồi bỏ đó (ứng dụng quên commit/rollback). Kẻ chặn hiện state='idle in transaction' — pg_cancel_backend không giúp (không có truy vấn để hủy), phải pg_terminate_backend. Đặt idle_in_transaction_session_timeout để PostgreSQL tự ngắt các giao dịch treo này.
pg_locks có thể rất lớn và không cho ngữ cảnh. Trên hệ thống bận, pg_locks có hàng nghìn dòng và không kèm nội dung truy vấn. Luôn dùng truy vấn "cây chặn" (join với pg_stat_activity) để có bức tranh có nghĩa, thay vì đọc pg_locks thô. Lưu sẵn truy vấn này ở nơi dễ lấy.
Chuỗi chặn có thể nhiều tầng. A chặn B, B chặn C, C chặn D... pg_blocking_pids() chỉ cho tầng trực tiếp. Trên sự cố phức tạp, cần lần theo chuỗi (hoặc dùng truy vấn đệ quy) để tìm gốc — kẻ chặn đầu tiên không bị ai chặn. Gỡ gốc thường giải phóng cả chuỗi.
Ba ý mang về
pg_blocking_pids(pid)là cách nhanh nhất tìm kẻ chặn: đo thật, nó trả về{43487}cho PID 43494 đang chờ — không cần tự giải mãpg_locksthô để biết ai chặn ai.- Truy vấn "cây chặn" (join pg_stat_activity qua pg_blocking_pids) là công cụ thực dụng nhất: hiện cả truy vấn bị chặn lẫn kẻ chặn kèm thời gian chờ (15 giây) — bức tranh đầy đủ để quyết định gỡ;
pg_locksthô chỉ cần khi soi chi tiết mức khóa (granted=false là kẻ chờ). - Gỡ bằng pg_cancel_backend (ưu tiên) hoặc pg_terminate_backend: chú ý thủ phạm phổ biến là
idle in transaction(phải terminate, và đặt idle_in_transaction_session_timeout), và chuỗi chặn nhiều tầng cần lần tới gốc.
Phần sau ta xét tình huống khóa nguy hiểm nhất — khi hai bên chặn lẫn nhau: Phần sau mổ xẻ deadlock — PostgreSQL tự phát hiện và hủy một giao dịch thế nào, và cách thiết kế để tránh.