Kiến thức nền
ACID, từng chữ cái một
Cách cơ sở dữ liệu thực sự cung cấp Atomicity, Consistency, Isolation, và Durability — write-ahead log, MVCC, lịch trình khóa, và vấn đề fsync đã làm xấu hổ hầu hết các stack lưu trữ ít nhất một lần.
TL;DR
ACID là từ viết tắt bốn chữ cái — Atomicity (Nguyên tử), Consistency (Nhất quán), Isolation (Cách ly), Durability (Bền vững) — tách biệt một cơ sở dữ liệu khỏi một file. Đó là một lời hứa: các giao dịch hoặc hoàn thành đầy đủ hoặc không; dữ liệu vẫn hợp lệ nội bộ; các giao dịch đồng thời không thấy công việc nửa chừng của nhau; dữ liệu đã commit sống sót qua các vụ sập. Triển khai đằng sau mỗi chữ cái là một chủ đề sâu riêng. Atomicity và Durability dựa trên write-ahead logging. Isolation được xây dựng với khóa hoặc kiểm soát đồng thời đa phiên bản. Consistency là một nửa câu chuyện thực thi ràng buộc và một nửa “mọi thứ khác đang hoạt động.” Mọi cơ sở dữ liệu quan hệ hiện đại dành hầu hết ngân sách kỹ thuật của mình để làm bốn lời hứa này giữ vững dưới tải.
ACID đến từ đâu
Thuật ngữ ACID được Theo Härder và Andreas Reuter đặt ra trong bài báo khảo sát năm 1983 của họ Principles of Transaction-Oriented Database Recovery. Các ý tưởng cơ bản cũ hơn — các bài báo của Jim Gray về giao dịch có từ giữa những năm 1970, và System R đã đang làm ra việc triển khai từ 1974 — nhưng Härder và Reuter đặt cho các tính chất một cái tên đáng nhớ và một phân loại gọn gàng.
Bài báo được đóng khung như khảo sát, không phải đặc tả. Nhưng từ viết tắt được giữ lại, và “tuân thủ ACID” trở thành cách nói tắt cho “chúng tôi coi trọng giao dịch.”
A (Nguyên tử): Tất cả hoặc không gì
Một giao dịch thực thi toàn bộ hoặc không gì cả. Không trạng thái một phần nào từng được nhìn thấy bởi các giao dịch khác hoặc được lưu trữ vào đĩa.
Ví dụ kinh điển:
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
Nếu hệ thống sập giữa hai UPDATE, giao dịch phải hoặc (a) hoàn thành — cả hai cập nhật đều xuống — hoặc (b) được cuộn lại như thể nó chưa bao giờ xảy ra. Không bao giờ được có trường hợp tiền biến mất khỏi tài khoản 1 mà không xuất hiện trong tài khoản 2.
Atomicity được triển khai như thế nào: write-ahead log (WAL).
Trước khi áp dụng bất kỳ thay đổi nào cho các file dữ liệu thực tế của cơ sở dữ liệu, cơ sở dữ liệu ghi một bản ghi của thay đổi vào một file log chỉ-nối:
LSN 1001: BEGIN tx 7
LSN 1002: UPDATE accounts SET balance=balance-500 WHERE id=1 (cũ: 1000, mới: 500)
LSN 1003: UPDATE accounts SET balance=balance+500 WHERE id=2 (cũ: 200, mới: 700)
LSN 1004: COMMIT tx 7
Các trang dữ liệu thực tế có thể đã hoặc chưa được cập nhật trên đĩa. Log là thứ quan trọng. Khi phục hồi sau sập, cơ sở dữ liệu:
- Quét log từ checkpoint cuối.
- Với mỗi giao dịch có bản ghi
COMMIT— áp dụng lại các thay đổi của nó (redo). - Với mỗi giao dịch không có bản ghi
COMMIT— cuộn lại các thay đổi của nó (undo).
Sau quá trình phục hồi này, cơ sở dữ liệu ở trạng thái nơi mọi giao dịch đã commit là bền vững và mọi giao dịch chưa hoàn thành đã bị xóa. Atomicity được bảo tồn qua các vụ sập.
WAL là cơ chế quan trọng nhất duy nhất trong kỹ thuật cơ sở dữ liệu. Mọi cơ sở dữ liệu quan hệ lớn đều có một cái — Postgres gọi nó là WAL, MySQL/InnoDB gọi nó là redo log, Oracle gọi nó là redo log, SQL Server gọi nó là transaction log. Tất cả đều cùng mô hình.
C (Nhất quán): Chỉ các trạng thái hợp lệ
Một giao dịch chuyển cơ sở dữ liệu từ một trạng thái hợp lệ sang một trạng thái hợp lệ khác. Các ràng buộc được định nghĩa (khóa ngoại, ràng buộc duy nhất, ràng buộc kiểm tra, trigger) không bao giờ bị vi phạm ở trạng thái đã commit.
C là chữ cái lẻ trong ACID. Ba chữ khác được thực thi bởi máy móc nội bộ của cơ sở dữ liệu. Consistency một phần là công việc của cơ sở dữ liệu và một phần của ứng dụng.
Điều cơ sở dữ liệu thực thi:
- Tính duy nhất
PRIMARY KEY. - Tính toàn vẹn tham chiếu
FOREIGN KEY. - Các ràng buộc
NOT NULL,CHECK,UNIQUE. - Bất kỳ trigger nào được định nghĩa trên bảng.
Điều cơ sở dữ liệu không thực thi:
- Các quy tắc kinh doanh chỉ được mã hóa trong logic ứng dụng.
- Các bất biến chéo-dịch-vụ (ví dụ, tổng số dư nên bằng tổng giao dịch).
- Tính đúng đắn ngữ nghĩa (schema có thể cho phép số dư tài khoản âm nếu không có CHECK ngăn nó).
Nếu giao dịch của bạn để cơ sở dữ liệu ở trạng thái kỹ thuật qua mọi ràng buộc nhưng không hợp lệ về ngữ nghĩa — trừ tiền từ một tài khoản mà không tạo một bản ghi giao dịch tương ứng ở đâu đó khác — cơ sở dữ liệu nói “nhất quán.” Bạn nói “không nhất quán.” Cả hai đều đúng về những điều khác nhau.
Ý nghĩa thực tế của C trong ACID là: nếu bạn đã định nghĩa các ràng buộc mà cơ sở dữ liệu có thể kiểm tra, nó sẽ kiểm tra chúng. Mọi thứ khác là của bạn.
I (Cách ly): Các giao dịch đồng thời không làm hỏng nhau
Các giao dịch đồng thời cư xử như thể chúng chạy từng cái một. Không giao dịch nào thấy trạng thái trung gian của giao dịch khác.
Đây là chữ cái khó cung cấp nhất trong thực tế, và là chữ nơi các cơ sở dữ liệu đưa ra các đánh đổi có thể điều chỉnh.
Các dị thường
Bốn dị thường đồng thời cổ điển, mỗi cái khó ngăn ngừa hơn cái trước:
- Đọc bẩn — giao dịch A đọc dữ liệu mà giao dịch B đã viết nhưng chưa commit. Nếu B cuộn lại, A đã thấy dữ liệu ma.
- Đọc không lặp lại — A đọc một hàng, B sửa đổi và commit hàng đó, A đọc lại và thấy các giá trị khác.
- Đọc ma — A chạy một truy vấn trả về một tập hàng, B chèn một hàng mới khớp với tiêu chí truy vấn và commit, A chạy lại truy vấn và thấy hàng mới.
- Dị thường nối tiếp / nghiêng ghi — hai giao dịch mỗi cái đọc cùng dữ liệu, đưa ra quyết định dựa trên nó, và ghi lại — với hiệu ứng kết hợp mà không thứ tự tuần tự nào của hai giao dịch có thể đã tạo ra.
Các mức cách ly
SQL định nghĩa bốn mức cách ly, mỗi mức ngăn ngừa nhiều dị thường hơn (và thường tốn kém hơn):
| Mức | Đọc bẩn | Không lặp lại | Ma | Nghiêng ghi |
|---|---|---|---|---|
| Read Uncommitted | cho phép | cho phép | cho phép | cho phép |
| Read Committed | ngăn | cho phép | cho phép | cho phép |
| Repeatable Read | ngăn | ngăn | phụ thuộc | cho phép |
| Serializable | ngăn | ngăn | ngăn | ngăn |
- Read Uncommitted — các đọc có thể thấy dữ liệu chưa commit. Gần như không bao giờ là điều bạn muốn. Hầu hết cơ sở dữ liệu không triển khai nó và âm thầm nâng lên Read Committed.
- Read Committed — mặc định trong Postgres và Oracle. Các đọc chỉ thấy dữ liệu đã commit, nhưng cùng truy vấn có thể trả về kết quả khác trong một giao dịch.
- Repeatable Read — mặc định trong MySQL/InnoDB. Trong một giao dịch, các đọc ổn định. Liệu các ma có được ngăn ngừa hay không phụ thuộc vào cơ sở dữ liệu.
- Serializable — nghiêm ngặt nhất. Các giao dịch cư xử chính xác như thể chúng chạy từng cái một. Tốn kém; hầu hết ứng dụng không sử dụng nó.
Đánh đổi là đồng thời vs. tính đúng đắn. Cách ly cao hơn cho bạn đảm bảo mạnh hơn nhưng giảm thông lượng và tăng độ trễ. Hầu hết hệ thống sản xuất chạy ở Read Committed và chấp nhận rằng một số dị thường có thể xảy ra — và thiết kế ứng dụng cẩn thận để giảm thiểu tác động.
Hai chiến lược triển khai
Có hai cách cơ bản để triển khai cách ly:
Khóa hai pha (2PL). Mỗi hàng một giao dịch đọc được khóa chia sẻ. Mỗi hàng nó ghi được khóa độc quyền. Khóa được giữ đến khi giao dịch commit. Xung đột giữa các giao dịch khiến một trong chúng phải chờ.
Ưu: đơn giản về mặt khái niệm, đảm bảo mạnh. Nhược: đọc chặn ghi, ghi chặn đọc, deadlock có thể, thông lượng bị ảnh hưởng dưới tranh chấp. Được MySQL/InnoDB sử dụng cho một số mức cách ly, và SQL Server truyền thống.
Kiểm soát đồng thời đa phiên bản (MVCC). Mỗi lần ghi tạo một phiên bản mới của hàng; các phiên bản cũ được giữ miễn là bất kỳ giao dịch nào vẫn có thể cần chúng. Các đọc không bao giờ chặn ghi, vì chúng luôn có thể thấy phiên bản hiện tại khi giao dịch của chúng bắt đầu. Các ghi không bao giờ chặn đọc.
Ưu: đồng thời cao hơn nhiều cho khối lượng công việc đọc-nặng. Nhược: chi phí không gian đĩa (các phiên bản hàng cũ tích lũy và cần dọn dẹp định kỳ, còn gọi là “vacuuming” trong Postgres), triển khai phức tạp hơn, ngữ nghĩa tinh vi hơn cho một số mức cách ly.
Postgres, Oracle, và SQL Server hiện đại sử dụng MVCC. MySQL/InnoDB sử dụng một cách tiếp cận lai chủ yếu là MVCC nhưng với một số khóa cho chế độ serializable. MVCC là cách tiếp cận hiện đại thống trị, vì khối lượng công việc đọc-nặng là trường hợp phổ biến và chặn các đọc là thảm họa cho thông lượng.
D (Bền vững): Dữ liệu đã commit sống sót qua các vụ sập
Một khi một giao dịch đã được commit, tác động của nó sống sót qua bất kỳ vụ sập nào sau đó. Mất điện, kernel panic, kill -9 trên tiến trình cơ sở dữ liệu — dữ liệu đã commit vẫn còn đó khi cơ sở dữ liệu quay lại.
Durability được triển khai như thế nào: fsync().
Khi một giao dịch commit, cơ sở dữ liệu ghi các bản ghi WAL của nó và sau đó gọi fsync() (hoặc fdatasync()) để buộc log xuống lưu trữ vật lý. Chỉ sau khi fsync() trả về cơ sở dữ liệu mới nói với ứng dụng “commit thành công.”
Đây là nút thắt cổ chai thực tế trong hầu hết khối lượng công việc giao dịch. Các đĩa hiện đại có thể duy trì hàng trăm nghìn ghi mỗi giây về thông lượng — nhưng fsync là một phép toán hoàn toàn khác. Đó là một rào cản: cơ sở dữ liệu đang nói với OS, OS đang nói với filesystem, filesystem đang nói với đĩa, lưu giữ dữ liệu này bây giờ và xác nhận lại với tôi trước khi tôi tiếp tục. Mỗi tầng trong stack đó có thể là nguồn của lời nói dối.
Vấn đề fsync()
Một sự xấu hổ nổi tiếng và lặp lại: fsync nói dối.
Các tổ hợp khác nhau của filesystem, đĩa, bộ điều khiển RAID, và cấu hình OS đã, qua nhiều năm, báo cáo “fsync thành công” trước khi dữ liệu thực sự ở lưu trữ ổn định. Ghi ở trong một cache đĩa hoặc cache điều khiển có thể mất khi mất điện.
- Năm 2018, Postgres và MySQL phát hiện rằng một số filesystem Linux, dưới một số chế độ lỗi, sẽ
fsync()thành công nhưng sau đó mất dữ liệu trên các lỗi tiếp theo. Điều này dẫn đến bài viết “fsync bug” nổi tiếng từ các nhà phát triển Postgres khiến có các thay đổi ở mức kernel. - Các SSD tiêu dùng từ lâu đã được biết là đôi khi báo hoàn thành fsync trong khi dữ liệu vẫn ở trong một cache điều khiển biến động. Các SSD doanh nghiệp có các cache được bảo vệ chống mất điện; các ổ tiêu dùng thì không.
- Các bộ điều khiển RAID với caching ghi-lại được cho là có sao lưu pin; khi pin xuống cấp, bền vững âm thầm trở thành nỗ lực tốt nhất.
Kết quả: “bền vững” là một tính chất của toàn bộ stack, không chỉ cơ sở dữ liệu. Một cơ sở dữ liệu chỉ có thể bền vững bằng mức tầng lưu trữ bên dưới cho phép. Hầu hết cơ sở dữ liệu sản xuất đi kèm với cảnh báo về cấu hình lưu trữ mà chúng coi là an toàn, và Postgres có một thiết lập rõ ràng fsync = off đánh đổi bền vững lấy hiệu năng (và rõ ràng vô hiệu hóa đảm bảo bền vững).
Commit đồng bộ vs. bất đồng bộ
Trong giới hạn lưu trữ, các cơ sở dữ liệu cung cấp các mức bền vững có thể điều chỉnh:
- Commit đồng bộ (mặc định). Chờ WAL fsync trước khi báo cáo commit. Bền vững tối đa, thông lượng tối thiểu.
- Commit bất đồng bộ. Báo cáo commit ngay lập tức; fsync WAL trong nền. Thông lượng cao hơn, nhưng một vụ sập trong cửa sổ giữa commit và fsync mất các giao dịch mới-commit.
- Commit nhóm. Gói các bản ghi WAL của nhiều giao dịch thành một fsync, phân bổ chi phí qua nhiều commit. Hầu hết cơ sở dữ liệu làm điều này tự động.
- Bền vững dựa trên nhân bản. Chờ WAL đến một bản sao thay vì đĩa cục bộ. Thường nhanh hơn fsync cục bộ (mạng nhanh, fsync chậm) và cung cấp một dạng đảm bảo bền vững khác.
ACID trong hệ thống phân tán: BASE và CAP
Các lời hứa của ACID tương đối dễ giữ trên một máy duy nhất. Phân tán qua nhiều máy, chúng trở nên khó hơn nhiều. Đây là điều định lý CAP và truyền thống BASE (Basically Available, Soft state, Eventually consistent) đã khám phá vào những năm 2000.
Phong trào NoSQL những năm 2000 một phần là phản ứng với chi phí của ACID trong hệ thống phân tán. Các cơ sở dữ liệu như MongoDB sớm, Cassandra, và Riak đã từ bỏ một số tính chất ACID để đổi lấy khả năng mở rộng ngang và chịu đựng phân vùng.
Những năm 2010 và 2020 đã một phần là đảo ngược: các hệ thống như Google Spanner, CockroachDB, YugabyteDB, và FoundationDB chứng minh rằng bạn có thể có ACID phân tán nếu bạn sẵn sàng trả giá cho cơ sở hạ tầng thông minh (đồng hồ nguyên tử, giao thức đồng thuận, kỹ thuật mạng cẩn thận). Spanner sử dụng API TrueTime — GPS và đồng hồ nguyên tử ở mỗi trung tâm dữ liệu — để cung cấp các timestamp commit được sắp xếp toàn cầu, cho phép cách ly serializable qua các lục địa.
ACID không thua. Hóa ra nó có thể đạt được trong hệ thống phân tán, chỉ đắt. Nơi chi phí đó không chấp nhận được, cách tiếp cận BASE sống sót. Hầu hết hệ thống hiện đại chọn mức ACID họ cần cho mỗi khối lượng công việc, thay vì coi nó là tất cả-hoặc-không-gì.
ACID thực sự là gì
Bài Mô hình Quan hệ mô tả ACID là “lời hứa cơ sở dữ liệu thực hiện.” Khuôn khổ đó chính xác nhưng đánh giá thấp điều bên dưới. Mỗi chữ cái là đỉnh của một tảng băng triển khai:
- A là write-ahead logging — một khối lượng nghiên cứu 40 năm về cách phục hồi bền vững từ các điểm lỗi tùy ý.
- C là thực thi ràng buộc và thiết kế schema — một mảnh tầng-cơ-sở-dữ-liệu tương đối mỏng trên một mối quan tâm tầng-ứng-dụng dày.
- I là kiểm soát đồng thời — vấn đề khó nhất trong cơ sở dữ liệu, với hai họ giải pháp chính (khóa, MVCC) và nghiên cứu liên tục về các biến thể.
- D là kỹ thuật stack-lưu-trữ — làm cho hệ điều hành, filesystem, và phần cứng lưu giữ dữ liệu đáng tin cậy, khó hơn bất cứ ai mong đợi lần đầu họ gặp nó.
Làm đúng cả bốn điều này, dưới tải cao, mà không hy sinh thông lượng, về cơ bản là toàn bộ vấn đề kỹ thuật của cơ sở dữ liệu. Mọi cơ sở dữ liệu quan hệ lớn vào năm 2026 — Postgres, MySQL, Oracle, SQL Server — ở cốt lõi của nó, là một cỗ máy rất tinh vi để làm cho ACID giữ vững trong khi phục vụ hàng chục nghìn giao dịch mỗi giây. Toán học Codd xuất bản năm 1970 cho bạn cái gì. Härder và Reuter cho lời hứa. Bốn mươi ba năm kể từ đó đã là về như thế nào.