“Cơn ác mộng” lúc 2 giờ sáng
Điện thoại rung liên hồi. Slack nhảy thông báo lỗi 500 liên tục. Sentry thì ngập ngụa log: Deadlock found when trying to get lock; try restarting transaction. Nếu bạn đang vận hành một hệ thống Backend, đây chính là tín hiệu báo động đỏ cho thấy database đang “nghẹt thở”.
Tôi từng quản lý một hệ thống MySQL 8.0 với DB khoảng 50GB, phục vụ hơn 2.000 đơn hàng mỗi phút. Có những lúc, vài câu lệnh Update tưởng chừng đơn giản lại khiến toàn bộ luồng xử lý bị treo cứng. Deadlock không phải lỗi cú pháp. Nó là trạng thái xung đột tài nguyên khi các transaction giằng co nhau. Thay vì hoảng loạn restart service, việc biết cách đọc log sẽ giúp bạn giải quyết gốc rễ vấn đề.
Nói nôm na: Deadlock là gì?
Hãy tưởng tượng tình huống “ông chờ tôi, tôi chờ ông”. Hai giao dịch (transaction) đang chạy song song và rơi vào thế kẹt:
- Transaction A: Đang giữ khóa (lock) ở dòng 1 và muốn cập nhật tiếp dòng 2.
- Transaction B: Đang giữ khóa ở dòng 2 và muốn cập nhật ngược lại dòng 1.
Cả hai bên đều không chịu nhả khóa và đứng đợi nhau mãi mãi. Lúc này, “trọng tài” InnoDB sẽ ra tay. Nó chủ động hủy một transaction (thường là cái ít thay đổi nhất) để giải phóng tài nguyên. Đó là lý do bạn thấy lỗi restarting transaction xuất hiện trong log ứng dụng.
Cách “bắt mạch” bằng InnoDB Status
Khi lỗi xảy ra, câu lệnh đầu tiên tôi gõ trên terminal luôn là:
SHOW ENGINE INNODB STATUS;
Lệnh này trả về một file text dài dằng dặc. Đừng bối rối, hãy cuộn ngay xuống phần LATEST DETECTED DEADLOCK. Đây chính là hiện trường vụ án với đầy đủ manh mối.
Phân tích log thực tế
Dưới đây là một đoạn log từ một bug thực tế tôi từng xử lý:
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 5 sec
UPDATE orders SET status = 'processing' WHERE id = 10;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 index PRIMARY of table `shop`.`orders` trx id 12345 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 3 sec
UPDATE orders SET status = 'shipped' WHERE id = 5;
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 50 index PRIMARY of table `shop`.`orders` trx id 12346 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 index PRIMARY of table `shop`.`orders` trx id 12346 lock_mode X locks rec but not gap waiting
Chúng ta đọc được gì từ đây?
- Transaction (1) đang đợi khóa trên dòng có
id = 10. - Transaction (2) lại đang nắm giữ (HOLDS) chính cái khóa mà Transaction (1) cần.
- Ngược lại, (2) cũng đang đợi một khóa khác từ (1).
Nguyên nhân thường do thứ tự cập nhật dữ liệu không đồng nhất. Một luồng xử lý từ 10 đến 5, luồng kia lại đi ngược từ 5 về 10.
5 bí kíp “xương máu” để chặn đứng Deadlock
Sau nhiều đêm thức trắng fix bug, tôi rút ra 5 quy tắc giúp database chạy êm ru ngay cả khi tải cao.
1. Luôn cập nhật theo một thứ tự cố định
Đây là quy tắc vàng. Nếu cần update nhiều record (ví dụ trừ kho 3 sản phẩm), hãy luôn sắp xếp chúng theo ID tăng dần trước khi thực thi câu lệnh SQL.
# Code an toàn: Luôn sort ID trước khi update
ids = sorted([10, 5, 8])
for id in ids:
cursor.execute(f"UPDATE products SET stock = stock - 1 WHERE id = {id}")
Khi cả hai transaction cùng muốn update ID 5 và 10, chúng sẽ cùng tranh nhau khóa ID 5 trước. Thắng thua sẽ rõ ràng, không còn chuyện đứng chờ nhau chéo cánh sẻ.
2. Rút ngắn thời gian thực thi Transaction
Đừng bao giờ nhồi nhét logic nặng, gọi API bên thứ ba hoặc gửi email vào trong một Database Transaction. Transaction càng kéo dài, thời gian giữ lock càng lâu. Điều này làm tăng xác suất đụng độ với các luồng khác lên gấp nhiều lần.
3. Tối ưu Index để tránh quét toàn bảng
Nếu câu lệnh UPDATE thiếu Index, MySQL phải quét toàn bộ bảng (Table Scan). Thay vì khóa một dòng, nó sẽ khóa cả một dải dữ liệu (Gap Lock). Hãy đảm bảo mọi câu lệnh thay đổi dữ liệu đều chạy dựa trên Primary Key hoặc Unique Index.
4. Hạ thấp Isolation Level nếu có thể
Mặc định MySQL dùng REPEATABLE READ với cơ chế Gap Locking rất chặt chẽ. Nếu ứng dụng của bạn không quá khắt khe về tính nhất quán hoàn hảo, hãy thử chuyển sang READ COMMITTED:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Trong thực tế, thay đổi này giúp tôi giảm tới 80% lỗi Deadlock liên quan đến Gap Lock trên các hệ thống thương mại điện tử.
5. Triển khai cơ chế Retry ở tầng ứng dụng
Deadlock là thứ khó tránh khỏi 100% khi hệ thống đạt ngưỡng hàng nghìn request mỗi giây. Thay vì trả về lỗi cho người dùng, hãy viết một đoạn logic nhỏ để tự động thực hiện lại giao dịch.
max_retries = 3
for i in range(max_retries):
try:
# Thực hiện logic DB tại đây
db.commit()
break
except Exception as e:
if "Deadlock found" in str(e):
time.sleep(0.1) # Nghỉ 100ms rồi thử lại
continue
raise e
Lời kết
Xử lý Deadlock không chỉ đơn thuần là sửa câu SQL. Nó đòi hỏi bạn hiểu cách dữ liệu được truy cập và tranh chấp. Hãy nhớ: đọc log bằng SHOW ENGINE INNODB STATUS, chuẩn hóa thứ tự update và luôn có phương án retry. Hy vọng kinh nghiệm này giúp bạn tự tin hơn khi đối mặt với những pha “treo hệ thống” bất ngờ.

