Xử lý deadlock PostgreSQL và giảm lock wait khi giao dịch đồng thời
Nhiều ứng dụng vượt qua giai đoạn kiểm thử chức năng một cách trơn tru nhưng lại phát sinh lỗi gián đoạn hoặc treo kết nối ngay khi nhiều người dùng cùng thao tác trên một nhóm dữ liệu. Trong PostgreSQL, hai triệu chứng điển hình nhất của bài toán đồng thời là hiện tượng chờ khóa kéo dài (lock wait timeout) và khóa chết (deadlock).
Việc xử lý deadlock PostgreSQL và giảm thời gian chờ khóa đòi hỏi phải nhìn vào vòng đời giao dịch (transaction), thứ tự truy cập tài nguyên và cách thức thực thi các bản cập nhật cấu trúc bảng (schema migration).
Phân biệt deadlock và lock wait trên môi trường vận hành
Dù cùng xuất phát từ cơ chế khóa để bảo vệ tính toàn vẹn dữ liệu, deadlock và lock wait có bản chất và cách xử lý khác nhau:
- Chờ khóa (Lock wait): Xảy ra khi giao dịch B cần khóa một dòng hoặc một bảng đang bị giao dịch A nắm giữ. Giao dịch B phải đứng đợi cho tới khi giao dịch A hoàn tất (
COMMIThoặcROLLBACK). Nếu giao dịch A kéo dài hoặc hàng đợi tích tụ thành dây chuyền, các kết nối ứng dụng sẽ bị nghẽn và hết thời gian chờ. - Khóa chết (Deadlock): Xảy ra khi hai hoặc nhiều giao dịch tạo thành một vòng tròn phụ thuộc khép kín — giao dịch A giữ khóa tài nguyên 1 và chờ tài nguyên 2, trong khi giao dịch B đang giữ tài nguyên 2 và chờ tài nguyên 1. Sau khoảng thời gian
deadlock_timeout(mặc định 1 giây), bộ phát hiện của PostgreSQL sẽ chủ động hủy một giao dịch để giải phóng vòng lặp, trả về mã lỗi40P01cho ứng dụng.
Khi một hệ thống liên tục gặp lỗi timeout hoặc deadlock, việc chỉ tăng giới hạn thời gian chờ hoặc tăng số lượng kết nối sẽ làm trầm trọng thêm tình trạng cạn kiệt tài nguyên máy chủ.
Ba nguyên nhân phổ biến gây tranh chấp khóa trong ứng dụng
Qua quá trình rà soát các hệ thống thực tế, phần lớn sự cố tranh chấp khóa bắt nguồn từ ba mẫu thiết kế ở tầng ứng dụng và cơ sở dữ liệu:
- Giữ giao dịch mở trong khi gọi dịch vụ bên ngoài: Một luồng xử lý mở giao dịch cơ sở dữ liệu, cập nhật trạng thái đơn hàng hoặc số dư, sau đó gọi sang cổng thanh toán, dịch vụ gửi email hoặc API của mô hình AI rồi mới đóng giao dịch. Chỉ cần dịch vụ bên ngoài phản hồi chậm vài giây, khóa trên các dòng dữ liệu liên quan sẽ bị giữ chặt, chặn toàn bộ các yêu cầu đồng thời khác.
- Cập nhật nhiều bản ghi nhưng không thống nhất thứ tự khóa: Khi hai tiến trình cùng cập nhật nhiều dòng trong một giao dịch (ví dụ chuyển điểm giữa hai tài khoản hoặc trừ tồn kho nhiều sản phẩm trong giỏ hàng) nhưng truyền danh sách định danh theo thứ tự ngẫu nhiên, xác suất xảy ra deadlock tăng nhanh theo mức độ đồng thời.
- Thiếu chỉ mục trên cột khóa ngoại (Foreign Key): Khi bảng cha thực hiện xóa bản ghi hoặc cập nhật khóa chính, cơ sở dữ liệu phải kiểm tra tính toàn vẹn tham chiếu trên bảng con. Nếu cột khóa ngoại ở bảng con chưa có chỉ mục phù hợp, thao tác kiểm tra và khóa mở rộng ra phạm vi lớn hơn cần thiết.
Nguyên tắc thiết kế giao dịch để triệt tiêu deadlock và giảm chờ khóa
Để kiểm soát tranh chấp khóa một cách có hệ thống, cần áp dụng đồng bộ các nguyên tắc ở cả mã nguồn ứng dụng lẫn cấu hình cơ sở dữ liệu:
- Rút ngắn tối đa thời gian sống của giao dịch: Tách toàn bộ các bước tính toán nặng, đọc tệp hoặc gọi mạng ra ngoài phạm vi
BEGIN ... COMMIT. Giao dịch chỉ nên mở khi mọi dữ liệu đầu vào đã sẵn sàng để ghi. - Cố định thứ tự khóa tài nguyên: Khi một giao dịch cần cập nhật hoặc khóa nhiều dòng dữ liệu (
SELECT ... FOR UPDATEhoặcUPDATEtheo danh sách), luôn sắp xếp danh sách khóa chính theo thứ tự tăng dần cố định trước khi thực thi. Khi mọi giao dịch đều đi theo cùng một chiều, vòng lặp deadlock giữa các dòng đó không thể hình thành. - Sử dụng cấp độ khóa vừa đủ: Nếu giao dịch chỉ cần đảm bảo các cột không phải khóa không bị sửa đổi trong lúc đọc, cân nhắc dùng
FOR NO KEY UPDATEhoặcFOR KEY SHAREthay vì khóa độc quyền nặng hơn, giúp các bảng con tham chiếu qua khóa ngoại không bị chặn oan. - Đặt ngưỡng bảo vệ
lock_timeoutvàstatement_timeout: Thay vì để một câu lệnh chờ khóa vô thời hạn làm treo toàn bộ hàng đợi kết nối, cần đặt ngưỡnglock_timeouthợp lý cho từng nhóm tác vụ và thiết kế cơ chế thử lại (retry) an toàn kèm độ trễ ngẫu nhiên (jitter) cho các giao dịch có tính lũy đẳng (idempotent).
Ngăn chặn hiệu ứng hàng đợi khóa khi chạy schema migration
Một rủi ro lớn nhưng thường bị bỏ sót là chạy lệnh thay đổi bảng (ALTER TABLE) trực tiếp trên môi trường đang có tải cao. Ngay cả một lệnh rất nhanh như thêm cột hoặc đổi kiểu dữ liệu cũng cần lấy khóa ACCESS EXCLUSIVE trên bảng.
Nếu ngay thời điểm đó có một truy vấn đọc báo cáo đang chạy kéo dài vài chục giây, lệnh ALTER TABLE sẽ phải xếp hàng chờ truy vấn báo cáo xong. Nguy hiểm hơn, mọi câu lệnh SELECT, INSERT, UPDATE ngắn ngủi đến sau lệnh ALTER TABLE đều bị chặn lại phía sau hàng đợi khóa đó, gây gián đoạn toàn bộ dịch vụ chỉ trong vài giây.
Do đó, mọi kịch bản migration trên môi trường vận hành cần:
- Đặt
SET lock_timeoutngắn (ví dụ vài giây) ngay đầu phiên chạy migration để nếu không lấy được khóa ngay thì chủ động hủy và thử lại, tuyệt đối không làm nghẽn hàng đợi truy vấn của người dùng. - Tách việc thêm ràng buộc (
ADD CONSTRAINT ... NOT VALIDrồi mớiVALIDATE CONSTRAINT) và tạo chỉ mục (CONCURRENTLY) thành các bước không khóa ghi độc quyền.
Để kết hợp việc giảm chờ khóa với tối ưu chi phí đọc/ghi của từng câu lệnh, bạn có thể đọc thêm bài tối ưu câu truy vấn SQL chậm và phân tích execution plan PostgreSQL và quy trình chiến lược rollback không gián đoạn. Ngoài ra, hãy tham khảo các bài thực hành chuyên sâu trên Wiki về tái hiện và gỡ lỗi Deadlock, kiểm soát DDL Lock khi chạy schema migration và cơ chế MVCC cùng mức cô lập giao dịch.
Rà soát giao dịch và tối ưu cơ sở dữ liệu cùng ShiftLeft
Khi hệ thống của bạn xuất hiện deadlock, timeout theo khung giờ cao điểm hoặc cần rà soát độ an toàn của các luồng giao dịch quan trọng, hãy tham khảo dịch vụ tối ưu database và dịch vụ audit hệ thống phần mềm của ShiftLeft để được chẩn đoán dựa trên bằng chứng thực tế.