MVCC (Kiểm soát đồng thời đa phiên bản) và giao dịch dựa trên snapshot
1. Tổng quan
Định nghĩa: MVCC (Multi-Version Concurrency Control, kiểm soát đồng thời đa phiên bản) là kỹ thuật kiểm soát đồng thời trong đó, khi cập nhật một dòng, giá trị cũ không bị ghi đè ngay mà được lưu giữ dưới dạng nhiều phiên bản; mỗi giao dịch xác định phiên bản nào nó được phép thấy trong snapshot (ảnh chụp dữ liệu) của mình, qua đó giảm xung đột giữa đọc và ghi.
Cơ sở dữ liệu của các dịch vụ trực tuyến có nhiều giao dịch chạy đồng thời cùng đọc và sửa một dòng. Nếu chỉ dùng khóa đơn thuần thì dễ giữ nhất quán khi ghi, nhưng yêu cầu đọc phải chờ khóa cập nhật khiến độ trễ tăng, và truy vấn phân tích có thể chặn giao dịch nghiệp vụ. MVCC tách xung đột này theo nguyên tắc “giao dịch đọc thì đọc phiên bản quá khứ mà nó được phép thấy, giao dịch ghi thì tạo phiên bản mới”.
Cốt lõi của MVCC là không coi dữ liệu chỉ có một giá trị hiện tại duy nhất. Về logic đó là một dòng, nhưng về vật lý có thể tồn tại nhiều bản ghi kèm theo giao dịch tạo, giao dịch hết hiệu lực và thông tin liên kết tới phiên bản trước. Yêu cầu đọc không trả về một bản ghi chỉ vì nó mới nhất, mà chọn phiên bản đã được commit và hiển thị trong snapshot của mình. Chính quá trình xác định này tạo nên tính cô lập và khả năng đọc không chặn.
Tuy nhiên MVCC không phải là công nghệ loại bỏ khóa. Xung đột cập nhật, ràng buộc duy nhất, đọc có khóa tường minh và kiểm chứng ở mức tuần tự hóa vẫn cần khóa hoặc phát hiện xung đột. Ngoài ra, nếu một giao dịch mở quá lâu cản trở việc dọn dẹp phiên bản cũ, dung lượng lưu trữ sẽ tăng và hiệu năng chỉ mục cũng như bảng sẽ giảm. Vì vậy MVCC chỉ phát huy hiệu quả khi thiết kế đồng thời cả cài đặt nội bộ của storage engine và chính sách vận hành giao dịch.
Trong bài thi Kỹ sư chuyên nghiệp (Professional Engineer), chỉ mô tả MVCC là “tính năng giúp cơ sở dữ liệu nhanh” là chưa đủ. Cần liên kết đến việc chọn snapshot nào, phiên bản trước và sau commit hiển thị ra sao, mỗi mức cô lập cho phép hiện tượng nào, và khi nào phiên bản cũ được thu hồi. Đặc biệt, khả năng hiển thị tuple và VACUUM của PostgreSQL khác với undo log và Read View của InnoDB về cách cài đặt, nên cần phân biệt nguyên lý chung với khác biệt theo từng sản phẩm.
1.1 Bối cảnh ra đời và sự cần thiết
2PL (Two-Phase Locking) truyền thống đặt khóa lên dữ liệu mà giao dịch đọc hoặc cập nhật để kiểm soát truy cập của giao dịch khác. Cách này dễ đạt nhất quán mạnh, nhưng khi khóa chia sẻ và khóa độc quyền chồng lên nhau thì đọc và ghi phải chờ nhau. Trong dịch vụ web quy mô lớn, một cập nhật đơn hàng ngắn có thể bị trễ chỉ vì một truy vấn báo cáo chạy lâu.
MVCC gán cho thao tác đọc một thời điểm logic của dữ liệu. Khi giao dịch A đang cập nhật một dòng mà giao dịch B truy vấn, B không đọc giá trị mới mà A chưa commit, mà đọc phiên bản trước nằm trong snapshot. Kết quả là tránh được “đọc bẩn” (dirty read) mà không chặn nhau một cách không cần thiết giữa các thao tác đọc đơn giản.
Cách tiếp cận này đặc biệt có lợi cho OLTP có tỷ lệ đọc cao, xử lý đồng thời đơn hàng và thanh toán, phân tích trên bản sao (replica) và truy vấn báo cáo dài. Ngược lại, trong môi trường có xung đột cập nhật rất nhiều, giao dịch dài xảy ra thường xuyên hoặc dung lượng lưu trữ hạn chế, cần đánh giá chi phí tạo và dọn dẹp phiên bản.
1.2 Mục tiêu cốt lõi
Mục tiêu thứ nhất là giúp giao dịch có được một thời điểm quan sát nhất quán. Giao dịch không tùy tiện nhìn thấy giá trị mà giao dịch khác đang ghi dở, mà xác định dòng dựa trên tập các thời điểm commit mà nó chấp nhận.
Mục tiêu thứ hai là giảm tranh chấp giữa đọc và ghi. Đọc không khóa thông thường sử dụng phiên bản quá khứ, nên kể cả khi bên cập nhật đang giữ khóa ghi, thao tác đọc không nhất thiết phải chờ. Tuy nhiên đặc tính này không áp dụng nguyên vẹn cho nghiệp vụ cần giá trị đã xác nhận mới nhất hoặc cần khóa mạnh.
Mục tiêu thứ ba là mở rộng phạm vi lựa chọn giữa mức cô lập và hiệu năng. READ COMMITTED có thể lấy thời điểm quan sát mới cho từng câu lệnh, REPEATABLE READ có thể giữ cùng một snapshot trong suốt giao dịch. SERIALIZABLE hướng tới kết quả tương đương thực thi tuần tự bằng cách kiểm chứng xung đột bổ sung.
2. Thành phần và nguyên lý hiển thị của MVCC
2.1 Cấu trúc hoạt động tổng thể
MVCC là cấu trúc kết hợp ứng dụng, bộ quản lý giao dịch, storage engine, vùng lưu phiên bản và tác vụ dọn dẹp. Khi ứng dụng thực thi SQL, bộ quản lý giao dịch tạo snapshot phù hợp với ID và mức cô lập của giao dịch hiện tại. Storage engine lần theo các phiên bản trước liên kết với dòng hiện tại để tìm giá trị hiển thị trong snapshot.
flowchart LR
A[SQL của ứng dụng] --> B[Bộ quản lý giao dịch]
B --> C[ID giao dịch·mức cô lập]
B --> D[Snapshot/Read View]
A --> E[Storage engine]
E --> F[Phiên bản dòng hiện tại]
F --> G{Xác định khả năng hiển thị}
D --> G
G -- Thấy --> H[Trả kết quả]
G -- Không thấy --> I[Duyệt phiên bản trước·Undo]
I --> G
J[Trạng thái commit·rollback] --> G
K[Tác vụ dọn dẹp] --> L[Thu hồi phiên bản không còn cần]
L -. Kiểm tra ranh giới lưu giữ .-> B
ID giao dịch là căn cứ để nhận diện chủ thể tạo và sửa phiên bản. Tùy cách cài đặt, header của dòng, bảng giao dịch, undo log và thông tin dòng thời gian được dùng cùng nhau. Điểm quan trọng là chỉ riêng giá trị của phiên bản thì không thể quyết định khả năng hiển thị. Phải kiểm tra thêm phiên bản được tạo bởi giao dịch nào và giao dịch đó đã commit tại thời điểm snapshot hay chưa.
Snapshot là chuẩn đọc phân biệt “các giao dịch được thấy” và “các giao dịch chưa được thấy” tại một thời điểm. Dù dòng hiện tại đã bị một giao dịch sau đó sửa, nếu giao dịch sửa đó không hiển thị trong snapshot thì storage engine sẽ lần theo phiên bản trước để trả về. Vì thế một phiên bản không mới nhất về vật lý lại là kết quả đúng về logic.
2.2 Phiên bản và xác định khả năng hiển thị
Phiên bản dòng thường có metadata về thời điểm tạo và thời điểm hết hiệu lực. Nếu giao dịch tạo phiên bản mới đã commit và giao dịch làm hết hiệu lực phiên bản đó không hiển thị trong snapshot, phiên bản đó trở thành đối tượng đọc. Ngược lại, nếu giao dịch tạo vẫn đang chạy thì phiên bản đó bị loại khỏi thao tác đọc thông thường của các giao dịch khác.
Việc một giao dịch có đọc được giá trị do chính nó sửa hay không thuộc quy tắc hiển thị của engine. Thay đổi tạo ra ở câu lệnh trước của chính nó có thể được thấy như kết quả công việc logic của cùng giao dịch, bất kể đã commit ra bên ngoài hay chưa. Nếu không có quy tắc này, luồng tự nhiên như INSERT rồi SELECT trong cùng một giao dịch sẽ bị phá vỡ.
Xóa cũng có thể được xử lý bằng đánh dấu xóa hoặc tạo phiên bản mới thay vì loại bỏ vật lý ngay lập tức. Nếu giao dịch xóa không hiển thị trong snapshot thì phiên bản trước vẫn đọc được; nếu giao dịch xóa hiển thị thì dòng đó bị loại khỏi kết quả. Sau đó, khi không còn giao dịch hoạt động nào có khả năng thấy phiên bản trước, tác vụ dọn dẹp mới thực sự thu hồi không gian.
| Thành phần | Vai trò | Điểm thiết kế·vận hành |
|---|---|---|
| ID giao dịch | Nhận diện chủ thể tạo·sửa phiên bản | Xét cạn kiệt ID·wraparound và theo dõi trạng thái |
| Snapshot/Read View | Phân biệt commit được thấy và giao dịch đang chạy | Thời điểm tạo khác nhau theo mức cô lập |
| Phiên bản dòng | Lưu giữ giá trị quá khứ·hiện tại về logic | Quản lý độ dài chuỗi phiên bản và dung lượng |
| Vùng Undo/phiên bản trước | Cung cấp giá trị quá khứ để đọc và thông tin rollback | Giám sát thời gian lưu giữ, I/O, độ trễ dọn dẹp |
| Bộ xác định hiển thị | Chọn phiên bản phù hợp snapshot | Kiểm tra header·trạng thái giao dịch theo engine |
| Tác vụ dọn dẹp | Thu hồi phiên bản không còn cần | Vận hành sao cho không xung đột với giao dịch dài |
Các thành phần trong bảng không phải danh sách chức năng độc lập mà là một vòng đời. Giao dịch tạo phiên bản, snapshot đọc phiên bản đó, và chỉ sau khi các giao dịch hoạt động kết thúc thì tác vụ dọn dẹp mới có thể xóa an toàn phiên bản cũ. Nếu một bước bị trễ, dung lượng lưu trữ và thời gian phản hồi sẽ chịu ảnh hưởng dây chuyền.
2.3 Luồng logic của đọc và ghi
Giao dịch đọc trước hết lấy snapshot phù hợp với mức cô lập của mình. Sau đó tìm các dòng ứng viên từ chỉ mục hoặc bảng và so sánh metadata tạo·xóa của phiên bản ứng viên với snapshot. Nếu ứng viên không hiển thị, nó chuyển sang vùng lưu phiên bản trước, cho tới khi tìm được phiên bản hiển thị hoặc xác định dòng đó không tồn tại trong snapshot.
Giao dịch ghi, dù thay đổi dòng hiện có, vẫn lưu giữ trạng thái quá khứ mà các giao dịch đọc có thể tham chiếu. Tùy storage engine, có cách đặt phiên bản mới thành bản ghi riêng, hoặc giữ bản ghi hiện tại và ghi giá trị trước vào vùng undo. Khác biệt này ảnh hưởng đến chi phí cập nhật chỉ mục, cách dọn dẹp và quy trình phục hồi sự cố.
Commit là ranh giới khiến phiên bản có thể hiển thị trong snapshot của giao dịch khác. Rollback vô hiệu hóa phiên bản mới hoặc dùng thông tin undo để khôi phục trạng thái logic trước đó. Do đó MVCC không phải chức năng riêng của tệp dữ liệu mà hoạt động cùng WAL·redo·undo và quản lý trạng thái giao dịch.
3. Mức cô lập và các hiện tượng bất thường khi đồng thời
3.1 Ý nghĩa của mức cô lập
Mức cô lập định nghĩa giao dịch đang chạy đồng thời được quan sát kết quả của giao dịch khác tới mức nào. Dù cùng tên, cách cài đặt và giá trị mặc định có thể khác nhau giữa các sản phẩm, nên tài liệu thiết kế cần ghi cả thuật ngữ chuẩn lẫn cấu hình thực tế của engine.
READ UNCOMMITTED có thể đọc cả thay đổi chưa commit nên là mức cô lập yếu nhất. Có những engine MVCC không cho phép nguyên trạng điều này ở truy vấn thông thường hoặc xử lý đặc biệt, vì vậy không nên khẳng định “đã là MVCC thì luôn không có đọc bẩn” mà cần kiểm tra tài liệu sản phẩm.
READ COMMITTED là mô hình lấy snapshot mới khi mỗi câu lệnh SQL bắt đầu hoặc tại thời điểm thực thi. Nếu truy vấn hai lần trong cùng một giao dịch, lần thứ hai có thể thấy thay đổi mà giao dịch khác commit ở giữa. Tính cập nhật của màn hình nghiệp vụ cao, nhưng quy tắc nghiệp vụ kết hợp nhiều câu lệnh có thể cần khóa bổ sung hoặc kiểm tra điều kiện.
REPEATABLE READ là mô hình duy trì thời điểm quan sát nhất quán của giao dịch. Cùng một truy vấn logic trong một giao dịch dễ cho kết quả nhất quán, nhưng giao dịch mở lâu đòi hỏi giữ phiên bản quá khứ lâu. MySQL InnoDB và PostgreSQL, dù cùng tên mức cô lập, có thể khác nhau về hiện tượng chi tiết và cách cài đặt.
SERIALIZABLE là mức mạnh nhất, giới hạn để kết quả thực thi đồng thời giống với một thứ tự thực thi tuần tự nào đó. Có cách cài đặt như SSI (Serializable Snapshot Isolation) dựa trên MVCC phát hiện các phụ thuộc nguy hiểm trong khi giảm thiểu khóa, cũng có cách kết hợp với khóa phạm vi. Khi xung đột phải thử lại, nên tính lũy đẳng (idempotency) và chính sách thử lại của ứng dụng là bắt buộc.
| Mức cô lập | Hiện tượng có thể được cho phép | Đặc điểm từ góc nhìn MVCC | Ví dụ áp dụng |
|---|---|---|---|
| READ UNCOMMITTED | Đọc bẩn, v.v. | Nhất quán yếu nhất, xử lý khác nhau theo engine | Truy vấn tham khảo nhanh hơn là chính xác |
| READ COMMITTED | Đọc không lặp lại, một phần phantom | Snapshot theo câu lệnh | Yêu cầu web thông thường·OLTP |
| REPEATABLE READ | Tùy cài đặt: phantom·xung đột ghi | Snapshot theo giao dịch hoặc đọc nhất quán | Truy vấn nhất quán theo đơn vị nghiệp vụ |
| SERIALIZABLE | Không cho phép vi phạm tính tuần tự | Phát hiện xung đột·bảo vệ phạm vi·thử lại | Tồn kho·quyết toán·quy tắc cốt lõi |
Khi ghi nhớ bảng này, đừng chỉ thuộc tên hiện tượng mà hãy liên kết với khác biệt về thời điểm tạo snapshot và cách xử lý xung đột ghi. Ví dụ, hai câu SELECT trong READ COMMITTED đọc ra giá trị khác nhau không phải là lỗi mà có thể vì mỗi câu lệnh tạo thời điểm quan sát mới. Ngược lại, nghiệp vụ như hạn mức thanh toán, phán đoán dựa trên tổng hợp nhiều kết quả đọc, cần một snapshot duy nhất hoặc khóa tường minh.
3.2 Các bất thường đồng thời tiêu biểu
Đọc bẩn là hiện tượng đọc giá trị mà giao dịch khác chưa commit. Khả năng hiển thị snapshot cơ bản của MVCC loại trừ phiên bản của giao dịch đang chạy nên giảm được hiện tượng này. Tuy nhiên, nếu cache bên ngoài hoặc replica đọc có quy tắc nhất quán riêng, thì dù trong cơ sở dữ liệu không có đọc bẩn, màn hình người dùng vẫn có thể xuất hiện ảo giác tương tự.
Đọc không lặp lại là hiện tượng trong cùng một giao dịch đọc với cùng điều kiện nhưng giá trị thay đổi do giao dịch khác commit. Nó có thể xuất hiện tự nhiên trong READ COMMITTED dùng snapshot theo câu lệnh. Nếu cần đọc tổng, số dư, quyền nhiều lần để so sánh thì nên đọc một lần hoặc chọn mức cô lập mạnh hơn.
Đọc bóng ma (phantom read) là hiện tượng khi lặp lại truy vấn phạm vi với cùng điều kiện, tập dòng kết quả thay đổi do có dòng mới được chèn hoặc bị xóa. Chỉ quản lý phiên bản của từng dòng thì không thể ngăn được thay đổi của cả phạm vi. Cần bảo vệ phạm vi phù hợp với nghiệp vụ như mức tuần tự hóa, khóa phạm vi, predicate locking, ràng buộc duy nhất.
Write skew (lệch ghi) là hiện tượng hai giao dịch đọc các dòng khác nhau và mỗi bên sửa một dòng, nhưng kết quả gộp hai thay đổi phá vỡ bất biến nghiệp vụ. Ví dụ, với quy tắc phải còn ít nhất một trong hai bác sĩ trực, nếu hai giao dịch lần lượt cho hai bác sĩ khác nhau tan ca thì cả hai đều có thể tan ca. Chỉ đọc nhất quán của MVCC đơn thuần không giải quyết được; cần SERIALIZABLE, khóa tường minh hoặc cập nhật có điều kiện mang tính nguyên tử.
4. Các cài đặt chính và cơ chế vận hành
4.1 Phiên bản tuple và VACUUM trong họ PostgreSQL
PostgreSQL không coi UPDATE là ghi đè giá trị của tuple hiện có mà xử lý bằng cách tạo phiên bản tuple mới. Nó so sánh thông tin giao dịch trong header tuple với snapshot để quyết định phiên bản nào hiển thị, và tuple trước trở thành đối tượng dọn dẹp khi không còn giao dịch hoạt động nào cần.
Cấu trúc này giảm xung đột đọc-ghi nhưng có thể khiến tuple chết (dead tuple) tích tụ trong bảng. VACUUM đánh dấu các tuple không còn hiển thị là có thể tái sử dụng và quản lý metadata giao dịch. Nếu VACUUM quá trễ sẽ xảy ra phình bảng, phình chỉ mục và tăng chi phí quét.
Autovacuum thực hiện dọn dẹp dựa trên lượng thay đổi và ngưỡng của từng bảng. Thay vì chỉ tin vào giá trị mặc định, cần quan sát đồng thời tần suất cập nhật, kích thước dòng, số chỉ mục, giao dịch dài và replication slot để tinh chỉnh theo từng bảng. Nếu giao dịch dài giữ snapshot quá khứ, VACUUM không thể thu hồi không gian, nên cũng phải kiểm tra connection pool của ứng dụng và các tác vụ batch.
Transaction ID wraparound không chỉ là vấn đề dung lượng mà liên quan đến tính an toàn của việc xác định hiển thị. Cơ sở dữ liệu freeze các transaction ID cũ một cách phù hợp để ý nghĩa của phiên bản quá khứ không bị thay đổi. Người vận hành cần giám sát định kỳ độ trễ autovacuum, oldest xmin, dead tuple và mức phình bảng·chỉ mục.
4.2 Undo log và Read View của InnoDB
InnoDB lưu giá trị trước khi thay đổi vào undo log, và khi cần đọc nhất quán thì dùng Read View cùng chuỗi undo để tái dựng phiên bản phù hợp với snapshot. Nếu giá trị trên trang hiện tại quá mới so với Read View của mình, nó lần theo undo log để tạo ra giá trị quá khứ.
Trong REPEATABLE READ, điều quan trọng là thao tác đọc nhất quán của giao dịch duy trì cùng một thời điểm quan sát. Trong READ COMMITTED, mỗi câu lệnh có thể tạo Read View mới hơn nên kết quả truy vấn có thể khác nhau ngay trong cùng giao dịch. Không nên nhầm khác biệt này với từ “giao dịch” ở tầng ứng dụng.
Việc dọn undo log được thực hiện tại thời điểm chưa có Read View hoạt động nào cần tới. Giao dịch mở lâu, batch lớn, quên trả kết nối có thể làm tăng không gian undo và độ trễ purge. Do đó dọn dẹp phiên bản vừa là vấn đề của tác vụ nền, vừa là vấn đề thiết kế ứng dụng để giữ ranh giới giao dịch ngắn.
4.3 Phân biệt đọc không khóa và đọc có khóa
SELECT thông thường có thể thực hiện đọc không khóa phù hợp với snapshot, nhưng các thao tác đọc kiểm tra phiên bản mới nhất và lấy khóa như SELECT ... FOR UPDATE hay SELECT ... FOR SHARE có mục đích khác. Khi bảo vệ số lượng ngay trước khi trừ tồn kho hoặc độc chiếm một dòng của hàng đợi công việc thì cần đọc có khóa.
Đọc không khóa có thể trả về phiên bản quá khứ, nên có thể không phù hợp với nghiệp vụ luôn cần “giá trị đã xác nhận mới nhất”. Ngược lại, nếu đổi mọi truy vấn sang đọc có khóa thì tính đồng thời mà MVCC mang lại sẽ lại mất đi do tranh chấp khóa. Cần định nghĩa ý nghĩa của truy vấn trước rồi mới quyết định có khóa hay không.
sequenceDiagram
participant T1 as T1 giao dịch cập nhật
participant DB as Storage engine MVCC
participant T2 as T2 giao dịch đọc
participant U as Undo/phiên bản trước
T1->>DB: Cập nhật dòng A từ 100 thành 120
DB->>U: Lưu giữ phiên bản trước 100
T2->>DB: Truy vấn dòng A với snapshot S
DB->>DB: Xác định thời điểm tạo·commit của phiên bản mới
DB->>U: Yêu cầu phiên bản trước phù hợp S
U-->>DB: Trả giá trị 100
DB-->>T2: Kết quả snapshot 100
T1->>DB: COMMIT
T2->>DB: Truy vấn bằng câu lệnh mới hoặc giao dịch mới
DB-->>T2: 120 hoặc 100 tùy mức cô lập
Luồng trên cho thấy kết quả khác nhau tùy vào thời điểm T2 tạo snapshot. Nếu trước khi T1 commit thì T2 không thể thấy 120, và ngay cả sau khi T1 commit, nếu mức cô lập của T2 giữ snapshot trước đó thì vẫn tiếp tục thấy 100. Do đó khi phân tích sự cố, không chỉ ghi thời điểm thực thi SQL mà phải ghi cả thời điểm bắt đầu giao dịch, tạo snapshot và commit.
4.4 Quan hệ giữa chỉ mục và dọn dẹp phiên bản
MVCC không chỉ liên quan đến thân bảng mà còn gắn với truy cập chỉ mục. Nếu tuple mà chỉ mục trỏ tới không hiển thị trong snapshot hiện tại, storage engine thực hiện kiểm tra hiển thị và tìm phiên bản trước nếu cần. Nếu thiết kế chỉ mục kém, số dòng ứng viên tăng khiến việc xác định phiên bản và I/O ngẫu nhiên cùng tăng.
Một số engine dùng metadata riêng hoặc visibility map để có thể xác nhận hiển thị chỉ bằng cách đọc chỉ mục. Để tối ưu này hoạt động đúng, tác vụ dọn dẹp và cập nhật thống kê phải bình thường. Vì vậy không phải “thêm chỉ mục thì chi phí MVCC biến mất” mà phải đánh giá đồng thời độ chọn lọc, covering và trạng thái dọn dẹp của chỉ mục.
5. So sánh và các trường hợp áp dụng trong công nghiệp
5.1 So sánh với 2PL và kiểm soát lạc quan
2PL đặt khóa lên dữ liệu có khả năng xung đột để trực tiếp giới hạn truy cập của giao dịch đang chạy. Cách này dễ biểu diễn quy tắc mạnh nhưng phải quản lý chờ khóa và bế tắc (deadlock). MVCC xử lý đường đọc bằng việc chọn phiên bản nên giảm chờ khi đọc, nhưng đòi hỏi lưu phiên bản cũ và thử lại khi xung đột ghi.
Kiểm soát đồng thời lạc quan làm việc không khóa với giả định xung đột hiếm, và kiểm tra số phiên bản hoặc timestamp khi commit. Nó có thể dùng cùng MVCC nhưng không phải cùng một khái niệm. MVCC là cơ chế cung cấp phiên bản để đọc, còn kiểm tra lạc quan là chính sách phán đoán khả năng commit.
| Phân loại | MVCC | 2PL | Kiểm chứng lạc quan |
|---|---|---|---|
| Cách đọc | Chọn phiên bản phù hợp trong snapshot | Đọc sau khi lấy khóa | Đọc giá trị hiện tại, kiểm chứng sau |
| Tranh chấp đọc-ghi | Thấp với đọc thông thường | Chờ tùy loại khóa | Thường thấp nhưng có thể commit thất bại |
| Chi phí chính | Phiên bản·undo·dọn dẹp | Chờ khóa·bế tắc | Thử lại·rollback khi xung đột |
| Điểm mạnh | Mở rộng đọc và snapshot nhất quán | Biểu diễn quy tắc và bảo vệ giá trị mới nhất | Giao dịch ngắn·xung đột thấp |
| Lưu ý | Giao dịch dài·phình dữ liệu | Bùng nổ chờ·bế tắc | Lũy đẳng khi thử lại·đói tài nguyên |
Ba cách này không cạnh tranh nhau mà là đối tượng để kết hợp. Cơ sở dữ liệu thực tế dùng MVCC cho truy vấn thông thường, đồng thời áp dụng khóa dòng cho xung đột cập nhật, kiểm chứng tuần tự hóa cho bất biến phạm vi và kiểm tra phiên bản lạc quan ở tầng ứng dụng. Đề xuất thiết kế trong bài thi Kỹ sư chuyên nghiệp cũng không nên dừng ở “áp dụng MVCC” mà phải đưa ra biện pháp bổ trợ cho từng đường phát sinh xung đột.
5.2 Trường hợp 1 — Dịch vụ đơn hàng và tồn kho
Danh sách sản phẩm và truy vấn đơn hàng có tỷ lệ đọc cao, nên đọc không khóa dựa trên MVCC giúp giảm độ trễ. Khi khách hàng tra cứu trạng thái đơn hàng trong lúc nhân viên vận hành cập nhật trạng thái, khách hàng vẫn nhận được phiên bản nhất quán đã commit và truy vấn thông thường không phải chờ khóa lâu.
Ngược lại, trừ tồn kho không được để hai đơn hàng cùng đọc lượng tồn cuối cùng rồi cả hai đều thành công. Đường này cần thiết kế cùng cập nhật có điều kiện nguyên tử như UPDATE ... SET stock = stock - 1 WHERE product_id = ? AND stock > 0 hoặc khóa dòng mới nhất. Không được khẳng định chỉ đọc snapshot MVCC là đảm bảo bất biến tồn kho.
5.3 Trường hợp 2 — Báo cáo chạy lâu và giao dịch vận hành
Trong môi trường báo cáo doanh thu tháng chạy vài phút, MVCC cho phép báo cáo đọc dữ liệu nhất quán tại thời điểm mà nó chọn trong khi nhân viên vẫn xử lý đơn hàng mới. Điều này giảm tình trạng báo cáo khóa toàn bộ bảng và chặn nhập đơn hàng.
Tuy nhiên nếu giao dịch báo cáo giữ lâu thì các phiên bản trước thời điểm đó không được dọn. Giải pháp là tách tải sang replica chỉ đọc hoặc kho phân tích, chia báo cáo thành các giao dịch ngắn theo trang, và thống nhất với nghiệp vụ xem có thật sự cần tính nhất quán của snapshot hay không.
5.4 Trường hợp 3 — Quyết toán tài chính và yêu cầu tuần tự hóa
Quyết toán tài chính coi trọng việc ngăn trùng lặp, thiếu sót và write skew hơn là tính không chặn của truy vấn đơn thuần. Logic đọc và cập nhật số dư và hạn mức rút tiền ở các dòng khác nhau có thể không an toàn nếu chỉ dùng READ COMMITTED. Quy tắc cốt lõi được phòng thủ nhiều lớp bằng SERIALIZABLE, khóa tường minh, cập nhật có điều kiện nguyên tử và đối soát sau quyết toán.
Vì giao dịch có thể bị thử lại do xung đột tuần tự hóa, API quyết toán phải dùng định danh yêu cầu và khóa lũy đẳng. Nếu gộp phê duyệt thanh toán bên ngoài hoặc phát hành thông điệp vào cùng một khối với việc thử lại cơ sở dữ liệu thì tác dụng phụ bên ngoài có thể bị lặp, nên cần thiết kế cùng outbox pattern và khóa khử trùng lặp.
6. Nâng cao — MVCC trong môi trường phân tán và đám mây
Cơ sở dữ liệu phân tán khó biểu diễn snapshot toàn cục chỉ bằng ID giao dịch của một nút. Chúng dùng các cơ chế như timestamp logic, đồng hồ logic lai (hybrid logical clock), leader theo phạm vi, chờ commit để điều phối thời điểm đọc giữa nhiều nút. Vì độ trễ mạng dẫn tới đánh đổi giữa nhất quán snapshot và tính sẵn sàng khi ghi, cần xem xét cùng CAP và mô hình nhất quán.
Snapshot tuần tự hóa trong hệ thống SQL phân tán không đơn giản là gộp “giá trị mới nhất thấy được ở mỗi nút”. Phải xác định timestamp đọc cần cho truy vấn, kiểm tra dữ liệu tại thời điểm đó có trên các bản sao hay không, và điều phối xung đột commit cùng việc thử lại. Nếu thời gian khứ hồi giữa các vùng (region) dài, đọc·ghi nhất quán mạnh có thể làm tăng độ trễ phía người dùng.
Trong cơ sở dữ liệu được quản lý trên đám mây, có thể không trực tiếp điều khiển được mọi tham số nội bộ của MVCC. Thay vào đó, biến giao dịch dài, phiên nhàn rỗi trong connection pool, độ trễ dọn dẹp, mức sử dụng undo·WAL·lưu trữ, chờ khóa và tỷ lệ thử lại thành các chỉ số dịch vụ có thể quan sát. Vì tự động mở rộng có thể che giấu vấn đề dung lượng, cần theo dõi cả chi phí lẫn hiệu năng.
Trong môi trường HTAP, việc dọn phiên bản MVCC của OLTP và tuổi thọ snapshot của quét phân tích có thể xung đột. Tách workload phân tích sang kho cột riêng, luồng sao chép hoặc lakehouse giúp giảm gánh nặng lưu giữ phiên bản cho giao dịch vận hành. Tuy nhiên sẽ phát sinh độ trễ về độ tươi của dữ liệu và chi phí chuyển đổi schema, nên cần phản ánh vào SLA.
Truy vấn du hành thời gian (time travel) và vết kiểm toán gần đây giống với khái niệm phiên bản quá khứ của MVCC nhưng khác mục đích. Phiên bản quá khứ của MVCC có thể là dữ liệu nội bộ phục vụ kiểm soát đồng thời và rollback, còn lịch sử phục vụ kiểm toán theo quy định đòi hỏi lưu giữ dài hạn, lý do thay đổi, kiểm soát truy cập và giá trị chứng cứ pháp lý. Không nên coi phiên bản nội bộ là sổ cái kiểm toán mà cần thiết kế riêng sự kiện bất biến và log kiểm toán.
7. Các lưu ý và hàm ý
7.1 Quản lý tuổi thọ giao dịch
Chỉ giữ giao dịch dài vừa đủ để đảm bảo đơn vị nghiệp vụ, không đặt việc chờ người dùng nhập liệu hay gọi API bên ngoài vào trong giao dịch cơ sở dữ liệu. Giao dịch dài cản trở dọn dẹp phiên bản quá khứ và thay đổi schema, đồng thời làm tăng phạm vi rollback khi có sự cố.
Áp dụng rollback tự động và timeout để kết nối trong connection pool không bị tái sử dụng khi vẫn còn trạng thái giao dịch. Bảng điều khiển vận hành nên hiển thị riêng giao dịch cũ nhất, giao dịch ở trạng thái nhàn rỗi, thời gian giữ snapshot và độ trễ dọn dẹp.
7.2 Ánh xạ yêu cầu nhất quán với mức cô lập
Cách áp dụng SERIALIZABLE cho mọi nghiệp vụ hoặc thống nhất mọi nghiệp vụ về READ COMMITTED đều nguy hiểm. Với mỗi luồng có ý nghĩa nghiệp vụ khác nhau như tra cứu đơn hàng, trừ tồn kho, quyết toán, truy vấn kiểm toán, cần định nghĩa độ trễ và hiện tượng bất thường có thể chấp nhận.
Trong yêu cầu, thay vì dùng cụm “mới nhất”, hãy ghi rõ thời điểm chuẩn và độ trễ cho phép. Ví dụ danh sách tìm kiếm cho phép trễ vài giây, nhưng xác nhận thanh toán hoàn tất có thể phải thấy ngay kết quả vừa commit trong cùng phiên. Gắn định tuyến đọc, cố định phiên, đọc có khóa và thử lại với chính sách này.
7.3 Quan sát dọn dẹp, dung lượng và hiệu năng
Lượng phiên bản tạo ra phụ thuộc vào tần suất UPDATE·DELETE, kích thước dòng và số chỉ mục. Thu thập các chỉ số engine như dead tuple, undo history length, purge lag, phình bảng·chỉ mục, lượng tăng WAL và liên kết với ngưỡng dung lượng lưu trữ.
Chạy dọn dẹp quá thường xuyên có thể tăng tranh chấp I/O, chạy quá muộn thì hiệu năng truy vấn và chi phí xấu đi. Tinh chỉnh tham số dọn dẹp tự động có xét đến giờ cao điểm nghiệp vụ và thời gian batch, và kiểm chứng thay đổi cấu hình bằng kiểm thử tải trên các bảng đại diện.
7.4 Thất bại, thử lại và lũy đẳng
Xung đột tuần tự hóa MVCC hay deadlock có thể phát sinh như kết quả bình thường của kiểm soát đồng thời. Ứng dụng không nên trả mọi lỗi thành thất bại cho người dùng hay thử lại vô hạn, mà cần phân loại lỗi có thể thử lại và đặt exponential backoff cùng số lần tối đa.
Giao dịch được thử lại phải có khóa lũy đẳng, outbox, chống nhận trùng và thủ tục bù trừ để tác dụng phụ bên ngoài không bị lặp. Cần tính đến trường hợp commit cơ sở dữ liệu đã thành công nhưng phản hồi bị mất, để dù client gửi lại cùng yêu cầu thì kết quả chỉ được phản ánh một lần.
7.5 Bảo mật, kiểm toán và kiểm soát vận hành
Việc snapshot đọc dữ liệu quá khứ không có nghĩa là vượt qua kiểm soát truy cập. Bảo mật mức dòng, điều kiện tenant và kiểm tra quyền phải áp dụng như nhau cho cả phiên bản quá khứ, đồng thời cần xem xét chính sách lưu giữ thông tin cá nhân đã xóa và khả năng còn sót lại trong undo·bản sao lưu.
Khi người vận hành buộc dừng giao dịch dài để giải quyết vấn đề hiệu năng, cần xác nhận ảnh hưởng nghiệp vụ và chi phí rollback. Ghi lại các chỉ số phiên·truy vấn·dọn dẹp trước và sau khi buộc dừng để nguyên nhân và hiệu quả của biện pháp có thể được kiểm toán.
7.6 Hàm ý tổng hợp từ góc nhìn Kỹ sư chuyên nghiệp
MVCC là chức năng của engine cơ sở dữ liệu, nhưng chất lượng thực tế được quyết định bởi sự kết hợp giữa ranh giới giao dịch, mức cô lập, chỉ mục, chính sách dọn dẹp, giám sát và thiết kế thử lại. Nếu chỉ nhấn mạnh ưu điểm “đọc không chặn” sẽ bỏ sót phình phiên bản và vi phạm bất biến nghiệp vụ.
Khi lựa chọn kiến trúc, cần đo không chỉ TPS và độ trễ trung bình mà cả độ trễ P99, tỷ lệ xung đột cập nhật, tỷ lệ giao dịch dài, độ trễ dọn dẹp, chi phí lưu trữ và lượng xử lý lại khi phục hồi sự cố. Cùng là MVCC nhưng mức cô lập và giá trị tinh chỉnh tối ưu khác nhau tùy engine và workload.
Kết luận của bài làm không nên là “áp dụng MVCC” mà nên tổng kết thành “định nghĩa cấp độ nhất quán theo nghiệp vụ, kết hợp đọc snapshot với khóa·tuần tự hóa·thử lại lũy đẳng, và quan sát·kiểm soát vòng đời phiên bản”.
Tài liệu tham khảo
- PostgreSQL, “Introduction to MVCC”: https://www.postgresql.org/docs/current/mvcc-intro.html
- PostgreSQL, “Concurrency Control”: https://www.postgresql.org/docs/current/mvcc.html
- MySQL, “Consistent Nonlocking Reads”: https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html
- MySQL, “InnoDB Multi-Versioning”: https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html
- PostgreSQL, “Transaction Isolation”: https://www.postgresql.org/docs/current/transaction-iso.html
Tóm tắt một câu: MVCC giảm tranh chấp đọc-ghi nhờ snapshot theo giao dịch và đa phiên bản, nhưng chỉ đạt được đồng thời nhất quán và hiệu năng khi thiết kế cùng mức cô lập, thử lại khi xung đột, giao dịch dài và dọn dẹp phiên bản.