Thứ Năm, 26 tháng 6, 2025
Điều khiển truy xuất đồng thời - phần 1
Đăng bởi

Mục lục:
- Transaction
- Các vấn đề của truy xuất đồng thời
- Các kỹ thuật giải quyết vấn đề truy xuất đồng thời
Transaction
Nhắc lại một xíu về transaction, là một hoặc nhiều các thao tác SQL tương tác với dữ liệu bao gồm đọc và ghi.
Các hành động của một transaction
- Input: lấy data từ db đưa vào buffer.
- Read: lấy data từ buffer vào Transaction.
- Write: ghi data từ Transaction vào buffer.
- Commit: ghi data từ buffer vào db.
ACID
ACID là bốn tính chất của một transaction, tượng trưng cho:
- Atomicity: Tất cả câu lệnh phải được thực hiện từ đầu tới cuối không sót câu nào, có 1 câu lệnh không thành công thì rollback mọi thứ đã thực hiện trước đó.
- Consistency: Transaction phải đưa cơ sở dữ liệu (CSDL) từ trạng thái nhất quán này đến trạng thái nhất quán khác, không vi phạm các ràng buộc toàn vẹn.
- Isolation: Transaction phải cô lập và không được dính líu tới các transaction khác diễn ra đồng thời.
- Durability: Dữ liệu đã cập nhật bởi transaction đã kết thúc phải được lưu trữ bền bỉ xuống ổ cứng bất chấp sự cố gì xảy ra.
Schedule
Là một tập hợp của nhiều transaction trên một thời gian cụ thể trong cùng một CSDL.
Có hai loại schedule:
- Serial schedules: đúng nhưng chậm
- Non-serialize schedules: nhanh nhưng có thể sai.
Chính vì nhánh non-serialize schedules nó nhanh nên ta sẽ tìm hiểu về nó và dùng trong những trường hợp nhất định. Trước hết cùng xem qua những lỗi có thể xảy ra của non-serialize schedules
Những lỗi sai của non-serialize schedules bao gồm:
Quy ước dùng cho các ví dụ sau:
- R(A, x): đọc dữ liệu trong tập A và cho vào biến x.
- W(A, x): ghi dữ liệu của x vào tập A.
- T1: Transaction 1, tương tự với T2 T3,.. Số này chỉ mang tính định danh chứ không định nghĩa transaction nào chạy trước chạy sau hén.
Lost update: mất dữ liệu cập nhật
Lost update là lỗi mà ở đó dữ liệu cập nhật ở một transaction bị ghi đè bởi dữ liệu cập nhật của một transaction khác.
Lấy ví dụ ta có 2 giao dịch chuyển tiền vào tài khoản A, một giao dịch nhân đôi số tiền hiện có (T1), giao dịch còn lại nhân ba số tiền hiện có (T2). T2 thực thi ra ngay lập tức sau khi T1 được thực thi.
Timestamp | T1 | T2 |
---|---|---|
1 | R(A, x) | |
2 | R(A, x) | |
3 | x = x * 2 | |
4 | x = x * 3 | |
5 | W(A, x) | |
6 | W(A, x) |
Có thể thấy, cả hai transaction đều đọc A, update A và ghi A vào CSDL. Nếu x ban đầu là 1 thì:
Timestamp | T1 | T2 | A | x1 | x2 |
---|---|---|---|---|---|
1 | R(A, x) | 1 | 1 | 1 | |
2 | R(A, x) | 1 | 1 | 1 | |
3 | x = x * 2 | 1 | 2 | 1 | |
4 | x = x * 3 | 1 | 2 | 3 | |
5 | W(A, x) | 2 | 2 | 3 | |
6 | W(A, x) | 3 | 2 | 3 |
Kết quả cuối cùng A sẽ nhận được là 3 thay vì 6 như ta mong muốn và kết quả từ giao dịch đầu tiên đã bị mất.
Dirty read: đọc dữ liệu rác
Lỗi dirty read diễn ra khi một transaction A đọc dữ liệu đã được chỉnh sửa từ transaction B nhưng dữ liệu đó chưa được commit.
Timestamp | T1 | T2 | x1 | x2 |
---|---|---|---|---|
1 | R(A, x) | 1 | . | |
2 | x = x + 2 | 3 | . | |
3 | R(A, x) | . | 3 | |
4 | ... | . | 3 | |
5 | ⚠️ Rollback | 1 | 3 |
Ví dụ trên cho thấy transaction 2 đã đọc kết quả x=3 nhưng sau đó một thời gian transaction 1 rollback, huỷ kết quả x=3 và reset nó về x=1. Tuy nhiên transaction 2 vẫn lấy kết quả x=3 đi tính toán cho những logic khác.
Non-repeatable read: không thể đọc lại
Non-repeatable read là diễn ra khi trong cùng một transaction, những lần đọc dữ liệu lại cho ra một kết quả khác nhau
Timestamp | T1 | T2 | x |
---|---|---|---|
1 | R(A, x) | 1 | |
2 | ... | R(A, x) | 1 |
3 | ... | x = x + 3 | 3 |
4 | ... | W(A, x) | 4 |
5 | ... | ... | 4 |
6 | ⚠️ R(A, x) | 4 |
Tại thời điểm 1, ở transaction 1, x đang là 1 nhưng tới thời điểm 6 thì x lại là 4 dù transaction 1 không có bất cứ lệnh write nào.
Phantom read: bóng ma
Timestamp | T1 | T2 |
---|---|---|
1 | R(A = {A1, ..., An}) | |
2 | ... | ... |
3 | ... | Ins/Del Ai ∈ A |
4 | ... | ... |
Tương tự unrepeatable read nhưng nó chỉ thay đổi số lượng phần tử mà không thay đổi giá trị. Dẫn đến việc khi tính trung bình, việc thay đổi số lượng phần tử này sẽ làm sai lệch giá trị của T1.
Để xử lý các vấn đề như vậy trong môi trường truy xuất đồng thời, có thể áp dụng một số kỹ thuật sau:
Các kỹ thuật giải quyết vấn đề truy xuất đồng thời
Gồm các kỹ thuật phổ biến như: locking, timestamp ordering, và kvalidation-based concurrency control. Trong phạm vi bài này, mình sẽ tập trung vào kỹ thuật locking – thứ mà hồi học đại học tui được học mỗi cái đấy 😅
Locking
Có nhiều loại như:
- Simple locks
- Granular locks (khóa đa hạt)
- Read/write locks
💡 Bài này tập trung vào read/write locks vì nó hay xuất hiện trong thực tế và giúp giải quyết nhiều vấn đề phổ biến như lost update, dirty read,...
Quy tắc transaction đúng đắn (Well-formed Transaction)
Một transaction được gọi là đúng đắn nếu tuân theo các nguyên tắc sau:
- Trước khi đọc → Gắn read lock:
R(A) ⇒ Rl(A)
- Trước khi ghi → Gắn write lock:
W(A) ⇒ Wl(A)
- Sau khi xong → Gỡ lock:
u(A) hoặc ul(A)
Valid Schedule
Một schedule (lịch) được xem là hợp lệ nếu nó tuân theo ma trận tương thích (Compatibility Matrix):
Đọc ma trận theo chiều: [Cột là loại lock đang tồn tại] → [Dòng là lock bạn muốn xin thêm].
Rl (read) | Wl (write) | ul (unlock) | |
---|---|---|---|
Rl | ✅ | ❌ | ❌ |
Wl | ❌ | ❌ | ❌ |
ul | ✅ | ❌ | ❌ |
Ý nghĩa:
- Nhiều read lock cùng lúc thì OK (✅)
- Write lock thì phải độc quyền (❌)
Two-Phase Locking – 2PL
Đây là một kỹ thuật đảm bảo tính đồng bộ (conflict-serializability) cho schedule.
“Một khi đã unlock, thì không được lock thêm bất kỳ tài nguyên nào nữa!”
Hai giai đoạn rõ ràng:
-
Giai đoạn 1 – Mở rộng (Growing Phase) → Chỉ được gắn lock, không được gỡ lock
-
Giai đoạn 2 – Thu hẹp (Shrinking Phase) → Chỉ được gỡ lock, không được gắn lock nữa
Nếu một transaction gỡ xong rồi lại gắn lock → vi phạm 2PL
Lịch khả tuần tự xung đột (Conflict-Serializable Schedule)
Nếu một schedule tuân thủ:
- Well-formed transaction
- Compatibility Matrix
- Two-phase locking (2PL)
→ Thì được xem là conflict-serializable, tức là có thể sắp xếp lại để tương đương với một lịch tuần tự – đảm bảo tính nhất quán của dữ liệu.
Note:
Qua bài viết này, bạn đã hiểu về kỹ thuật locking và cách dùng để giải quyết các vấn đề truy xuất đồng thời như lost update, dirty read,... Bài sau chúng ta sẽ đi sâu vào Transaction Isolation Level – thứ cực kỳ quan trọng nếu bạn làm việc với RDBMS hoặc các hệ thống phân tán như DynamoDB.