Danh mục:
Bài viết này hệ thống lại các kiến thức về mức độ cô lập giao dịch (Transaction Isolation Level), cơ chế MVCC và cách triển khai thực tế với R2DBC.
Mức độ cô lập giao dịch (Transaction Isolation Level) là gì?
Đây là tiêu chuẩn để quyết định mức độ cho phép các vấn đề về tính nhất quán dữ liệu xảy ra trong quá trình xử lý đồng thời (concurrency) giữa các giao dịch.

Các vấn đề có thể phát sinh
- Dirty Read: Đọc dữ liệu từ một giao dịch khác chưa được Commit.
- Non-Repeatable Read: Trong cùng một giao dịch, đọc cùng một dữ liệu nhiều lần nhưng kết quả trả về khác nhau (do giao dịch khác đã Update).
- Phantom Read: Trong cùng một giao dịch, thực hiện cùng một câu lệnh truy vấn điều kiện nhiều lần nhưng số lượng dòng trả về khác nhau (do giao dịch khác đã Insert/Delete).
So sánh 4 mức độ cô lập giao dịch

MVCC là gì?
MVCC (Multi-Version Concurrency Control) là cơ chế kiểm soát đồng thời đa phiên bản.
→ Giúp duy trì tính nhất quán bằng cách cho phép mỗi giao dịch đọc một phiên bản snapshot riêng, từ đó không cần dùng khóa (lock) mà vẫn đảm bảo tính nhất quán.
Được kích hoạt mặc định trong MySQL InnoDB, PostgreSQL...
MVCC đảm bảo việc đọc dựa trên snapshot từ mức độ
REPEATABLE READ trở lên.FOR UPDATE là gì?
Khi thực hiện lệnh
SELECT ... FOR UPDATE bên trong một giao dịch, hệ thống sẽ đặt một khóa độc quyền (Exclusive Lock - X-Lock) lên dòng dữ liệu đó, ngăn chặn các giao dịch khác sửa đổi nó cho đến khi giao dịch hiện tại kết thúc.Ví dụ sử dụng:
BEGIN; -- Khóa dòng dữ liệu sản phẩm có id = 1 SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- Cập nhật kho hàng an toàn UPDATE product SET stock = stock - 1 WHERE id = 1; COMMIT;
Ví dụ thực tế với R2DBC + MySQL
1. Kiểm tra mức độ cô lập trong MySQL
-- Kiểm tra mức độ cô lập của session hiện tại
SELECT @@tx_isolation;
-- Thiết lập mức độ REPEATABLE READ
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
2. Định nghĩa bảng dữ liệu
CREATE TABLE product (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
stock INT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
3. Kotlin Repository (Sử dụng FOR UPDATE)
Sử dụng
DatabaseClient để thực hiện khóa dòng dữ liệu khi tìm kiếm sản phẩm.@Repository class ProductRepositoryImpl(private val client: DatabaseClient) : ProductRepository { override fun findWithLock(productId: Long): Mono<Product> { return client.sql("SELECT * FROM product WHERE id = :id FOR UPDATE") .bind("id", productId) .map { row -> Product( id = row.get("id", Long::class.java)!!, name = row.get("name", String::class.java)!!, stock = row.get("stock", Int::class.java)!! ) }.one() } override fun updateStock(productId: Long, newStock: Int): Mono<Void> { return client.sql("UPDATE product SET stock = :stock WHERE id = :id") .bind("stock", newStock) .bind("id", productId) .then() } }
4. Xử lý giao dịch với TransactionalOperator
Đảm bảo tính nguyên tử (Atomicity) cho quy trình mua hàng.
@Service class ProductService( private val productRepository: ProductRepository, private val tx: TransactionalOperator, ) { fun purchase(productId: Long): Mono<Void> { return tx.execute { productRepository.findWithLock(productId) .flatMap { product -> if (product.stock <= 0) { return@flatMap Mono.error(IllegalStateException("Hết hàng")) } productRepository.updateStock(product.id, product.stock - 1) } }.then() } }
Tổng kết
- Mức độ cô lập: REPEATABLE READ (Mặc định của MySQL)
- Xử lý nhất quán: Đọc snapshot dựa trên MVCC
- Ngăn chặn xung đột: Sử dụng FOR UPDATE để chiếm khóa độc quyền
- Tổ hợp công nghệ: MySQL + Spring WebFlux + R2DBC
Nguồn bài viết ryukato.github.io