[SQL] Transaction - Isolation Level

Ha Ngoc HieuHa Ngoc Hieu
15 min read

Hãy tưởng tượng bạn đang làm việc với một hệ thống dữ liệu quan trọng, ví dụ như tài khoản ngân hàng. Nhiều thao tác cần được thực hiện cùng lúc (ví dụ: chuyển tiền từ tài khoản A sang B cần trừ tiền ở A và cộng tiền ở B). Chúng ta cần đảm bảo rằng những thao tác này hoặc là thành công tất cả hoặc thất bại tất cả, và không bị ảnh hưởng bởi những người dùng khác đang thao tác cùng lúc. Đây chính là lúc Transaction và Isolation Levels phát huy tác dụng.


1. Transaction và Các Lệnh Cơ Bản: BEGIN, COMMIT, ROLLBACK

a. Transaction là gì?

Một Transaction (giao dịch) là một chuỗi các thao tác SQL (như SELECT, INSERT, UPDATE, DELETE) được xem như một đơn vị công việc duy nhất, không thể chia nhỏ (atomic).

Nó phải tuân thủ 4 tính chất quan trọng, gọi là ACID:

  1. Atomicity (Tính nguyên tử):

    Hoặc tất cả các thao tác trong transaction đều thành công và được ghi nhận vào cơ sở dữ liệu (CSDL), hoặc nếu có bất kỳ lỗi nào xảy ra, tất cả các thay đổi đã thực hiện sẽ bị hủy bỏ (rollback), đưa CSDL về trạng thái trước khi transaction bắt đầu. Không có chuyện "làm nửa vời".

  2. Consistency (Tính nhất quán):

    Transaction phải đảm bảo rằng sau khi nó kết thúc (dù là commit hay rollback), CSDL vẫn ở trạng thái hợp lệ, tuân thủ mọi ràng buộc đã định nghĩa. Nếu một transaction vi phạm bất kỳ ràng buộc nào, nó phải bị huỷ bỏ (ví dụ ràng buộc số dư tài khoản không thể âm, transaction thực hiện chuyển tiền/trừ tiên phải bị rollback nếu vi phạm điều này).

  3. Isolation (Tính cô lập): Các transaction diễn ra đồng thời phải được cô lập/cách ly với nhau. Kết quả trung gian của một transaction chưa hoàn thành (chưa commit) không nên ảnh hưởng đến các transaction khác đang chạy. Mức độ cách ly này được định nghĩa bởi "Isolation Levels"

  4. Durability (Tính bền vững): Một khi transaction đã được commit thành công, các thay đổi của nó phải được lưu trữ vĩnh viễn và không bị mất ngay cả khi có sự cố hệ thống (mất điện, server crash…).

b. Các Lệnh Điều Khiển Transaction

  1. BEGIN (hoặc START TRANSACTION):

  • Mục đích:

    • Đánh dấu điểm bắt đầu của một transaction mới.

    • Mọi lệnh SQL sau BEGIN cho đến khi gặp COMMIT hoặc ROLLBACK sẽ thuộc về transaction này.

  • Cách thức hoạt động:

    • Khi bạn gõ BEGIN, CSDL tạo một "Transaction ID" (XID) duy nhất cho transaction để theo dõi các thay đổi.

    • Các thay đổi (XID, thông tin thao tác, trạng thái transaction) được ghi vào Write-Ahead Log (WAL) - file log tuần tự trên bộ nhớ bền vững, trước khi thực sự thay đổi dữ liệu trên đĩa nhằm.

  1. COMMIT:

  • Mục đích:

    • Đánh dấu sự kết thúc thành công của transaction.

    • Tất cả các thay đổi được thực hiện bởi transaction này sẽ được ghi nhận vĩnh viễn vào CSDL và được hiển thị với các transaction khác (tùy thuộc vào Isolation Level).

  • Cách thức hoạt động:

    • CSDL kiểm tra lại các ràng buộc, ghi một bản ghi "commit" vào WAL.

    • Đảm bảo rằng tất cả các bản ghi WAL liên quan đến transaction này đã được đẩy xuống đĩa (fsync). Đây là bước quan trọng cho Durability.

    • Các thay đổi dữ liệu thực tế trên các trang dữ liệu (data pages) có thể chưa được ghi ngay lập tức xuống đĩa chính (chúng nằm trong bộ nhớ đệm - shared buffers), nhưng vì WAL đã được ghi, hệ thống có thể khôi phục lại trạng thái sau commit nếu có sự cố.

    • CSDL giải phóng các tài nguyên mà transaction đang giữ và đánh dấu transaction là hoàn thành. Các phiên bản dữ liệu được tạo/thay đổi bởi transaction này có thể được "nhìn thấy" bởi các transaction khác.

  1. ROLLBACK:

  • Mục đích:

    • Hủy bỏ tất cả các thay đổi đã được thực hiện bởi transaction kể từ lệnh BEGIN gần nhất.

    • Database sẽ quay trở lại trạng thái như trước khi transaction bắt đầu. Đây là lệnh để kết thúc không thành công hoặc chủ động hủy bỏ một transaction.

  • Cách thức hoạt động:

    • Đối với các thay đổi, CSDL sẽ sử dụng thông tin hoàn tác (undo information) trong WAL để hoàn tác/đảo ngược các thay đổi đã thực hiện trong bộ nhớ hoặc trên đĩa nếu cần.

    • CSDL ghi một bản ghi "abort" hoặc "rollback" vào WAL và đánh dấu transaction là đã hủy (aborted). Các tài nguyên (như locks) mà transaction đang giữ được giải phóng.

c. Ví dụ Đơn Giản

Hãy tưởng tượng bạn muốn cập nhật email cho người dùng có id = 10:

-- Bắt đầu một transaction
BEGIN;

-- Thực hiện việc cập nhật
UPDATE users SET email = 'new.email@example.com' WHERE id = 10;

-- Giả sử bạn kiểm tra lại và thấy đúng
-- Xác nhận thay đổi
COMMIT;

-- HOẶC, nếu bạn phát hiện sai sót
-- Hủy bỏ thay đổi
-- ROLLBACK;

→ Nếu thực hiện COMMIT, email mới sẽ được lưu vĩnh viễn.

→ Nếu thực hiện ROLLBACK, email sẽ giữ nguyên như trước khi chạy lệnh UPDATE.


2. SAVEPOINT

a. SAVEPOINT là gì?

Một transaction có thể bao gồm nhiều bước phức tạp. Nếu một bước giữa chừng thất bại, bạn có thể không muốn hủy bỏ toàn bộ transaction mà chỉ muốn quay lại một điểm trước đó.

Một SAVEPOINT là một điểm đánh dấu bên trong một transaction đang diễn ra, cho phép rollback về chỉ điểm đó thay vì phải rollback toàn bộ transaction.

b. Các Lệnh Liên Quan

  • SAVEPOINT <sp_name>: Tạo một điểm lưu với tên <sp_name> tại vị trí hiện tại trong transaction.

  • ROLLBACK TO SAVEPOINT <sp_name>: Hủy bỏ tất cả các thay đổi và các SAVEPOINT được tạo sau SAVEPOINT <sp_name>, nhưng vẫn giữ lại các thay đổi trước SAVEPOINT đó. Transaction vẫn tiếp tục hoạt động sau lệnh này.

  • RELEASE SAVEPOINT <sp_name>: (Ít dùng) Xóa một SAVEPOINT đã định nghĩa.

c. Ví dụ:

Giả sử bạn tạo đơn hàng mới và thêm chi tiết sản phẩm:

BEGIN;

-- Thao tác 1
INSERT INTO orders (customer_id, order_date) VALUES (5, CURRENT_DATE);

-- Tạo điểm lưu trước khi thêm chi tiết đơn hàng
SAVEPOINT before_details;

-- Thao tác 2: Thêm chi tiết
INSERT INTO order_details (order_id, product_id, quantity) VALUES (LASTVAL(), 101, 2); 
INSERT INTO order_details (order_id, product_id, quantity) VALUES (LASTVAL(), 105, 1);

-- Thực hiện Rollback, 2 transaction ở thao tác 2 bị hủy bỏ nhưng giữ lại thao tác 1
ROLLBACK TO SAVEPOINT before_details;

-- Transaction vẫn đang tiếp tục ở trạng thái sau Thao tác 1
-- Bây giờ bạn có thể thêm sản phẩm khác hoặc làm gì đó khác
INSERT INTO order_details (order_id, product_id, quantity) VALUES (LASTVAL(), 102, 5);

-- Kết thúc thành công toàn bộ (Thao tác 1 và lần insert cuối cùng)
COMMIT;

→ CSDL sẽ có đơn hàng 123 và chi tiết đơn hàng cho sản phẩm 102 (số lượng 5). Các chi tiết cho sản phẩm 101 và 105 không được lưu do đã được ROLLBACK .

d. Cách thức hoạt động:

  • Khi SAVEPOINT được thực thi, CSDL ghi một bản ghi đặc biệt vào WAL để đánh dấu vị trí này.

  • Các thao tác khác sau savepoint được ghi vào log được gắn nhãn chúng với savepoint tương ứng để theo dõi.

  • Khi thực hiện ROLLBACK TO SAVEPOINT, hệ thống hoàn tác tất cả thay đổi được ghi lại sau savepoint dựa vào WAL và giải phóng các khóa (locks) đã được cấp phát (tùy thuộc vào loại CSDL).

  • Các savepoint được định nghĩa sau đó cũng bị vô hiệu hóa.


3. Transaction Isolation Levels

Đây là phần phức tạp nhưng cực kỳ quan trọng. Isolation Levels định nghĩa mức độ một transaction được "cách ly" khỏi các thay đổi được thực hiện bởi các transaction khác đang chạy đồng thời. Mức độ cách ly càng cao thì càng an toàn về dữ liệu nhưng có thể làm giảm hiệu năng (giảm khả năng chạy song song).

a. Tại sao cần Isolation? Các Hiện Tượng Không Mong Muốn

Khi nhiều transaction chạy cùng lúc mà không có sự cách ly đủ mạnh, các hiện tượng sau có thể xảy ra, dẫn đến dữ liệu sai lệch:

  1. Dirty Read

  • Transaction T1 đọc dữ liệu đã được sửa đổi bởi transaction T2 chưa được commit. Nếu transaction T2 bị ROLLBACK, transaction T1 đã đọc phải dữ liệu "bẩn", "không có thật".

  • Ví dụ: T2 cập nhật giá sản phẩm A thành 150 (chưa commit). T1 đọc giá sản phẩm A thấy 150. Sau đó T2 bị lỗi và rollback (giá trở về 100). T1 đã đọc và có thể đã sử dụng giá 150 không hợp lệ.

  1. Non-Repeatable Read

  • Transaction T1 đọc cùng một dòng dữ liệu nhiều lần và thấy giá trị khác nhau giữa các lần đọc, bởi vì một transaction T2 đã sửa đổi hoặc xóa dòng đó và COMMIT trước lần đọc tiếp theo của transaction T1.

  • Ví dụ: T1 đọc số lượng tồn kho sản phẩm B là 50. T2 thực hiện thành công một transaction cập nhật số lượng sản phẩm B còn 40. T1 đọc lại số lượng tồn kho sản phẩm B thấy còn 40. Dữ liệu T1 đọc không nhất quán trong cùng một transaction.

  1. Phantom Read

  • Transaction T1 thực hiện một truy vấn theo một số điều kiện nhất định để lấy một tập dữ liệu. Sau đó, transaction T2 thực hiện INSERT , DELETE vào các dữ liệu mà transaction T1 vừa lấy được. Nếu transaction T1 thực hiện lại cùng truy vấn trước đó, nó sẽ thấy một tập hợp kết quả khác (xuất hiện các dữ liệu mới và mất đi một số dữ liệu cũ - "bóng ma").

  • Ví dụ: T1 đếm số nhân viên ở phòng 'Sales' được 20 người. T2 thêm một nhân viên mới vào phòng 'Sales' và commit. T1 chạy lại lệnh đếm và thấy 21 người.

  1. Lost Update

  • Hai transaction cùng đọc một dữ liệu, cùng tính toán dựa trên dữ liệu đó, rồi cùng cập nhật lại. Cập nhật của transaction commit sau có thể ghi đè lên cập nhật của transaction commit trước, làm mất kết quả của transaction trước. Đây là vấn đề nghiêm trọng cần tránh.

b. Các Mức Độ Cô Lập (Isolation Levels) Tiêu Chuẩn

SQL định nghĩa 4 mức độ cô lập, từ thấp nhất đến cao nhất:

  1. READ UNCOMMITTED

  • Mô tả: Mức độ thấp nhất, gần như không có sự cách ly. Một giao dịch có thể đọc cả những thay đổi chưa được COMMIT bởi các giao dịch khác.

  • Ngăn chặn: Không ngăn chặn hiện tượng nào cả.

  • Cho phép: Dirty Read, Non-Repeatable Read, Phantom Read.

  • Use cases: READ UNCOMMITTED rất hiếm khi dùng trong thực tế vì độ rủi ro cao trừ một số tác vụ thống kê không yêu cầu độ chính xác tuyệt đối mà cần tốc độ nhanh nhất.

  • Cách thức hoạt động: Gần như không sử dụng cơ chế khóa (locking) khi đọc. Transaction đọc trực tiếp dữ liệu hiện có trong bộ nhớ đệm (buffer cache), bất kể trạng thái commit của giao dịch đã sửa đổi nó.

  1. READ COMMITTED

  • Mô tả: Một transaction chỉ có thể đọc những dữ liệu đã được COMMIT bởi các transaction khác.

  • Ngăn chặn: Dirty Read.

  • Cho phép: Non-Repeatable Read, Phantom Read.

  • Mức độ phổ biến: Đây là mức mặc định của nhiều CSDL phổ biến. Là sự cân bằng tốt giữa an toàn và hiệu năng cho nhiều ứng dụng.

  • Cách thức hoạt động:

    • Dùng khóa (Locking): Khi đọc dữ liệu, transaction yêu cầu một shared lock ngắn hạn trên dòng/trang dữ liệu đó. Khóa này ngăn không cho giao dịch khác lấy exclusive lock (khóa độc quyền) để sửa/xóa, nhưng chỉ giữ trong thời gian đọc câu lệnh hiện tại. Nó không ngăn giao dịch khác sửa đổi sau khi bạn đọc xong và trước khi bạn đọc lại. Khi một giao dịch muốn sửa đổi dữ liệu, nó phải đợi để lấy exclusive lock, và khóa này được giữ cho đến khi COMMIT hoặc ROLLBACK.

    • Dùng Đa phiên bản (Multi-Version Concurrency Control - MVCC): Các database như PostgreSQL, Oracle, và MySQL (InnoDB) thường dùng MVCC. Thay vì khóa khi đọc, mỗi câu lệnh SELECT sẽ thấy một snapshot của dữ liệu đã được commit tại thời điểm câu lệnh đó bắt đầu. Nếu một giao dịch khác commit thay đổi sau khi snapshot được tạo, câu lệnh SELECT hiện tại sẽ không thấy thay đổi đó. Tuy nhiên, một câu lệnh SELECT khác trong cùng giao dịch bắt đầu sau khi commit đó xảy ra sẽ thấy dữ liệu mới (dẫn đến Non-Repeatable Read).

  1. REPEATABLE READ

  • Mô tả: Đảm bảo rằng nếu một transaction đọc một dòng dữ liệu nhiều lần, nó sẽ luôn thấy cùng một giá trị. Transaction cũng chỉ thấy dữ liệu đã commit trước khi nó bắt đầu.

  • Ngăn chặn: Dirty Read, Non-Repeatable Read.

  • Cho phép: Phantom Read.

  • Cách thức hoạt động:

    • Toàn bộ transaction chỉ sử dụng một snapshot dữ liệu duy nhất, được chụp tại thời điểm câu lệnh SQL đầu tiên của transaction đó bắt đầu thực thi (Khác biệt chính so với READ COMMITTED)

    • Tất cả các câu lệnh SELECT trong transaction này đều nhìn vào snapshot đó. Nó sẽ không thấy các thay đổi được commit bởi các transaction khác sau khi transaction này bắt đầu. Điều này giúp ngăn chặn Non-Repeatable Read.

  • Lưu ý: Phantom Read vẫn có thể xảy ra, tương tự việc xung đột (conflict) giữa 2 commit khi đều cùng sửa vào 1 file khi sử dụng Git.

    • Ví dụ, transaction A đọc các dòng thỏa mãn điều kiện WHERE status = 'pending'. Một transaction B đã thực hiện INSERT một dòng mới cũng có status = 'pending' . Transaction A không thấy dòng mới này khi SELECT lại do kết quả vẫn lấy ra từ snapshot. Tuy nhiên, trong transaction A thực hiện việc cập nhật dữ liệu cho tất cả các dòng có status = 'pending'. Lúc này, CSDL nhận ra có dòng mới đã được commit sau bản snapshot và nếu việc UPDATE của transaction A xảy ra, nó có thể dẫn đến một xung đột tiềm ẩn khiến kết quả không nhất quán. CSDL ném ra lỗi serialization failure và buộc một trong hai transaction phải rollback và thử lại.

4. SERIALIZABLE

  • Mô tả: Mức cô lập cao nhấtan toàn nhất. Đảm bảo rằng kết quả của việc thực thi đồng thời nhiều transaction giống hệt như khi chúng được thực thi tuần tự (nối tiếp nhau).

  • Ngăn chặn: Dirty Read, Non-Repeatable Read, Phantom Read.

  • Cho phép: Không cho phép hiện tượng nào (về mặt lý thuyết).

  • Mức độ phổ biến: thường được sử dụng khi tính toàn vẹn dữ liệu là tuyệt đối quan trọng như các hệ thống tài chính, cực kỳ chính xác...

  • Cách thức hoạt động:

    • Dùng khóa (Strict Two-Phase Locking - S2PL): Sử dụng các cơ chế khóa mạnh mẽ hơn.

      • Ngoài việc giữ shared lock khi đọc và exclusive lock khi ghi cho đến cuối giao dịch (như Repeatable Read locking), nó còn sử dụng thêm các predicate locks hoặc range locks (khóa theo vị ngữ hoặc khoảng).

      • Khi bạn thực hiện SELECT ... WHERE condition, CSDL sẽ khóa không chỉ các dòng hiện có thỏa mãn điều kiện mà còn khóa cả "khoảng" hoặc "vị ngữ" đó để ngăn các giao dịch khác chèn thêm dòng mới thỏa mãn điều kiện (ngăn Phantom Reads). Việc này rất phức tạp và tốn kém.

    • Dùng MVCC với kiểm tra tuần tự hóa: Một số hệ hiện đại CSDL như PostgreSQL sử dụng một kỹ thuật tiên tiến gọi là Serializable Snapshot Isolation (SSI).

      • Các transaction vẫn hoạt động dựa trên một snapshot cho toàn bộ quá trình (tương tự REPEATABLE READ ).

      • SSI theo dõi các mối phụ thuộc đọc-ghi giữa các transaction đồng thời. Nếu nó phát hiện ra một "chu trình nguy hiểm" có thể vi phạm tính tuần tự (ví dụ: Tx1 đọc dữ liệu mà Tx2 sẽ ghi, và Tx2 đọc dữ liệu mà Tx1 sẽ ghi), nó sẽ chủ động gây ra lỗi serialization failure cho một trong các transaction liên quan, buộc nó phải rollback và thử lại

      • Bắt buộc phải có logic xử lý retry trong ứng dụng vì lỗi serialization failure có thể sẽ xảy ra thường xuyên

Hiệu năng của việc sử dụng SERIALIZABLE có thể bị ảnh hưởng nhiều nhất trong 4 mức. Nó làm giảm đáng kể khả năng chạy đồng thời và có thể tăng nguy cơ lỗi hoặc tắc nghẽn.

e. Cách Thiết Lập Isolation Level

Bạn có thể đặt mức cô lập cho transaction tiếp theo hoặc cho cả phiên làm việc:

-- Đặt cho transaction tiếp theo 
-- (phải đặt trước BEGIN hoặc là lệnh đầu tiên sau BEGIN)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- ... các lệnh SQL khác ...
COMMIT;

-- Hoặc đặt mặc định cho cả session hiện tại
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;

Cú pháp cụ thể có thể khác nhau giữa các hệ cơ sở dữ liệu

Tổng kết

Bảng Tóm Tắt Isolation Levels

Isolation LevelNgăn Chặn ĐượcCho Phép Vẫn Xảy RaMức Độ An ToànHiệu Năng Đồng ThờiMặc định ở đâu (phổ biến)Cần Lưu Ý Đặc Biệt
READ UNCOMMITTED(Không)Dirty, Non-Repeatable, PhantomRất thấpCao nhất(Hiếm)Hầu như không dùng
READ COMMITTEDDirty ReadNon-Repeatable, PhantomTrung bìnhCaoPostgreSQL, SQL Server, OracleCân bằng tốt
REPEATABLE READDirty, Non-RepeatablePhantomCaoTrung bìnhMySQL (InnoDB)Có thể gặp Phantom
SERIALIZABLEDirty, Non-Repeatable, Phantom(Không)Cao nhấtThấp(Không mặc định thường)Bắt buộc code retry lỗi
  1. Transaction (BEGIN, COMMIT, ROLLBACK): Giống như việc bạn "bắt đầu chỉnh sửa" (BEGIN), thực hiện thay đổi, rồi "Lưu lại" (COMMIT) hoặc "Hủy bỏ tất cả" (ROLLBACK). SAVEPOINT cho phép "Lưu tạm" một điểm để quay lại nếu cần.

  2. Isolation Levels: là quy tắc xem người khác có thể "nhìn" vào tài liệu bạn đang sửa hay không và khi nào họ thấy được thay đổi của bạn.

    • READ UNCOMMITTED: Ai cũng nhìn được bản nháp của bạn (nguy hiểm!).

    • READ COMMITTED: Người khác chỉ có thể thấy bản đã "Lưu" (COMMIT) lần cuối.

    • REPEATABLE READ: Người khác chỉ thấy bản tài liệu tại thời điểm bạn bắt đầu chỉnh sửa (BEGIN). Dù bạn có lưu tạm bao nhiêu lần, họ vẫn chỉ thấy bản gốc đó cho đến khi bạn "Lưu tất cả" (COMMIT). Tuy nhiên, họ có thể chỉnh sửa, thêm mới vào cùng phần mà bạn sẽ sửa.

    • SERIALIZABLE: Giống như bạn khóa cửa phòng làm việc, không ai khác vào được cho đến khi bạn làm xong và "Lưu tất cả". Mặc dù nó an toàn nhất nhưng cũng là chậm nhất.

0
Subscribe to my newsletter

Read articles from Ha Ngoc Hieu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ha Ngoc Hieu
Ha Ngoc Hieu