Các Chiến lược Khóa trong Cơ sở dữ liệu
Bạn sẽ Học được gì
Kết thúc trang này, bạn sẽ có thể:
- Giải thích tại sao khóa (Locking) cần thiết trong môi trường cơ sở dữ liệu đồng thời (Concurrent Environment)
- Nhận diện các loại khóa (chia sẻ, độc quyền, ý định, cập nhật) và các mức độ chi tiết (Granularity) của chúng
- So sánh khóa bi quan (Pessimistic) và khóa lạc quan (Optimistic) — cách hoạt động, khi nào sử dụng, và đánh đổi giữa chúng
- Nhận diện và ngăn chặn Deadlock (Khóa chết)
- Hiểu leo thang khóa (Lock Escalation) và tác động đến hiệu năng
- Chọn chiến lược khóa phù hợp bằng khung quyết định (Decision Framework)
Vấn đề Đồng thời (The Concurrency Problem)
Hãy tưởng tượng một hệ thống đặt vé xem hòa nhạc. Chỉ còn một ghế trống, và hai người dùng cố gắng đặt cùng lúc:
User A: READ seat → status = "available"
User B: READ seat → status = "available"
User A: UPDATE seat → status = "booked by A"
User B: UPDATE seat → status = "booked by B"
Cả hai người dùng đều đọc "available", cả hai đều cập nhật, và ghế bị đặt trùng (Double-booked). Đây là một điều kiện tranh chấp (Race Condition) — và nó xảy ra bất cứ khi nào nhiều giao dịch (Transactions) truy cập cùng dữ liệu đồng thời mà không có sự phối hợp.
Điều gì có thể sai nếu không có khóa?
| Vấn đề | Mô tả | Ví dụ |
|---|---|---|
| Cập nhật bị mất (Lost Update) | Hai giao dịch ghi đè thay đổi của nhau | Cả hai người dùng đặt ghế cuối cùng |
| Đọc bẩn (Dirty Read) | Đọc dữ liệu chưa được commit từ giao dịch khác | Đọc số dư giữa quá trình chuyển tiền |
| Đọc không lặp lại (Non-Repeatable Read) | Đọc cùng một dòng hai lần nhận được giá trị khác nhau | Dòng bị sửa đổi giữa hai lần đọc trong cùng giao dịch |
| Đọc ma (Phantom Read) | Chạy lại truy vấn trả về tập dòng khác nhau | Dòng mới được chèn giữa các truy vấn |
Khóa (Locks) là cơ chế chính mà cơ sở dữ liệu sử dụng để phối hợp truy cập đồng thời và ngăn chặn các vấn đề này.

Kiến thức Nền tảng về Khóa (Lock Fundamentals)
Trước khi so sánh các chiến lược, hãy hiểu các khối xây dựng cơ bản.
Các loại khóa
| Loại khóa | Phạm vi | Tương thích với | Mục đích |
|---|---|---|---|
| Shared Lock (S - Khóa chia sẻ) | Dòng / Trang / Bảng | Các khóa chia sẻ khác | Cho phép đọc đồng thời |
| Exclusive Lock (X - Khóa độc quyền) | Dòng / Trang / Bảng | Không khóa nào khác | Ngăn chặn mọi truy cập đồng thời |
| Intent Shared (IS - Khóa chia sẻ ý định) | Bảng | Các khóa ý định khác | Báo hiệu: "Tôi có thể khóa một dòng ở chế độ chia sẻ" |
| Intent Exclusive (IX - Khóa độc quyền ý định) | Bảng | Khóa IS, nhưng không với khóa S trên cùng tài nguyên | Báo hiệu: "Tôi có thể khóa một dòng ở chế độ độc quyền" |
| Update Lock (U - Khóa cập nhật) | Dòng | Khóa chia sẻ, nhưng không với khóa U/X khác | Ngăn Deadlock khi nâng cấp S → X |
Ma trận tương thích — khóa nào có thể cùng tồn tại trên cùng một tài nguyên:
S X IS IX U
S ✓ ✗ ✓ ✗ ✓
X ✗ ✗ ✗ ✗ ✗
IS ✓ ✗ ✓ ✓ ✓
IX ✗ ✗ ✓ ✓ ✗
U ✓ ✗ ✓ ✗ ✗
Mức độ Chi tiết của Khóa (Lock Granularity)
Việc chọn mức độ chi tiết phù hợp cân bằng giữa tính nhất quán và hiệu năng:
| Mức độ chi tiết | Phạm vi khóa | Tính đồng thời | Chi phí | Trường hợp sử dụng |
|---|---|---|---|---|
| Database (Cơ sở dữ liệu) | Toàn bộ cơ sở dữ liệu | Không | Rất thấp | Bảo trì, sao lưu |
| Table (Bảng) | Tất cả các dòng trong bảng | Rất thấp | Thấp | Thao t ác hàng loạt, thay đổi schema |
| Page (Trang) | Một khối các dòng | Trung bình | Trung bình | Mặc định của một số công cụ cơ sở dữ liệu |
| Row (Dòng) | Một dòng duy nhất | Cao | Cao hơn | Hầu hết các thao tác OLTP |
Luôn áp dụng khóa ở mức độ chi tiết nhất (dòng thay vì bảng) để giảm thiểu tranh chấp và tối đa hóa tính đồng thời.
Vòng đời Khóa (Lock Lifecycle)
Mọi khóa đều tuân theo cùng một vòng đời:
- Yêu cầu (Request) — một giao dịch yêu cầu trình quản lý khóa cấp khóa
- Được cấp hoặc Bị chặn — nếu tương thích với các khóa hiện có, khóa được cấp; nếu không, giao dịch phải chờ
- Đang giữ (Held) — khóa hoạt động trong suốt thời gian của giao dịch
- Đã giải phóng (Released) — khóa được giải phóng khi
COMMIThoặcROLLBACK
Khóa Bi quan (Pessimistic Locking)
Khóa bi quan giả định xung đột sẽ xảy ra. Nó thu nhận khóa trên dữ liệu trước khi thực hiện bất kỳ sửa đổi nào, ngăn chặn các giao dịch khác đọc hoặc sửa đổi dữ liệu bị khóa cho đến khi khóa được giải phóng.
Cách hoạt động (How it works)
Hai giao dịch (Transaction) tranh giành cùng một dòng products (id=1, stock=10). Giao dịch A thu nhận khóa trước — Giao dịch B phải chờ cho đến khi A commit (xác nhận).
Đặc điểm chính: Giao dịch B bị chặn vật lý ở mức cơ sở dữ liệu — nó không thể tiếp tục cho đến khi A giải phóng khóa.
Các lệnh SQL phổ biến
-- Khóa đọc bi quan (khóa chia sẻ)
SELECT * FROM orders WHERE id = 100 LOCK IN SHARE MODE;
-- Khóa ghi bi quan (khóa độc quyền)
SELECT * FROM orders WHERE id = 100 FOR UPDATE;
-- PostgreSQL: khóa cố vấn (Advisory Lock - mức ứng dụng)
SELECT pg_advisory_lock(12345);
SELECT pg_advisory_unlock(12345);
-- SQL Server: khóa dựa trên gợi ý (Hint-based Locking)
SELECT * FROM orders WITH (UPDLOCK, HOLDLOCK) WHERE id = 100;
Khi nào nên sử dụng Khóa Bi quan
- Mức độ tranh chấp cao (High Contention) — nhiều giao dịch cạnh tranh cho cùng các dòng (ví dụ: đặt vé, quản lý tồn kho)
- Tính toàn vẹn dữ liệu quan trọng (Critical Data Integrity) — chi phí của một xung đột là không thể chấp nhận được (ví dụ: giao dịch tài chính)
- Giao dịch ngắn (Short Transactions) — khóa được giữ ngắn, giảm thiểu thời gian chặn
- Yêu cầu nhất quán thời gian thực (Real-time Consistency) — người dùng phải thấy trạng thái cập nhật nhất
Nhược điểm
- Giảm tính đồng thời (Reduced Concurrency) — các giao dịch khác bị chặn trong khi khóa được giữ
- Deadlock (Khóa chết) — hai giao dịch chờ khóa của nhau vô thời hạn
- Nút thắt hiệu năng (Performance Bottleneck) — các giao dịch chạy lâu giữ khóa và chặn toàn b ộ hệ thống
Khóa Lạc quan (Optimistic Locking)
Khóa lạc quan giả định xung đột là hiếm. Nó cho phép nhiều giao dịch đọc và sửa đổi dữ liệu đồng thời mà không cần thu nhận khóa trước. Thay vào đó, nó kiểm tra xung đột tại thời điểm commit. Nếu phát hiện xung đột, giao dịch được hoàn tác và phải thử lại.
Cách hoạt động
Khóa lạc quan thường sử dụng cột phiên bản (Version Column) hoặc cột dấu thời gian (Timestamp Column) để phát hiện xung đột:
Transaction A: Transaction B:
Đọc sản phẩm (version = 1)
Đọc sản phẩm (version = 1)
Cập nhật: SET stock = stock - 1,
version = version + 1
WHERE id = 1 AND version = 1
→ 1 dòng được cập nhật ✓
Cập nhật: SET stock = stock - 1,
version = version + 1
WHERE id = 1 AND version = 1
→ 0 dòng được cập nhật ✗ (version giờ là 2)
→ Phát hiện xung đột, thử lại hoặc hủy
Đặc điểm chính: Không có khóa nào được giữ — Giao dịch B tiến hành tự do và chỉ phát hiện xung đột khi cố gắng commit.
Các cách triển khai
1. Cột phiên bản (Version Column - khuyến nghị)
-- Thêm cột phiên bản
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
-- Đọc
SELECT id, stock, version FROM products WHERE id = 1;
-- Trả về: id=1, stock=10, version=3
-- Cập nhật (kiểm tra phiên bản)
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 3;
-- Nếu rowcount = 1 → thành công
-- Nếu rowcount = 0 → xung đột, giao dịch khác đã sửa đổi
2. Cột dấu thời gian (Timestamp Column)
-- Thêm cột dấu thời gian
ALTER TABLE products ADD COLUMN modified_at TIMESTAMP DEFAULT NOW();
-- Đọc
SELECT id, stock, modified_at FROM products WHERE id = 1;
-- Cập nhật (kiểm tra dấu thời gian)
UPDATE products
SET stock = stock - 1, modified_at = NOW()
WHERE id = 1 AND modified_at = '2025-01-15 10:30:00';
-- Nếu rowcount = 0 → phát hiện xung đột
3. Checksum cột (Xác minh tất cả các cột)
-- Cập nhật kiểm tra tất cả giá trị gốc
UPDATE products
SET stock = stock - 1
WHERE id = 1
AND stock = 10 -- giá trị gốc
AND name = 'Widget' -- giá trị gốc
AND price = 9.99; -- giá trị gốc
-- Nếu rowcount = 0 → có gì đó đã thay đổi, xung đột
Khóa lạc quan trong EF Core
// Cấu hình Token đồng thời (Concurrency Token)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.Property(p => p.Version)
.IsRowVersion()
.IsConcurrencyToken();
}
// Sử dụng với logic thử lại
var maxRetries = 3;
for (var i = 0; i < maxRetries; i++)
{
try
{
var product = await context.Products.FindAsync(id);
product.Stock -= 1;
await context.SaveChangesAsync();
break; // thành công
}
catch (DbUpdateConcurrencyException)
{
if (i == maxRetries - 1) throw;
// Tách các thực thể cũ và thử lại
foreach (var entry in context.ChangeTracker.Entries())
entry.State = EntityState.Detached;
}
}
Khi nào nên sử dụng Khóa Lạc quan
- Mức độ tranh chấp thấp (Low Contention) — xung đột hiếm khi xảy ra (ví dụ: cập nhật hồ sơ người dùng, chỉnh sửa wiki)
- Khối lượng đọc cao (High Read Volume) — nhiều lần đọc, ít lần ghi vào cùng các dòng
- Hệ thống phân tán (Distributed Systems) — khóa giữa các dịch vụ là không thực tế
- Giao dịch dài (Long Transactions) — giữ khóa bi quan trong thời gian dài là tốn kém
Nhược điểm
- Chi phí thử lại (Retry Overhead) — các giao dịch thất bại phải được thử lại bởi ứng dụng
- Công việc lãng phí (Wasted Work) — giao dịch thực hiện tất cả công việc rồi mới phát hiện xung đột
- Không phù hợp khi tranh chấp cao — thử lại thường xuyên làm mất đi lợi ích hiệu năng
Trực quan Tương tác (Interactive Visualization)
Xem cách khóa bi quan và lạc quan xử lý hai giao dịch đồng thời cạnh tranh cho cùng một dòng. Chuyển đổi giữa các chiến lược để thấy sự khác biệt.
Lock data before modifying — other transactions are BLOCKED until the lock is released. Prevents conflicts but reduces concurrency.
Deadlock (Khóa chết)
Deadlock xảy ra khi hai giao dịch mỗi giao dịch giữ một khóa mà giao dịch kia cần — không bên nào có thể tiếp tục.
Phát hiện và Giải quyết
Hầu hết cơ sở dữ liệu phát hiện Deadlock tự động bằng cách kiểm tra vòng chờ tuần hoàn (Circular Wait) trong đồ thị khóa. Khi tìm thấy, cơ sở dữ liệu:
- Chọn một giao dịch nạn nhân (Victim Transaction) (thường là giao dịch đã thực hiện ít công việc nhất)
- Hoàn tác (Rollback) nó và giải phóng các khóa
- Trả về lỗi cho ứng dụng (ví dụ: mã lỗi 1205 trong SQL Server)
Ứng dụng phải bắt lỗi này và thử lại giao dịch.
Chiến lược Ngăn chặn
| Chiến lược | Cách hoạt động | Hiệu quả |
|---|---|---|
| Thứ tự khóa nh ất quán (Consistent Lock Ordering) | Luôn thu nhận khóa theo cùng thứ tự trong tất cả các giao dịch | Loại bỏ vòng chờ — phòng ngừa hiệu quả nhất |
| Giao dịch ngắn (Short Transactions) | Giảm thiểu thời gian giữa lúc thu nhận và giải phóng khóa | Giảm cửa sổ hình thành Deadlock |
| Thời gian chờ khóa (Lock Timeouts) | Đặt lock_timeout để giao dịch không chờ vô thời hạn | Ngăn chặn chặn vô thời hạn, nhưng không ngăn Deadlock |
| Mức cô lập thấp hơn (Lower Isolation Levels) | Sử dụng mức cô lập yếu nhất đáp ứng yêu cầu | Giảm số lượng và thời gian giữ khóa |
Với khóa bi quan, luôn triển khai logic thử lại Deadlock trong ứng dụng của bạn. Deadlock không phải là lỗi — chúng là điều bình thường trong các hệ thống đồng thời.
Leo thang Khóa (Lock Escalation)
Leo thang khóa (Lock Escalation) là khi cơ sở dữ liệu chuyển đổi nhiều khóa chi tiết (mức dòng) thành khóa thô hơn (mức trang hoặc mức bảng) để giảm chi phí bộ nhớ.
Tại sao nó xảy ra
Quản lý hàng nghìn khóa dòng riêng biệt tiêu tốn bộ nhớ đáng kể. Khi một giao dịch vượt quá ngưỡng, cơ sở dữ liệu thực hiện leo thang:
- SQL Server leo thang ở khoảng 5,000 khóa mỗi đối tượng
- PostgreSQL sử dụng cách tiếp cận khác — nó không leo thang mà sử dụng khóa vị từ (Predicate Locks) cho mức cô lập có thể tuần tự hóa (Serializable)
Tác động
- Giảm chi phí quản lý khóa và sử dụng bộ nhớ
- Giảm tính đồng thời — các truy vấn không liên quan trên cùng bảng có thể bị chặn
- Có thể gây giảm hiệu năng bất ngờ trong các khối lượng công việc OLTP
Cách quản lý
- Chia các thao tác hàng loạt lớn thành các phần nhỏ hơn — xử lý 1,000 dòng mỗi lần thay vì 100,000
- Sử dụng mức cô lập phù hợp — không cô lập quá mức cần thiết
- Giám sát số lượng khóa — sử dụng
sys.dm_tran_locks(SQL Server) hoặcpg_locks(PostgreSQL) - SQL Server: sử dụng
ALTER TABLE ... SET (LOCK_ESCALATION = DISABLE)một cách thận trọng — ngăn leo thang nhưng có thể tăng áp lực bộ nhớ
Khóa Bi quan vs Khóa Lạc quan: So sánh
Bảng so sánh
| Khóa Bi quan (Pessimistic) | Khóa Lạc quan (Optimistic) | |
|---|---|---|
| Giả định | Xung đột sẽ xảy ra | Xung đột là hiếm |
| Thời điểm khóa | Trước khi đọc/sửa đổi | Chỉ kiểm tra khi commit |
| Chặn | Có — các giao dịch khác phải chờ | Không — các giao dịch tự do tiến hành |
| Xử lý xung đột | Ngăn chặn (chặn cho đến khi an toàn) | Phát hiện (hoàn tác và thử lại) |
| T ính đồng thời | Thấp hơn (truy cập tuần tự) | Cao hơn (truy cập song song) |
| Rủi ro Deadlock | Có (cần thứ tự khóa) | Không (không giữ khóa) |
| Phù hợp nhất cho | Tranh chấp cao, dữ liệu quan trọng | Tranh chấp thấp, nặng về đọc |
| Triển khai | Mức cơ sở dữ liệu (FOR UPDATE) | Mức ứng dụng (cột phiên bản) |
| Hiệu năng | Có thể giảm dưới tải | Thông lượng tốt hơn, chi phí thử lại khi xung đột |
Hướng dẫn Quyết định
Sử dụng biểu đồ sau để chọn chiến lược phù hợp cho kịch bản của bạn:
Các Lỗi Phổ biến
- Giữ khóa quá lâu — các giao dịch chạy lâu với khóa bi quan chặn người dùng khác và giảm hiệu năng. Giữ giao dịch ngắn.
- Leo thang khóa (Lock Escalation) — cơ sở dữ liệu có thể leo thang khóa dòng thành khóa bảng khi nhiều dòng bị khóa. Điều này có thể làm giảm tính đồng thời bất ngờ.
- Thiếu logic thử lại — khóa lạc quan không có logic thử lại gây lỗi hiển thị cho người dùng thay vì phục hồi trong suốt.
- Bỏ qua Deadlock — với khóa bi quan, luôn triển khai phát hiện Deadlock và logic thử lại trong ứng dụng của bạn.
- Sử dụng
SELECT FOR UPDATEcho đọc — chỉ sử dụng khi bạn có ý định sửa đổi. Khóa độc quyền không cần thiết làm giảm tính đồng thời.
Best Practices
- Giữ khóa trong thời gian tối thiểu — giảm tranh chấp bằng cách giữ giao dịch ngắn
- Khóa ở mức độ chi tiết nhất — ưu tiên khóa mức dòng thay vì mức bảng
- Triển khai logic thử lại — xử lý lỗi đồng thời lạc quan và Deadlock một cách khéo léo
- Chọn chiến lược phù hợp — bi quan cho dữ liệu quan trọng có tranh chấp cao, lạc quan cho kịch bản tranh chấp thấp
- Giám sát thời gian chờ khóa (Lock Waits) — sử dụng giám sát cơ sở dữ liệu để phát hiện các điểm nóng tranh chấp khóa
- Sử dụng mức cô lập (Isolation Level) phù hợp — khớp mức cô lập với yêu cầu nhất quán của bạn