8 phút

Cách các đảm bảo ACID định hình hệ thống giao dịch đáng tin cậy

Tìm hiểu cách các đảm bảo ACID ảnh hưởng đến thiết kế cơ sở dữ liệu và hành vi ứng dụng. Khám phá tính nguyên tử, nhất quán, cô lập, bền vững, các đánh đổi và ví dụ thực tế.

Cách các đảm bảo ACID định hình hệ thống giao dịch đáng tin cậy

Ý nghĩa của “ACID” với các giao dịch hàng ngày

Khi bạn trả tiền mua hàng, đặt vé máy bay, hoặc chuyển tiền giữa các tài khoản, bạn mong kết quả rõ ràng: hoặc thành công, hoặc thất bại. Cơ sở dữ liệu cố gắng cung cấp sự chắc chắn tương tự — ngay cả khi nhiều người dùng truy cập cùng lúc, máy chủ chết, hoặc mạng chập chờn.

Giao dịch, nói một cách đơn giản

Một giao dịch là một đơn vị công việc mà cơ sở dữ liệu coi như một “gói” duy nhất. Nó có thể gồm nhiều bước — trừ tồn kho, tạo đơn hàng, thu thẻ, và ghi biên nhận — nhưng mục tiêu là hành xử như một hành động liên kết.

Nếu bất kỳ bước nào thất bại, hệ thống nên quay về điểm an toàn thay vì để lại một mớ nửa chừng.

Tại sao cập nhật một phần gây ra vấn đề lớn cho doanh nghiệp

Cập nhật một phần không chỉ là lỗi kỹ thuật; chúng trở thành phiền toái cho bộ phận hỗ trợ và rủi ro tài chính. Ví dụ:

  • Thanh toán bị thu nhưng đơn hàng không được tạo — khách hàng bị trừ tiền mà không có xác nhận.
  • Đơn hàng được tạo nhưng tồn kho không giảm — trang của bạn bán quá mức và bạn phải huỷ sau đó.
  • Chuyển khoản trừ tài khoản này nhưng không cộng tài khoản kia — số dư không còn hợp lý.

Những lỗi này khó gỡ vì mọi thứ nhìn có vẻ “hầu như đúng”, nhưng các con số không khớp.

ACID là tập các đảm bảo (không phải một sản phẩm)

ACID là từ viết tắt cho bốn đảm bảo mà nhiều cơ sở dữ liệu có thể cung cấp cho giao dịch:

  • Atomicity: thực thi tất cả hoặc không bước nào
  • Consistency: dữ liệu tuân theo các quy tắc hợp lệ
  • Isolation: các giao dịch đồng thời không can thiệp theo cách không an toàn
  • Durability: một khi commit, thay đổi tồn tại

Nó không phải là một nhãn hiệu cơ sở dữ liệu cụ thể hay một tính năng bạn bật/tắt; đó là một cam kết về hành vi.

Lợi ích — và chi phí bạn nên mong đợi

Mức đảm bảo mạnh hơn thường đòi hỏi cơ sở dữ liệu phải làm nhiều việc hơn: phối hợp thêm, chờ khoá, theo dõi phiên bản, và ghi vào nhật ký. Điều đó có thể làm giảm thông lượng hoặc tăng độ trễ khi tải cao. Mục tiêu không phải là “ACID tối đa mọi lúc”, mà là chọn mức đảm bảo phù hợp với rủi ro thực tế của doanh nghiệp bạn.

Tính nguyên tử: Cập nhật tất cả hoặc không

Tính nguyên tử nghĩa là một giao dịch được xem như một đơn vị công việc: nó hoặc hoàn tất hoàn toàn, hoặc không có hiệu lực. Bạn không bao giờ thấy “nửa cập nhật” trong cơ sở dữ liệu.

Ví dụ chuyển tiền đơn giản

Giả sử chuyển $50 từ Alice sang Bob. Ở mức kỹ thuật, thường có ít nhất hai thay đổi:

  • Trừ $50 khỏi số dư của Alice
  • Cộng $50 vào số dư của Bob

Với tính nguyên tử, hai thay đổi đó thành công cùng nhau hoặc thất bại cùng nhau. Nếu hệ thống không thể thực hiện an toàn cả hai, nó phải không làm gì cả. Điều này ngăn kịch bản tồi tệ khi Alice bị trừ tiền nhưng Bob không nhận được (hoặc ngược lại).

Commit và rollback (nói dễ hiểu)

Cơ sở dữ liệu cho giao dịch hai lối thoát:

  • Commit: “Tất cả các bước thành công; công bố kết quả.”
  • Rollback: “Có lỗi; hoàn tác mọi thứ trong giao dịch này.”

Một mô hình hữu ích là “nháp vs. xuất bản”. Khi giao dịch đang chạy, các thay đổi là tạm thời. Chỉ commit mới xuất bản chúng.

Những gì có thể xảy ra giữa chừng?

Tính nguyên tử quan trọng vì thất bại là bình thường:

  • Ứng dụng crash: dịch vụ dừng sau khi cập nhật một bảng nhưng trước khi cập nhật bảng kia.
  • Mất mạng: ứng dụng không tới được cơ sở dữ liệu, hoặc client không nhận được phản hồi “thành công”.
  • Mất điện: máy chủ cơ sở dữ liệu dừng đột ngột.

Nếu bất kỳ điều này xảy ra trước khi commit hoàn tất, tính nguyên tử đảm bảo cơ sở dữ liệu có thể rollback để tránh rò rỉ công việc một phần vào số dư thực.

Tính nguyên tử kết hợp idempotency và retry

Tính nguyên tử bảo vệ trạng thái DB, nhưng ứng dụng vẫn phải xử lý sự không chắc chắn — đặc biệt khi mất mạng khiến bạn không biết commit đã diễn ra hay chưa.

Hai biện pháp thực tế:

  • Retry: lặp lại yêu cầu khi không nhận được phản hồi.
  • Idempotency: làm cho việc lặp lại cùng yêu cầu an toàn (ví dụ dùng khóa idempotency để “chuyển #123” chỉ được áp dụng tối đa một lần).

Kết hợp giao dịch nguyên tử và retry idempotent giúp bạn tránh cả cập nhật một phần lẫn trùng lặp tính phí.

Tính nhất quán: Giữ dữ liệu trong các quy tắc hợp lệ

Trong ACID, tính nhất quán không phải là “dữ liệu trông hợp lý” hay “tất cả bản sao giống nhau”. Nó nghĩa là mỗi giao dịch phải đưa DB từ một trạng thái hợp lệ sang một trạng thái hợp lệ — theo các quy tắc bạn đặt ra.

Tính nhất quán do bạn định nghĩa

Cơ sở dữ liệu chỉ có thể giữ tính nhất quán so với các ràng buộc, trigger và bất biến mà bạn khai báo. ACID không tạo ra những quy tắc này; nó thực thi chúng trong giao dịch.

Ví dụ phổ biến:

  • Foreign keys: mọi order.customer_id phải trỏ tới một khách hàng tồn tại.
  • Unique constraints: hai người dùng không được cùng email.
  • Check constraint / bất biến: số dư tài khoản không được âm, hay số lượng hàng không thể âm.

Nếu các quy tắc này tồn tại, DB sẽ từ chối giao dịch vi phạm — nên bạn không có dữ liệu “một phần hợp lệ”.

Kiểm tra ở ứng dụng so với ràng buộc ở DB

Kiểm tra ở app quan trọng nhưng không đủ:

  • Kiểm tra ở app cải thiện UX (thông báo lỗi rõ ràng, phản hồi sớm) và có thể thi hành quy tắc nghiệp vụ phức tạp.
  • Ràng buộc DB là cổng chốt — đặc biệt khi nhiều dịch vụ, job nền, import, hoặc công cụ admin ghi vào cùng bảng.

Một lỗi kinh điển là kiểm tra ở app (“email còn”) rồi chèn bản ghi. Dưới cạnh tranh, hai request có thể cùng vượt qua kiểm tra. Ràng buộc unique ở DB mới đảm bảo chỉ có một insert thành công.

Tính nhất quán trông như thế nào trong thực tế

Nếu bạn mã hoá “không âm số dư” bằng một ràng buộc (hoặc đảm bảo nó trong một giao dịch đơn), thì mọi chuyển tiền dẫn đến âm đều phải thất bại toàn bộ. Nếu bạn không mã hoá quy tắc này ở đâu cả, ACID không thể bảo vệ — vì không có gì để thi hành.

Tính nhất quán là về rõ ràng: định nghĩa quy tắc, rồi để giao dịch đảm bảo chúng không bị phá vỡ.

Cô lập: Hoạt động an toàn khi có nhiều người cùng làm việc

Cô lập đảm bảo các giao dịch không đạp lên nhau. Khi một giao dịch đang diễn ra, các giao dịch khác không nên thấy công việc nửa chừng hoặc vô tình ghi đè. Mục tiêu: mỗi giao dịch nên hành xử như thể nó chạy một mình, dù nhiều người dùng cùng hoạt động.

Tại sao đồng thời làm cho việc này khó

Hệ thống thực tế rất bận rộn: khách đặt hàng, nhân viên hỗ trợ cập nhật hồ sơ, job nền đối chiếu thanh toán — tất cả cùng lúc. Những hành động này chồng chéo thời gian và thường chạm vào cùng các hàng (số dư tài khoản, số lượng tồn kho, slot đặt phòng).

Không có cô lập, thời điểm trở thành phần của logic nghiệp vụ. Một cập nhật “trừ tồn kho” có thể tranh nhau với một checkout khác, hoặc báo cáo có thể đọc dữ liệu giữa lúc thay đổi và hiển thị con số không tồn tại trong trạng thái ổn định.

Cô lập thường có thể cấu hình

Cô lập hoàn toàn (“giả sử bạn đang độc quyền”) có thể tốn kém. Nó giảm thông lượng, tăng chờ đợi (khóa), hoặc gây retry. Trong khi đó, nhiều luồng công việc không cần bảo vệ nghiêm ngặt nhất — đọc số liệu thống kê hôm qua chẳng hạn có thể chấp nhận nhất quán nhẹ.

Vì vậy, DB cung cấp mức cô lập cấu hình: bạn chọn rủi ro đồng thời bao nhiêu để đổi lấy hiệu năng tốt hơn và ít xung đột hơn.

Nhìn nhanh: các bất thường mà cô lập ngăn chặn (hoặc cho phép)

Khi cô lập quá yếu cho workload của bạn, bạn sẽ gặp các bất thường cổ điển:

  • Dirty reads: đọc thay đổi chưa commit của giao dịch khác.
  • Lost updates: hai giao dịch ghi đè nhau khiến một thay đổi biến mất.
  • Phantom reads: chạy lại truy vấn trả về tập hàng khác do giao dịch khác chèn/xóa hàng khớp.

Hiểu các chế độ lỗi này giúp bạn chọn mức cô lập phù hợp với cam kết sản phẩm.

Các bất thường phổ biến mà cô lập ngăn chặn (hoặc cho phép)

Làm cho việc retry an toàn
Tạo nhanh các endpoint không gây lỗi khi lặp lại bằng mẫu API có thể chỉnh sửa và kiểm thử.

Cô lập quyết định giao dịch của bạn được phép “thấy” gì khi giao dịch khác vẫn chạy. Khi cô lập quá yếu, bạn có thể thấy các hành vi hợp lý về mặt kỹ thuật nhưng gây bất ngờ cho người dùng.

Bất thường khi đọc

Dirty read xảy ra khi bạn đọc dữ liệu một giao dịch khác đã ghi nhưng chưa commit.

Kịch bản: Alex chuyển $500 ra khỏi tài khoản, số dư tạm thời là $200, bạn đọc thấy $200 trước khi giao dịch của Alex sau đó thất bại và rollback.

Hệ quả: khách thấy số dư thấp không đúng, quy tắc gian lận có thể kích hoạt nhầm, hoặc nhân viên hỗ trợ trả lời sai.

Non-repeatable read nghĩa là bạn đọc cùng một hàng hai lần và nhận giá trị khác vì giao dịch khác đã commit giữa chừng.

Kịch bản: bạn tải tổng đơn ($49.00), rồi làm mới và thấy $54.00 vì một dòng giảm giá bị xoá.

Hệ quả: “Tổng của tôi thay đổi khi tôi thanh toán”, gây mất niềm tin hoặc bỏ giỏ hàng.

Phantom read giống non-repeatable nhưng ở mức tập hàng: truy vấn lần hai trả về hàng thêm/bớt do giao dịch khác chèn/xóa.

Kịch bản: tìm phòng khách sạn thấy “3 phòng trống”, lúc check lại thì không còn vì các đặt chỗ mới xuất hiện.

Hệ quả: đặt chỗ bị trùng, giao diện hiển thị không nhất quán, hoặc bán quá mức.

Bất thường khi ghi (lỗi thực tế hay gặp)

Lost update xảy ra khi hai giao dịch đọc cùng một giá trị và đều ghi trở lại, với ghi sau cùng ghi đè ghi trước.

Kịch bản: hai admin sửa giá sản phẩm. Cả hai bắt đầu từ $10; một lưu $12, người kia lưu $11 sau cùng.

Hệ quả: thay đổi của ai đó biến mất; tổng và báo cáo sai.

Write skew xảy ra khi hai giao dịch mỗi cái làm thay đổi hợp lệ riêng, nhưng cùng nhau vi phạm một quy tắc.

Kịch bản: Quy tắc: “Ít nhất một bác sĩ trực phải luôn được phân công.” Hai bác sĩ độc lập đều ghi off-call sau khi kiểm thấy người kia vẫn trực.

Hệ quả: bạn kết thúc với không ai trực, dù mỗi giao dịch khi chạy độc lập đều “hợp lệ”.

Tại sao không luôn dùng mức cô lập chặt nhất?

Cô lập mạnh hơn giảm bất thường nhưng có thể tăng chờ, retry và chi phí khi đồng thời cao. Nhiều hệ thống chọn cô lập yếu hơn cho phân tích nhiều đọc, trong khi dùng cấu hình chặt cho chuyển tiền, đặt chỗ và các luồng quan trọng về đúng đắn.

Các mức cô lập: chọn mức bảo vệ phù hợp

Cô lập quyết định giao dịch của bạn được phép “thấy” gì khi các giao dịch khác chạy. DB biểu diễn điều này bằng mức cô lập: mức cao hơn giảm hành vi ngạc nhiên nhưng có thể ảnh hưởng thông lượng và tăng chờ.

Các mức phổ biến

  • Read Uncommitted: bạn có thể đọc thay đổi chưa commit của giao dịch khác (“dirty reads”). Hầu như không có gì được ngăn chặn.
  • Read Committed: bạn chỉ đọc dữ liệu đã commit, nên dirty reads được ngăn. Nhưng chạy cùng truy vấn hai lần có thể trả về khác nhau vì người khác commit giữa chừng (“non-repeatable reads”).
  • Repeatable Read: các lần đọc bạn đã thực hiện giữ nguyên trong giao dịch, nên non-repeatable reads thường bị ngăn. Tùy engine, bạn vẫn có thể thấy “phantom” hoặc không.
  • Serializable: giao dịch hành xử như chạy lần lượt từng cái một. Đây là mức mạnh nhất, thường ngăn dirty reads, non-repeatable reads và phantoms, và giảm nhiều bất thường ghi tinh vi.

Chọn mức: thông lượng vs. đúng đắn

Các đội thường chọn Read Committed làm mặc định cho app hướng người dùng: hiệu năng tốt và “không đọc bẩn” đáp ứng phần lớn kỳ vọng.

Dùng Repeatable Read khi bạn cần kết quả ổn định trong giao dịch (ví dụ sinh hóa đơn) và chịu được chi phí. Dùng Serializable khi đúng đắn quan trọng hơn đồng thời (ví dụ đảm bảo không bán quá mức) hoặc khi bạn không thể dễ dàng lý giải race condition trong code ứng dụng.

Read Uncommitted hiếm ở hệ OLTP; đôi khi dùng cho giám sát hoặc báo cáo gần đúng.

Cảnh báo quan trọng: hành vi khác nhau

Tên các mức được chuẩn hóa, nhưng đảm bảo chính xác khác nhau theo engine (và đôi khi theo cấu hình). Hãy tham khảo tài liệu DB và kiểm thử các bất thường quan trọng với nghiệp vụ của bạn.

Bền vững: Làm cho commit tồn tại

Bền vững nghĩa là khi một giao dịch đã commit, kết quả của nó sẽ sống sót qua sự cố — mất điện, restart process, hoặc reboot máy. Nếu app báo khách “thanh toán thành công”, bền vững là lời hứa cơ sở dữ liệu sẽ không “quên” điều đó sau sự cố.

Cách DB làm cho commit tồn tại

Hầu hết DB quan hệ dùng write-ahead logging (WAL). Ở mức cao, DB ghi một “biên nhận” tuần tự các thay đổi vào log trên đĩa trước khi coi giao dịch là đã commit. Nếu DB crash, nó có thể replay log khi khởi động để khôi phục các thay đổi đã commit.

Để giữ thời gian phục hồi trong tầm kiểm soát, DB cũng tạo checkpoint. Checkpoint là thời điểm DB đảm bảo đủ thay đổi gần đây được ghi vào các file dữ liệu chính, nên recovery không cần replay lượng log vô hạn.

Bền vững phụ thuộc vào lưu trữ và cấu hình

Bền vững không phải công tắc bật/tắt; nó phụ thuộc vào mức độ DB ép dữ liệu vào lưu trữ bền vững.

  • Với cấu hình đồng bộ, DB chờ log được flush (thường qua fsync) trước khi xác nhận commit. An toàn hơn nhưng có thể tăng độ trễ.
  • Với cấu hình bất đồng bộ, DB có thể xác nhận commit trước khi log thực sự bền vững trên đĩa. Tốc độ cải thiện nhưng crash có thể mất các giao dịch “đã commit” gần đây.

Phần cứng cũng quan trọng: SSD, RAID controller với cache ghi, và ổ đám mây có hành vi khác nhau khi thất bại.

Sao lưu và nhân bản liên quan nhưng khác biệt

Sao lưu và replication giúp bạn khôi phục hoặc giảm thời gian chết, nhưng không giống durability. Một giao dịch có thể bền trên primary ngay cả khi chưa tới replica, và sao lưu thường là snapshot theo thời điểm thay vì đảm bảo từng commit.

DB thực thi ACID bên trong như thế nào

Ngăn các lỗi tranh chấp phổ biến
Khởi tạo hệ thống đặt chỗ hoặc quản lý tồn kho tránh được việc bán quá mức và mất cập nhật.

Khi bạn BEGIN một giao dịch rồi COMMIT, DB điều phối nhiều phần: ai được đọc hàng nào, ai được cập nhật, và chuyện gì xảy ra nếu hai người cùng sửa một bản ghi.

Kiểm soát đồng thời: pessimistic vs optimistic

Một lựa chọn then chốt là xử lý xung đột thế nào:

  • Pessimistic locking giả định xung đột hay xảy ra. Khi giao dịch cập nhật hàng, DB khóa nó để giao dịch khác phải chờ. Điều này ngăn nhiều bất thường nhưng có thể gây blocking.
  • Optimistic giả định xung đột hiếm. Giao dịch chạy thoải mái, DB phát hiện xung đột lúc commit (hoặc bằng kiểm tra) và có thể bác bỏ một giao dịch để retry.

Nhiều hệ kết hợp hai cách tuỳ workload và mức cô lập.

MVCC: đọc không chặn ghi

Các DB hiện đại thường dùng MVCC (Multi-Version Concurrency Control): thay vì chỉ giữ một bản của hàng, DB giữ nhiều phiên bản.

  • Reader thấy snapshot nhất quán (phiên bản cũ) mà không phải chờ.
  • Writer tạo phiên bản mới trong khi đọc vẫn diễn ra.

Đây là lý do chính giúp một số DB xử lý nhiều đọc và ghi đồng thời ít blocking hơn — dù xung đột ghi/ghi vẫn cần giải quyết.

Deadlock: khi vòng chờ hình thành

Khoá có thể dẫn tới deadlock: Transaction A chờ khoá do B nắm, trong khi B chờ khoá do A. DB thường giải quyết bằng cách phát hiện vòng và abort một giao dịch (nạn nhân deadlock), trả lỗi để app retry.

Dấu hiệu thực tế cho thấy có vấn đề

Nếu việc thực thi ACID gây ma sát, bạn thường thấy:

  • Lock waits tăng lên trong giờ cao điểm
  • Timeouts (truy vấn lỗi sau khi chờ quá lâu)
  • Điểm nóng contention (vài hàng/bảng bị cập nhật liên tục, như bộ đếm hoặc trường “last seen”)

Những triệu chứng này thường báo cần xem lại kích thước giao dịch, indexing, hoặc chiến lược cô lập/khoá phù hợp với workload.

ACID ảnh hưởng đến thiết kế ứng dụng ra sao

ACID không chỉ là lý thuyết DB — nó ảnh hưởng cách bạn thiết kế API, job nền và cả luồng UI. Ý chính: quyết định bước nào phải thành công cùng nhau, rồi chỉ bọc những bước đó trong một giao dịch.

Thiết kế API quanh “một thay đổi nghiệp vụ”

Một API giao dịch tốt thường ánh xạ tới một hành động nghiệp vụ duy nhất, dù nó chạm nhiều bảng. Ví dụ, /checkout có thể: tạo đơn hàng, giữ tồn kho, và ghi ý định thanh toán. Những ghi DB đó thường nên nằm trong một giao dịch để commit cùng nhau (hoặc rollback cùng nhau) nếu có lỗi xác thực.

Mẫu hay dùng:

  • Kiểm tra input trước khi mở giao dịch.
  • Mở giao dịch.
  • Thực hiện tối thiểu các đọc/ghi cần thiết.
  • Commit.

Điều này giữ tính nguyên tử và nhất quán trong khi tránh giao dịch chậm, mong manh.

Ranh giới giao dịch trong request, service và job

Nơi bạn đặt ranh giới giao dịch phụ thuộc vào “một đơn vị công việc” nghĩa là gì:

  • Yêu cầu người dùng: Giữ giao dịch ngắn — lý tưởng vài truy vấn. Đừng giữ khoá khi render view hoặc chờ phản hồi bên ngoài.
  • Job nền: Xử lý mỗi lần chạy job như một đơn vị. Nếu job xử lý 10.000 bản ghi, commit theo lô để dễ restart an toàn.
  • Ranh giới service: Ưu tiên giữ giao dịch trong DB của một service. Qua nhiều service thường cần cách khác (như outbox), vì một giao dịch ACID khó bao phủ nhiều DB.

Xử lý lỗi: rollback, retry và replay an toàn

ACID hữu ích, nhưng app vẫn phải xử lý lỗi đúng:

  • Rollback khi lỗi: Nếu bất kỳ bước nào thất bại, abort giao dịch để không rò rỉ cập nhật một phần.
  • Retry khi lỗi tạm thời: Serialization failure và deadlock là bình thường. Retry toàn bộ giao dịch thường là cách đúng.
  • Làm cho thao tác idempotent: Nếu request được retry (từ client hoặc job runner), bạn nên có thể replay an toàn mà không bị trùng — dùng idempotency key và ràng buộc unique.

Các anti-pattern thường gặp

Tránh giao dịch dài, gọi API bên ngoài trong giao dịch, và để user chờ trong giao dịch (ví dụ, “khóa hàng trong giỏ, chờ user xác nhận”). Những điều này tăng contention và làm xung đột cô lập dễ xảy ra.

Nơi công cụ có thể giúp (không thay đổi nguyên tắc)

Khi bạn xây hệ thống giao dịch nhanh, rủi ro lớn nhất hiếm khi là “không biết ACID” — mà là vô tình phân tán một hành động nghiệp vụ qua nhiều endpoint, job, hoặc bảng mà không có ranh giới giao dịch rõ ràng.

Nền tảng như Koder.ai có thể giúp bạn đi nhanh hơn trong khi vẫn thiết kế quanh ACID: bạn mô tả workflow (ví dụ “checkout với giữ tồn kho và ý định thanh toán”) trong chat theo hướng lập kế hoạch, sinh UI React cùng backend Go + PostgreSQL, và lặp bằng snapshot/rollback nếu schema hoặc ranh giới giao dịch cần thay đổi. Cơ sở dữ liệu vẫn cưỡng chế các đảm bảo; giá trị là rút ngắn con đường từ thiết kế đúng đến triển khai hoạt động.

ACID trong hệ phân tán và nhiều service

Kiểm thử đồng thời sớm
Mô phỏng deadlock, retry và giao dịch ngắn trong một dự án thực tế có thể chạy được.

Một DB đơn thường có thể cung cấp ACID trong một ranh giới giao dịch. Khi bạn chia công việc ra nhiều service (và thường nhiều DB), các đảm bảo tương tự trở nên khó giữ — và tốn kém hơn khi cố giữ.

Nhất quán vs khả dụng: trao đổi bạn cảm nhận trong thực tế

Nhất quán chặt nghĩa là mọi đọc thấy “sự thật commit mới nhất.” Khả dụng cao nghĩa là hệ vẫn phản hồi ngay cả khi một phần chậm hoặc không liên lạc được.

Trong multi-service, vấn đề tạm thời về mạng có thể buộc bạn chọn: chặn hoặc thất bại request cho tới khi mọi thành phần đồng ý (nhất quán hơn, ít khả dụng hơn), hoặc chấp nhận dịch vụ tạm thời lệch nhau (khả dụng hơn, nhất quán kém hơn). Không có lựa chọn luôn đúng — tuỳ mức lỗi doanh nghiệp chấp nhận.

Tại sao giao dịch phân tán khó

Giao dịch phân tán cần phối hợp qua biên giới bạn không kiểm soát hoàn toàn: độ trễ mạng, retry, timeout, crash service, và thất bại một phần.

Ngay cả khi mọi service đúng, mạng có thể tạo ra mơ hồ: service thanh toán đã commit nhưng service đơn hàng không nhận được xác nhận? Để giải quyết an toàn, hệ dùng giao thức phối hợp (như two-phase commit), nhưng chúng có thể chậm, giảm khả dụng khi lỗi, và tăng độ phức tạp vận hành.

Mẫu thực tế thay thế “một giao dịch lớn”

Sagas chia workflow thành bước, mỗi bước commit cục bộ. Nếu bước sau thất bại, các bước trước được “bù trừ” bằng hành động đối nghịch (ví dụ hoàn tiền).

Outbox/inbox làm cho xuất bản sự kiện và tiêu thụ tin cậy. Service ghi dữ liệu nghiệp vụ và một bản ghi “sẽ xuất bản” trong cùng giao dịch cục bộ (outbox). Consumer ghi ID thông đi xử lý (inbox) để retry mà không trùng lặp.

Eventual consistency chấp nhận khoảng thời gian ngắn dữ liệu khác nhau giữa các service, với kế hoạch đồng bộ hoá sau.

Khi nào nên nới lỏng đảm bảo — và cách kiểm soát rủi ro

Nới lỏng khi:

  • Bạn chấp nhận lệch tạm thời (trạng thái vận chuyển trễ sau tạo đơn)
  • Bạn có thể sửa lỗi bằng bù trừ (hoàn tiền, huỷ)
  • Độ trễ và thời gian hoạt động quan trọng hơn tính nhất quán tức thời

Kiểm soát rủi ro bằng cách định nghĩa bất biến (các gì không bao giờ được vi phạm), thiết kế thao tác idempotent, dùng timeout và retry với backoff, và giám sát drift (sagas bị treo, bù trừ lặp lại, bảng outbox lớn dần). Với bất biến thực sự quan trọng (ví dụ “không chi tiêu vượt quá số dư”), giữ chúng trong một service duy nhất và một giao dịch DB duy nhất khi có thể.

Checklist thực tế: Thiết kế, kiểm thử và giám sát hệ ACID

Một giao dịch có thể đúng trong unit test nhưng thất bại dưới tải thực, restart, và cạnh tranh. Dùng checklist này để đảm bảo đảm bảo ACID phù hợp với hành vi ở production.

1) Thiết kế: Định nghĩa bất biến và ranh giới giao dịch

Bắt đầu bằng viết ra những gì phải luôn đúng (các bất biến dữ liệu). Ví dụ: “số dư không âm”, “tổng đơn bằng tổng dòng”, “tồn kho không âm”, “một thanh toán liên kết đúng một đơn hàng.” Xem đó là quy tắc sản phẩm, không chỉ chi tiết DB.

Rồi quyết định cái gì phải nằm trong một giao dịch và cái gì có thể hoãn.

  • Bất biến dữ liệu: liệt kê bảng/hàng liên quan và quy tắc chính xác.
  • Kịch bản thất bại: crash giữa yêu cầu, timeout mạng sau commit, retry gây trùng, failover replica, đĩa đầy.
  • Hồ sơ cạnh tranh: thao tác nào chạy song song, nơi có contention, và liệu đọc có cần “ngay bây giờ” hay cho phép hơi cũ.

Giữ giao dịch nhỏ: chạm ít hàng hơn, làm ít việc hơn (không gọi API ngoài), và commit nhanh.

2) Kiểm thử: Chứng minh hành vi dưới race và lỗi

Làm cho cạnh tranh thành một chiều kích test chính.

  • Test race-condition: chạy cùng lúc thao tác quan trọng (ví dụ hai checkout cho món hàng cuối) và assert bất biến không bị phá vỡ.
  • Fault injection: kill process app giữa giao dịch; chèn timeout; giả retry; ép restart DB; xác minh kết quả hoặc đã commit một lần hoặc rollback an toàn.
  • Load test với kiểm tra đúng đắn: dưới tải cao, kiểm tra không chỉ độ trễ mà cả tổng số, đếm, và “không có bản sao” vi phạm.

Nếu bạn hỗ trợ retry, thêm khóa idempotency rõ ràng và test “yêu cầu lặp lại sau khi thành công.”

3) Giám sát: Phát hiện vấn đề ACID trước người dùng

Theo dõi chỉ báo cho thấy đảm bảo của bạn đang trở nên tốn kém hoặc mong manh:

  • Lock waits và thời gian chờ hàng đợi (tăng contention)
  • Deadlocks (tần suất, truy vấn bị chọn làm nạn nhân)
  • Giao dịch chạy lâu (thường là nguyên nhân gốc)
  • Replication lag (đọc cũ và failover chậm)
  • Thời gian commit/fsync (áp lực lưu trữ; chi phí durability)

Cảnh báo theo xu hướng hơn là chỉ đột biến, và gắn các chỉ số này với endpoint hoặc job gây ra.

Nguyên tắc: cô lập và phạm vi giao dịch

Dùng mức cô lập yếu nhất nhưng vẫn bảo vệ bất biến của bạn; đừng mặc định “max” nó. Khi cần đúng đắn cho đoạn nhỏ quan trọng (chuyển tiền, giảm tồn kho), thu hẹp giao dịch vào đúng đoạn đó và giữ mọi thứ khác ở ngoài.

Câu hỏi thường gặp

ACID có ý nghĩa gì trong cơ sở dữ liệu, về mặt thực tế?

ACID là tập hợp các đảm bảo giao dịch giúp cơ sở dữ liệu hoạt động có thể đoán trước khi có lỗi và truy cập đồng thời xảy ra:

  • Atomicity: tất cả bước thành công hoặc không bước nào thành công
  • Consistency: mỗi commit bảo toàn các quy tắc/ràng buộc bạn định nghĩa
  • Isolation: các giao dịch đồng thời không tạo ra can thiệp không an toàn
  • Durability: các thay đổi đã commit còn tồn tại sau sự cố
Giao dịch là gì và tại sao nó quan trọng?

Giao dịch là một “đơn vị công việc” mà cơ sở dữ liệu xử lý như một gói duy nhất. Dù thực hiện nhiều câu lệnh SQL (ví dụ: tạo đơn hàng, giảm tồn kho, ghi ý định thanh toán), giao dịch chỉ có hai kết quả:

  • Commit: tất cả thay đổi trở nên chính thức
  • Rollback: không có thay đổi nào có hiệu lực
Tại sao các cập nhật một phần lại là vấn đề lớn cho doanh nghiệp?

Vì các cập nhật một phần tạo ra những mâu thuẫn thực tế khó sửa sau này, ví dụ:

  • khách hàng bị trừ tiền nhưng không có đơn hàng
  • đơn hàng có nhưng tồn kho không giảm (bán quá mức)
  • một phía của chuyển khoản đã áp dụng nhưng phía kia không

ACID (đặc biệt là tính nguyên tử + tính nhất quán) ngăn chặn trạng thái “bán dở” này xuất hiện như sự thật.

Tính nguyên tử ngăn chặn các thao tác nửa chừng như thế nào?

Tính nguyên tử đảm bảo cơ sở dữ liệu không bao giờ lộ ra một giao dịch “nửa chừng”. Nếu có lỗi trước commit — crash ứng dụng, mất mạng, restart DB — giao dịch sẽ bị rollback để các bước trước đó không rò rỉ vào trạng thái bền vững.

Thực tiễn: tính nguyên tử làm cho các thay đổi nhiều bước (ví dụ chuyển tiền cập nhật hai số dư) an toàn.

Nếu ACID an toàn, tại sao tôi vẫn cần idempotency và retry?

Bạn có thể không biết commit đã xảy ra nếu client mất phản hồi (ví dụ timeout mạng ngay sau commit). Kết hợp ACID với:

  • Retry cho lỗi tạm thời
  • Khóa idempotency (hoặc ràng buộc duy nhất) để lặp lại cùng yêu cầu chỉ áp dụng tối đa một lần

Điều này ngăn cả cập nhật một phần lẫn trùng lặp tính phí/ghi nhận.

“Consistency” trong ACID nghĩa là gì (và điều gì không phải vậy)?

Trong ACID, “consistency” nghĩa là cơ sở dữ liệu chuyển từ một trạng thái hợp lệ sang một trạng thái hợp lệ khác theo các quy tắc bạn định nghĩa — ràng buộc, foreign key, unique, check. Nếu bạn không mã hoá một quy tắc (ví dụ “số dư không được âm”), ACID không thể tự bảo vệ nó. Cơ sở dữ liệu cần có các bất biến rõ ràng để thực thi.

Tại sao nên dùng ràng buộc cơ sở dữ liệu nếu ứng dụng đã kiểm tra input?

Kiểm tra ở ứng dụng tốt cho trải nghiệm người dùng nhưng có thể thất bại khi cạnh tranh (concurrency). Hai request cùng lúc có thể cùng vượt qua kiểm tra. Các ràng buộc ở cơ sở dữ liệu là hàng rào cuối cùng:

  • Unique constraint chặn email trùng
  • Foreign key ngăn bản ghi mồ côi
  • Check constraint / bất biến ngăn giá trị không hợp lệ

Dùng cả hai: validate sớm ở app, bắt buộc ở DB.

Cô lập bảo vệ chống loại lỗi đồng thời nào?

Cô lập kiểm soát những gì giao dịch của bạn có thể thấy khi các giao dịch khác đang chạy. Cô lập yếu có thể dẫn tới các bất thường như:

  • Dirty reads: thấy dữ liệu chưa commit
  • Non-repeatable reads: cùng một hàng thay đổi giữa hai lần đọc
  • Phantoms: tập kết quả của truy vấn khác nhau do insert/delete của giao dịch khác
  • Lost updates / write skew: ghi đồng thời phá vỡ tính đúng đắn

Các mức cô lập cho phép bạn đánh đổi hiệu năng đổi lấy bảo vệ chống những sai lệch này.

Làm sao chọn mức cô lập mà không phá hỏng hiệu năng?

Một chọn lựa thực tế là Read Committed cho nhiều ứng dụng OLTP: ngăn dirty reads với hiệu năng tốt. Nâng lên khi cần:

  • Repeatable Read cho kết quả ổn định trong giao dịch (ví dụ sinh hóa đơn)
  • Serializable cho bất biến quan trọng (ví dụ tránh bán quá mức) khi bạn chấp nhận tranh chấp/retry nhiều hơn

Luôn kiểm thử trên engine cơ sở dữ liệu cụ thể vì hành vi có thể khác nhau.

Durability đảm bảo điều gì, và điều gì có thể làm yếu nó?

Durability nghĩa là khi cơ sở dữ liệu xác nhận commit, thay đổi sẽ tồn tại sau khi sự cố. Thường thực hiện bằng write-ahead logging (WAL) và checkpoint.

Lưu ý các cấu hình làm yếu durability:

  • Commit đồng bộ / fsync: an toàn hơn, độ trễ cao hơn
  • Cấu hình không đồng bộ: nhanh hơn, nhưng có thể mất các giao dịch “đã commit” gần đây khi crash

Sao lưu và nhân bản giúp phục hồi/khả dụng nhưng không thay thế durability per-commit.

Related posts