Lỗi
Bạn sẽ thấy nội dung tương tự thế này trong logs hoặc output của ứng dụng:
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 7890; blocked by process 67890.
Process 67890 waits for ShareLock on transaction 12345; blocked by process 12345.
HINT: See server log for query details.
PostgreSQL đã hủy một transaction (gọi là "nạn nhân") để phá vỡ vòng lặp. Transaction còn lại commit bình thường. Transaction nạn nhân bị rollback — và ứng dụng của bạn nhận được lỗi này thay vì kết quả.
Nguyên nhân gây ra Deadlock
Tình huống điển hình: hai transaction khóa cùng các row theo thứ tự ngược chiều nhau.
-- Transaction A -- Transaction B
BEGIN; BEGIN;
UPDATE accounts UPDATE accounts
SET balance = balance - 50 SET balance = balance - 30
WHERE id = 1; WHERE id = 2; -- khóa row 2
-- bây giờ cố truy cập row 1 → bị chặn bởi A
UPDATE accounts
SET balance = balance + 30
WHERE id = 2; -- cố truy cập row 2 → bị chặn bởi B
-- DEADLOCK
Cả hai transaction đều không thể tiến lên. PostgreSQL phát hiện vòng lặp sau deadlock_timeout (mặc định 1 giây), chọn một nạn nhân và kết thúc nó.
Sửa nhanh: Thử lại Transaction bị lỗi
PostgreSQL đã rollback transaction nạn nhân. Bạn chỉ cần chạy lại nó. Thêm vòng lặp retry — ba lần thử với khoảng dừng ngắn là đủ để xử lý hầu hết các trường hợp thực tế.
Python (psycopg2)
import psycopg2
from psycopg2 import errors
import time
def run_with_retry(conn, fn, max_retries=3):
for attempt in range(max_retries):
try:
with conn.cursor() as cur:
fn(cur)
conn.commit()
return
except errors.DeadlockDetected:
conn.rollback()
if attempt setTimeout(r, 100 * (attempt + 1)));
} else {
throw err;
}
}
}
}
Sửa vĩnh viễn: Khóa Rows theo Thứ tự Nhất quán
Retry chỉ sửa triệu chứng. Để ngăn deadlock xảy ra ngay từ đầu, luôn lấy khóa theo cùng một thứ tự — sắp xếp theo primary key trước khi thao tác trên bất kỳ row nào.
-- Xấu: Transaction A khóa id=1 rồi id=2
-- Transaction B khóa id=2 rồi id=1
-- Tốt: khóa cả hai row ngay từ đầu, theo thứ tự id tăng dần
BEGIN;
SELECT * FROM accounts
WHERE id IN (1, 2)
ORDER BY id
FOR UPDATE; -- khóa row 1, rồi row 2 — luôn luôn như vậy
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;
SELECT ... FOR UPDATE ORDER BY id lấy tất cả các khóa cần thiết trong một lần, theo thứ tự có thể dự đoán. Hai transaction theo mẫu này sẽ không bao giờ deadlock nhau — một cái đơn giản là chờ trong khi cái kia hoàn thành.
Giữ Transactions Ngắn Gọn
Transaction chạy lâu giữ khóa lâu hơn, làm tăng nguy cơ xung đột. Một vài quy tắc đơn giản giúp giảm đáng kể tần suất deadlock:
- Thực hiện tất cả tính toán trước khi mở transaction — không phải bên trong nó.
- Không bao giờ thực hiện network call hay chờ input từ người dùng bên trong transaction.
- Đối với job queue, dùng
FOR UPDATE SKIP LOCKEDđể các worker bỏ qua row đã được worker khác nhận.
-- Job queue: lấy một row đang chờ mà không chặn các worker khác
SELECT id, payload FROM jobs
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;
Xác định Query Nào Đang Gây Deadlock
Trước khi viết lại bất cứ thứ gì, hãy xác nhận deadlock thực sự đến từ đâu. Hai cách tiếp cận:
Cách 1 — Bật logging (duy trì qua các lần khởi động lại, phù hợp để giám sát production):
-- postgresql.conf hoặc ALTER SYSTEM SET
deadlock_timeout = 200ms -- mặc định là 1s; giảm xuống để bắt được nhiều hơn
log_lock_waits = on
log_min_duration_statement = 1000 -- log các query chậm hơn 1s
Cách 2 — Query trực tiếp (không cần khởi động lại, hiển thị những gì đang bị chặn ngay lúc này):
SELECT
blocked.pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
Kiểm tra Kết quả Sửa lỗi
- Tái hiện lỗi trước: chạy hai transaction đồng thời theo thứ tự cũ và xác nhận bạn nhận được
ERROR: deadlock detected. - Áp dụng bản sửa lỗi — khóa nhất quán hoặc
FOR UPDATEngay từ đầu. - Chạy lại test: lần này không có lỗi deadlock.
- Theo dõi production logs: sau khi deploy, grep tìm
deadlock detected. Nếu không còn xuất hiện, bạn đã giải quyết được nguyên nhân gốc rễ.
-- Smoke test: mở hai tab psql và chạy đồng thời
-- Tab 1:
BEGIN;
SELECT * FROM accounts WHERE id IN (1,2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;
-- Tab 2 (cùng thứ tự khóa = không deadlock):
BEGIN;
SELECT * FROM accounts WHERE id IN (1,2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance + 30 WHERE id = 1;
UPDATE accounts SET balance = balance - 30 WHERE id = 2;
COMMIT;
Tab 2 tạm dừng ngắn trong khi Tab 1 giữ các khóa, sau đó tiếp tục bình thường. Không có lỗi, không có nạn nhân — chỉ là hành vi chờ-và-tiếp-tục như mong đợi.

