Transaction và ACID: giữ dữ liệu đúng khi mọi thứ có thể hỏng

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Chuyển 100 nghìn từ tài khoản A sang B gồm hai bước: trừ tiền A và cộng tiền B. Nếu hệ thống sập giữa hai bước, tiền biến mất. Transaction ra đời để bảo đảm một nhóm thao tác hoặc hoàn thành trọn vẹn, hoặc như chưa từng xảy ra.

Transaction hoạt động thế nào?

Bạn bắt đầu transaction, thực hiện các câu lệnh, rồi chốt (COMMIT) nếu mọi thứ ổn hoặc hủy (ROLLBACK) nếu có lỗi:

START TRANSACTION;UPDATE accounts SET balance = balance - 100000 WHERE id = 1;UPDATE accounts SET balance = balance + 100000 WHERE id = 2;COMMIT;  -- hoặc ROLLBACK nếu có sự cố

Trong MySQL, bảng cần dùng engine hỗ trợ transaction như InnoDB (mặc định hiện nay).

ACID là gì?

  • Atomicity (nguyên tử): tất cả hoặc không gì cả.
  • Consistency (nhất quán): dữ liệu đi từ trạng thái hợp lệ này sang trạng thái hợp lệ khác, không vi phạm ràng buộc (khóa ngoại, duy nhất, kiểm tra).
  • Isolation (cô lập): các transaction chạy đồng thời không làm nhiễu loạn nhau.
  • Durability (bền vững): đã commit thì dữ liệu được giữ lại ngay cả khi máy chủ sập sau đó.

Vấn đề khi nhiều transaction chạy đồng thời

Khi nhiều người cùng thao tác, có thể xảy ra:

  • Dirty read: đọc dữ liệu của transaction khác chưa commit.
  • Non-repeatable read: đọc cùng một dòng hai lần trong một transaction nhưng giá trị khác nhau vì người khác vừa sửa.
  • Phantom read: chạy cùng truy vấn hai lần nhưng số dòng thay đổi vì có dòng mới chèn vào.

Mức cô lập (isolation level) quyết định bạn chấp nhận hiện tượng nào, đổi lại là hiệu năng. Bốn mức theo chuẩn SQL gồm Read Uncommitted, Read Committed, Repeatable Read và Serializable; mức càng cao càng an toàn nhưng càng hạn chế đồng thời. MySQL InnoDB mặc định dùng Repeatable Read, còn PostgreSQL mặc định Read Committed.

Khóa dòng để tránh ghi đè

Tình huống kinh điển: hai người cùng mua món hàng chỉ còn một cái. Nếu cả hai đọc "còn 1" rồi cùng trừ, bạn bán lố. Giải pháp là khóa dòng khi đọc:

START TRANSACTION;SELECT stock FROM products WHERE id = 7 FOR UPDATE;-- kiểm tra stock > 0 ở tầng ứng dụngUPDATE products SET stock = stock - 1 WHERE id = 7;COMMIT;

FOR UPDATE khiến transaction khác muốn sửa dòng này phải chờ. Cách khác gọn hơn là kiểm tra điều kiện ngay trong câu cập nhật:

UPDATE products SET stock = stock - 1 WHERE id = 7 AND stock > 0;-- kiểm tra số dòng bị ảnh hưởng: 0 nghĩa là hết hàng

Dùng transaction trong code

// LaravelDB::transaction(function () use ($from, $to, $amount) {    $from->decrement('balance', $amount);    $to->increment('balance', $amount);});// Djangofrom django.db import transactionwith transaction.atomic():    ...

Nếu có ngoại lệ ném ra trong khối, framework tự rollback. Hãy giữ transaction ngắn: không gọi API bên ngoài, không chờ người dùng bên trong, vì giữ khóa lâu làm nghẽn cả hệ thống.

Deadlock

Deadlock xảy ra khi hai transaction mỗi bên giữ khóa mà bên kia cần. Database phát hiện và hủy một trong hai với lỗi. Cách giảm thiểu: luôn khóa các tài nguyên theo cùng một thứ tự, giữ transaction ngắn, và viết code thử lại khi gặp lỗi deadlock.

Transaction phân tán và những giới hạn

Transaction truyền thống chỉ bảo đảm trong phạm vi một database. Khi nghiệp vụ trải qua nhiều dịch vụ hoặc nhiều database, ví dụ trừ kho ở một dịch vụ, tính tiền ở dịch vụ khác, bạn không thể dùng một lệnh COMMIT chung. Lúc đó người ta dùng các mẫu như saga, trong đó mỗi bước có một thao tác bù trừ để hoàn tác nếu bước sau thất bại, hoặc chấp nhận nhất quán cuối cùng (eventual consistency). Đây là chủ đề nâng cao, nhưng chỉ cần biết giới hạn của transaction đơn đã giúp bạn thận trọng khi chia nhỏ hệ thống thành microservice.

Một mẹo thực tế nữa là luôn kiểm thử các tình huống đồng thời. Viết một kịch bản chạy hai tiến trình cùng mua món hàng cuối cùng, và xác nhận chỉ một bên thành công. Những lỗi liên quan đến đua dữ liệu hiếm khi lộ ra khi bạn tự bấm thử từng bước, nhưng sẽ xuất hiện ngay khi có hàng trăm người dùng thật.

Kết luận

Bất kỳ khi nào một nghiệp vụ gồm nhiều bước ghi liên quan nhau (đặt hàng, thanh toán, chuyển tiền), hãy bọc trong transaction. Đồng thời hiểu mức cô lập và khóa dòng để xử lý các tình huống đồng thời. Đó là nền tảng của dữ liệu đáng tin cậy.

Nếu muốn luyện tập cùng bài tập thực tế, hãy ghé các khóa học tại codebem.com khi bạn sẵn sàng.

author_photo
Nguyễn Đình Cường

0 comment