Đang tải nội dung…
Đang tải nội dung…
Ben tách consistent read khỏi locking read và đo chính khoảng trống giữa hai index record
Transaction, Isolation & Locking · Phần 5Fortex, Tom, Ben và cuộc trao đổi dưới đây là hư cấu. Số đo 1.283 ms và 116 ms đến từ Docker Lab được dẫn trong bài, không phải latency production.
Hành trình ở Fortex · Chương 21
Hà giao Ben xử lý chuyện hai request cùng đặt chỗ trong một dải ghế. Ben đã đi qua lost update và deadlock, nên cậu không còn bắt đầu bằng câu “thêm transaction là xong”. Cậu đề nghị khóa range còn trống trước khi chèn:
SELECT id FROM seats
WHERE id BETWEEN 10 AND 20
FOR UPDATE;
“Nếu ID 15 chưa tồn tại thì chẳng có row 15 nào để khóa,” Ben nói, rồi tự dừng lại. “Nhưng nếu không khóa được khoảng trống, một transaction khác có thể chèn 15 và làm điều kiện của em đổi ngay trong transaction.”
Tom để Ben nói hết. “Em đang muốn giữ ổn định tập kết quả, không chỉ những row đang tồn tại. Mình đo xem InnoDB biểu diễn lời hứa đó bằng gì nhé.”
Trong InnoDB, row lock thực chất gắn với index record. MySQL 8.4 mô tả next-key lock là record lock cộng gap lock trên khoảng trước record đó. Với index chứa 10 và 20, một range locking read có thể bảo vệ cả khoảng không có row 15.
Điểm Ben ghi lên bảng là: missing row không có nghĩa missing lock. Lock có thể nằm trên khoảng index nơi row mới muốn xuất hiện.
Tài liệu locks set by SQL statements phân biệt hai trường hợp quan trọng:
REPEATABLE READ.Vì vậy query plan và index không chỉ ảnh hưởng tốc độ. Chúng còn xác định phạm vi lock. Một query thiếu index phù hợp có thể quét và khóa rộng hơn điều người viết câu SQL tưởng.
Ben thử diễn giải: “REPEATABLE READ chặn INSERT nên em không thấy phantom.”
Tom chưa phản bác. Anh đặt hai câu cạnh nhau:
SELECT COUNT(*) FROM seats WHERE id BETWEEN 10 AND 20;
SELECT id FROM seats WHERE id BETWEEN 10 AND 20 FOR UPDATE;
Câu đầu là consistent read: nó đọc snapshot và thông thường không đặt row lock. Câu sau là locking read: nó đọc trạng thái mới nhất phù hợp với việc cấp lock và có thể chặn insert vào range. Cùng isolation level nhưng hai cơ chế bảo vệ khác nhau.
Ở REPEATABLE READ, hai consistent read trong cùng transaction có thể đều trả 2 dù transaction khác đã chèn row thứ ba; snapshot che phantom khỏi reader. Ở READ COMMITTED, mỗi statement có snapshot mới nên lần đọc sau có thể thấy 3. Điều đó tách biệt với câu hỏi locking range có chặn writer hay không.
Ben sửa câu trên bảng: “Snapshot giữ góc nhìn ổn định. Next-key lock giữ range khỏi bị thay đổi bởi writer khác.”
Lab chạy MySQL 8.4.7, InnoDB mặc định REPEATABLE-READ, bảng có hai primary key 10 và 20.
Kịch bản đầu giữ SELECT ... BETWEEN 10 AND 20 FOR UPDATE trong khoảng 1,6 giây. Một session khác chèn ID 15 và phải chờ 1.283 ms. Control chỉ khóa id=10 bằng unique equality; insert 15 hoàn thành trong 116 ms. Các con số chỉ chứng minh blocking tương đối trong lịch chạy đã kiểm soát.
| Kịch bản | INSERT 15 chờ |
|---|---|
| Range locking read | 1.283 ms |
| Unique equality control | 116 ms |
Lab sau đó chạy consistent read riêng:
| Isolation | COUNT lần 1 | COUNT lần 2 |
|---|---|---|
| REPEATABLE READ | 2 | 2 |
| READ COMMITTED | 2 | 3 |
d3ab5f00-715e-482b-b57f-39838caa95ceLab không đo SSD, throughput hay p99. Nó dùng một container, lịch sleep xác định và kiểm tra consistent read tách khỏi locking read.
READ COMMITTED giảm nhiều trường hợp gap locking và tăng concurrency, nhưng consistent read có thể thấy tập kết quả đổi giữa hai statement. Đó có thể là đúng nếu ứng dụng đã dùng unique constraint, conditional write hoặc retry cho invariant cần bảo vệ. Nó không phải công tắc “tắt lock mà vẫn giữ mọi bảo đảm cũ”.
SERIALIZABLE cho mô hình mạnh hơn nhưng có thêm blocking hoặc abort. SKIP LOCKED phù hợp cho work queue nơi bỏ qua job đang được worker khác giữ là đúng nghiệp vụ; dùng nó cho đặt chỗ có thể âm thầm bỏ qua dữ liệu cần xét.
Một unique constraint thường bảo vệ invariant “không trùng key” rõ hơn việc khóa một range rộng. Nhưng invariant kiểu “không quá N booking trong khoảng” vẫn cần thiết kế transaction, counter có điều kiện hoặc serializable/retry phù hợp.
Từ bài deadlock, Ben nhớ rằng log engine và wait-for graph đáng tin hơn trực giác. Cậu mở performance_schema.data_locks; MySQL ghi rõ cột LOCK_MODE có thể biểu diễn gap và LOCK_STATUS cho biết GRANTED hay WAITING.
Cậu để lại checklist review:
data_locks, thay vì chỉ nhìn query text không?Tom nhìn bảng rồi hỏi: “Nếu lock tự hết hạn thì owner cũ còn quyền ghi không?”
Ben nhận ra đó không còn là câu hỏi của MySQL. Nó là chiếc cầu sang bài tiếp theo: một Redis lease có thể hết hạn trong lúc worker vẫn chạy.
MySQL 8.4.7 InnoDB · digest pinned · isolated Docker
Tải source Docker Lab (.zip)COMMIT 165fa77561ad172dda1a6ff562132589b3ee22c4
Đọc kỹ README trước khi chạy. Các lệnh thực nghiệm chuẩn trong Docker Lab:
make setupmake seedmake reproducemake verifymake cleanupMôi trường Docker độc lập, giới hạn tài nguyên và tự động dọn dẹp sau khi chạy. Số liệu trong bài viết là kết quả lần chạy chính thức; mỗi lần chạy mới đều lưu lịch sử riêng.