Tại sao bạn không thể ngó lơ cơ chế Locking?
Hãy tưởng tượng bạn đang vận hành một app đặt vé máy bay. Chỉ còn đúng một ghế cuối cùng, nhưng lại có hai khách hàng cùng nhấn “Đặt chỗ” vào một thời điểm. Nếu thiếu cơ chế khóa (Locking), database có thể ghi nhận cả hai đều thành công. Kết quả là một thảm họa về dữ liệu và trải nghiệm khách hàng.
Hồi mình còn quản lý hệ thống e-commerce chạy MySQL 8.0 với traffic khoảng 500 transaction/giây, mình từng chủ quan nghĩ rằng dùng Transaction là đủ. Thế nhưng khi lượng user tăng vọt vào đợt Flash Sale, log bắt đầu tràn ngập lỗi “Deadlock found”. Hệ thống chậm dần rồi đứng hẳn. Việc hiểu rõ cách InnoDB khóa dữ liệu sẽ giúp bạn viết code an toàn hơn. Đồng thời, nó còn giúp tối ưu đáng kể khả năng xử lý đồng thời (concurrency) cho hệ thống.
Thử nghiệm thực tế trong 5 phút
Để thấy tận mắt cơ chế khóa, bạn hãy mở hai cửa sổ terminal (Session A và Session B). Chúng ta sẽ tạo một bảng đơn giản để test ngay:
CREATE TABLE inventory (
id INT PRIMARY KEY,
item_name VARCHAR(50),
stock INT
) ENGINE=InnoDB;
INSERT INTO inventory VALUES (1, 'iPhone 15', 10), (5, 'Samsung S24', 5), (10, 'Sony A7IV', 2);
Giờ hãy thực hiện theo kịch bản sau:
Session A: Bắt đầu giao dịch và giữ dòng id=5.
START TRANSACTION;
UPDATE inventory SET stock = stock - 1 WHERE id = 5;
-- Lúc này Session A đã chiếm quyền kiểm soát dòng id=5.
Session B: Cố gắng can thiệp vào dòng id=5.
START TRANSACTION;
UPDATE inventory SET stock = stock - 1 WHERE id = 5;
-- Session B sẽ bị treo (waiting). Nó phải đợi Session A nhả khóa mới được chạy tiếp.
Ngay khi bạn gõ COMMIT; ở Session A, Session B sẽ được giải phóng và thực hiện lệnh ngay lập tức. Đây chính là Row Lock – dạng khóa cơ bản nhất.
Giải mã 3 loại khóa “quyền lực” trong InnoDB
Nhiều dev lầm tưởng MySQL chỉ khóa đúng dòng đang sửa. Thực tế phức tạp hơn nhiều, nhất là với isolation level mặc định REPEATABLE READ.
1. Record Lock (Khóa bản ghi)
Đây là loại khóa trực tiếp trên các chỉ mục (index). Khi bạn chạy SELECT * FROM table WHERE id = 10 FOR UPDATE;, MySQL khóa chặt dòng có ID 10. Không ai có thể sửa hay xóa dòng này cho đến khi bạn kết thúc transaction.
2. Gap Lock (Khóa khoảng cách)
Gap Lock không khóa một dòng cụ thể mà khóa khoảng trống giữa các index. Đây là khái niệm thường gây nhầm lẫn nhất.
Ví dụ bảng inventory của chúng ta có các ID: 1, 5, 10. Khoảng cách giữa 1 và 5 là (2, 3, 4). Khoảng cách giữa 5 và 10 là (6, 7, 8, 9).
Nếu bạn chạy: SELECT * FROM inventory WHERE id BETWEEN 2 AND 4 FOR UPDATE;, dù không có ID nào là 2, 3, 4, MySQL vẫn khóa toàn bộ khoảng đó. Mục đích là để ngăn Session khác INSERT thêm dòng mới có ID = 3. Cơ chế này giúp triệt tiêu hiện tượng “Phantom Read” (đọc bóng ma).
3. Next-Key Lock
Next-Key Lock là sự kết hợp giữa Record Lock và Gap Lock. Nó khóa chính bản ghi đó kèm theo khoảng trống ngay phía trước nó.
Trong thực tế, InnoDB thường dùng Next-Key Lock khi quét index để đảm bảo dữ liệu không bị thay đổi bất ngờ. Điều này giải thích tại sao đôi khi bạn update một dòng không tồn tại nhưng vẫn khiến lệnh insert của người khác bị treo.
Cảnh báo: Đừng để mất Index khi Lock
Đây là bài học xương máu của mình. InnoDB chỉ thực hiện khóa ở cấp độ dòng (Row-level) nếu bạn sử dụng Index trong câu lệnh WHERE.
Thử tưởng tượng bạn chạy lệnh update trên một cột không có index:
-- Giả sử item_name không có index
UPDATE inventory SET stock = 0 WHERE item_name = 'iPhone 15';
Lúc này, MySQL buộc phải quét toàn bộ bảng (Full Table Scan). Hệ quả là nó sẽ khóa mọi dòng trong bảng! Với database lớn, hành động này chẳng khác nào tự tay rút phích cắm server, khiến mọi giao dịch khác phải xếp hàng chờ vô tận.
Làm sao để kiểm tra tình trạng khóa?
Nếu thấy hệ thống bỗng dưng chậm chạp, mình thường dùng lệnh sau để truy tìm “hung thủ” đang giữ khóa:
SELECT * FROM information_schema.innodb_trx;
-- Hoặc xem báo cáo chi tiết về engine
SHOW ENGINE INNODB STATUS;
4 quy tắc vàng để tối ưu Concurrency
Sau nhiều lần xử lý sự cố trên hệ thống dữ liệu lớn, mình rút ra các nguyên tắc bất di bất dịch:
- Transaction phải cực ngắn: Đừng bao giờ gọi API bên thứ ba hoặc xử lý ảnh nặng nề bên trong một transaction. Hãy tính toán xong xuôi, mở transaction, update rồi commit ngay.
- Luôn bám sát Index: Mọi câu lệnh
UPDATE,DELETEphải đi kèm với Primary Key hoặc Index để tránh khóa nhầm toàn bảng. - Thứ tự thao tác nhất quán: Nếu Transaction A sửa dòng 1 rồi đến dòng 2, thì Transaction B cũng phải theo đúng thứ tự đó. Nếu B sửa ngược lại (2 rồi đến 1), Deadlock chắc chắn sẽ xảy ra.
- Chọn Isolation Level phù hợp: Với các tính năng không cần chính xác 100% như đếm view, hãy cân nhắc dùng
READ COMMITTED. Việc này giúp giảm bớt Gap Lock và tăng tốc độ xử lý rõ rệt.
Lời kết
Làm chủ cơ chế khóa trong MySQL cũng giống như việc bạn hiểu luật giao thông khi lái xe. Ban đầu có thể rắc rối, nhưng khi đã nắm vững, bạn sẽ tự tin thiết kế những hệ thống chịu tải cao mà không lo dữ liệu bị sai lệch hay hệ thống bị treo cứng.
Nếu bạn đang đau đầu vì query chậm, hãy xem thêm bài viết về Slow Query Log của mình để có cái nhìn toàn diện hơn về tối ưu database nhé!

