Atomicity & Durability
Quy tắc: COMMIT SUCCESSFUL là một lời hứa về các dòng dữ liệu cục bộ trong database của bạn, và
không gì khác. Nó không nói gì về lời gọi HTTP bạn thực hiện bên trong transaction, cũng không nói
gì về tính đúng đắn của nghiệp vụ.
Thuộc nhóm transaction: atomicity & durability (trang này) · isolation · locking · distributed transaction · ranh giới nhất quán
Mục lục
- Anti-pattern
- Vì sao môi trường dev che giấu lỗi này
- Những gì ACID không hứa
- Transaction dở dang còn tệ hơn không có transaction
- Atomicity thực sự kết thúc ở đâu
- Durability là một lời hứa, không phải một lần ghi
- Những cách durability hóa ra là nói dối
- Checklist review
Anti-pattern
with db.transaction():
payment = Payment.create(order_id=order.id, amount=order.total, status="succeeded")
provider.charge(order.total) # ← external system, outside the transaction's reach
inventory_service.reserve(order.items) # ← another one
order.status = "paid"
Dashboard sau đó: CPU 30%, không có exception nào từ database, trả về 200 OK, dòng log ghi
Transaction committed. Còn tiền của khách thì mất mà không có đơn hàng nào tương ứng.
Mọi tín hiệu bạn đang theo dõi đều xanh, vì mọi tín hiệu bạn theo dõi đều nói về database. Database đã làm đúng những gì nó hứa — commit các dòng của chính nó một cách atomic. Khoản charge đã xảy ra phía provider không nằm trong lời hứa đó, việc giữ hàng trong kho cũng vậy.
Nếu transaction rollback sau khi provider.charge() đã trả về, database sạch sẽ như chưa có gì xảy ra
còn khách hàng thì vẫn đã trả tiền.
Vì sao môi trường dev che giấu lỗi này
Stub ở local không bao giờ lỗi, nên nhánh rollback không bao giờ chạy vào lúc một lời gọi ra ngoài đã thành công. Không có network partition nào giữa lần charge và lần commit, không có OOM kill, không có lần deploy nào rơi đúng giữa transaction.
Lý do sâu hơn khiến nó lọt qua review: về mặt kỹ thuật, code này đúng. Không ai đọc mà thấy được lỗi, vì lỗi nằm ở những gì đảm bảo của transaction loại trừ — và phần loại trừ đó vô hình tại chỗ gọi.
Những gì ACID không hứa
ACID là một hợp đồng bảo hiểm cho tính toàn vẹn của các thao tác dữ liệu. Nó không phải một checklist
cứ dùng BEGIN là đạt, và nó không làm cho logic của bạn đúng.
Cụ thể, ACID không bảo vệ bạn khỏi:
- Lỗi ngữ nghĩa. Trừ nhầm tài khoản, tính sai phí, áp giảm giá hai lần. Mỗi thứ đều commit hoàn hảo.
- Bất cứ thứ gì nằm ngoài database. Lời gọi API, publish vào queue, ghi file, gửi email. Transaction không thể rollback chúng.
- Những invariant nghiệp vụ mà database không biết. Chữ
Ctrong ACID có nghĩa là các constraint bạn đã khai báo vẫn được giữ — không phải các quy tắc nghiệp vụ của bạn được thỏa mãn. Database không hề biết số dư không bao giờ được âm, trừ khi bạn nói cho nó. Xem ranh giới nhất quán.
Database xanh không có nghĩa là nghiệp vụ còn sống. Nhất quán ở database và nhất quán ở nghiệp vụ là hai chuyện khác nhau.
Transaction dở dang còn tệ hơn không có transaction
Một transaction thất bại thường là an toàn. Một transaction hoàn thành nửa vời mới là thảm họa thật.
Thất bại trọn vẹn để thế giới nguyên như cũ, và khách hàng thử lại. Không ai bị thiệt. Thành công một phần lại tạo ra một trạng thái trung gian mà không phần nào của hệ thống được thiết kế để hiểu:
- Số dư đã bị trừ, lịch sử giao dịch không có → tiền đã đi mà không giải thích được
- Thanh toán đã ghi nhận, đơn hàng không có → khách hàng trả tiền cho không gì cả
- Đơn hàng đã tạo, hàng chưa được giữ trong kho → bạn đã bán thứ mình không có
Những trạng thái này tệ hơn một lỗi vì chúng im lặng. Không có gì throw. Hệ thống báo thành công, và sự lệch pha chỉ được phát hiện sau đó bởi khách hàng hoặc kế toán.
Đó là thứ atomicity mang lại: không phải "thao tác thành công" mà là "hệ thống không bao giờ bị bỏ lại giữa chừng một suy nghĩ." Nó là một yêu cầu về niềm tin nghiệp vụ, được diễn đạt thành yêu cầu kỹ thuật.
Atomicity thực sự kết thúc ở đâu
Ranh giới là database. Bên trong đó, WAL (write-ahead logging) biến atomicity thành hiện thực: thay
đổi được ghi vào một log tuần tự trước khi được áp dụng vào file dữ liệu, nên một lần crash giữa
chừng vẫn khôi phục được — khi khởi động lại, engine phát lại các transaction đã commit và bỏ những
transaction chưa hoàn tất.
Cơ chế đó bao phủ các dòng trong database đó. Nó không vươn ra ngoài thêm một li nào.
WAL cũng là lý do atomicity và durability là cùng một cơ chế nhìn từ hai phía. Log được ghi tuần tự — điều quan trọng vì ghi đĩa tuần tự nhanh hơn ghi ngẫu nhiên cả một bậc độ lớn, nên log có thể được flush đồng bộ mà không giết chết throughput, còn các data page thì được cập nhật lười về sau. Quá trình khôi phục khi đó dùng log hai lần: redo để áp dụng lại các transaction đã commit mà page chưa được ghi xuống, và undo để đảo ngược những transaction chưa commit. Checkpoint định kỳ flush các dirty page và cắt ngắn log, đó là thứ giới hạn thời gian khôi phục — khoảng cách checkpoint dài hơn nghĩa là ghi ở trạng thái ổn định nhanh hơn và khởi động lại chậm hơn, và sự đánh đổi đó chính là RTO của bạn.
Vậy nên quy tắc khi review: một transaction chỉ được chứa thao tác với database. Đưa mọi tác động ra bên ngoài khỏi nó:
charge = provider.charge(order.total) # outside: slow, irreversible, unrollbackable
with db.transaction(): # inside: local, fast, atomic
Payment.create(order_id=order.id, charge_id=charge.id, status="succeeded")
Outbox.insert("payment_succeeded", payload)
# the outbox worker publishes after commit
Điều này làm lộ ra câu hỏi thật mà đoạn code ban đầu đang che: nếu charge thành công mà commit thất bại
thì sao? Đó là một bài toán distributed transaction với các lời giải của distributed transaction —
idempotency key, outbox pattern, đối soát — không phải thứ mà một câu BEGIN giải được. Xem
distributed transaction. Transaction chưa bao giờ làm cho nó atomic; nó
chỉ làm cho mọi thứ chậm đi, và giữ lock trong lúc chờ. Xem lock-contention.
Durability là một lời hứa, không phải một lần ghi
Durability là cam kết: tôi đã chấp nhận thay đổi này và nó sẽ sống sót, kể cả khi máy này chết ngay khoảnh khắc tiếp theo.
Nó không đồng nghĩa với việc đã ghi dữ liệu xuống đĩa. Một lời gọi write() trả về thường chỉ mới
nằm trong page cache của hệ điều hành, chưa nằm trên thiết bị lưu trữ vật lý. Durability đòi hỏi
write-ahead log phải được flush và fsync trước khi commit được xác nhận — đó là lý do độ trễ commit bị
chi phối bởi việc đồng bộ đĩa, và lý do synchronous_commit = off làm cho việc ghi nhanh lên bằng cách
biến lời hứa thành có điều kiện.
Trong môi trường phân tán, lời hứa còn vươn xa hơn: một commit chỉ bền khi đủ số replica đã xác nhận nó. Chỉ xác nhận trên primary thôi thì một lần failover sẽ làm mất những transaction đã được báo là commit.
Những cách durability hóa ra là nói dối
fsyncbị tắt hoặc nới lỏng.synchronous_commit = offtrong PostgreSQL, hayinnodb_flush_log_at_trx_commitđặt 0 hoặc 2 trong MySQL, đổi một khoảng thời gian có thể mất transaction đã commit lấy throughput. Đó là lựa chọn hợp lý cho một số dữ liệu và là thảm họa với tiền — điểm mấu chốt là nó phải là một quyết định, không phải một giá trị mặc định ai đó chép lại.- Phần cứng nói dối. SSD dân dụng và một số storage ảo hóa xác nhận
fsynctrong khi dữ liệu vẫn nằm trong một cache dễ bay hơi. Mất điện khi đó sẽ làm mất những commit đã được xác nhận. - Replication bất đồng bộ cộng với failover. Primary đã xác nhận, replica chưa nhận được, primary chết. Những transaction đó biến mất, trong khi ứng dụng đã được báo là chúng an toàn.
- Backup chưa từng được restore. Một bản backup bạn chưa restore thử thì không phải backup — nó là một giả định chưa được kiểm chứng. Tương tự với một replica bạn chưa bao giờ failover sang.
Checklist review
- Không có lời gọi HTTP, publish queue, ghi file, gửi email hay gọi SDK bên thứ ba nào bên trong transaction
- Transaction chỉ chứa thao tác với database, và được mở muộn nhất có thể
- Các tác động ra bên ngoài được sắp thứ tự sao cho một lỗi không thể để tiền bị chuyển mà không có bản ghi
- Các thao tác xuyên hệ thống dùng idempotency key và outbox, không dùng một transaction bao ngoài
- Invariant nghiệp vụ được đảm bảo bằng constraint, không mặc định suy ra từ ACID
- Các trạng thái lỗi một phần được liệt kê đầy đủ, và không trạng thái nào im lặng
- Các thiết lập durability (
synchronous_commit,innodb_flush_log_at_trx_commit) là có chủ đích - Mức xác nhận replication khớp với lượng dữ liệu đã commit mà bạn chấp nhận được việc mất
- Failover và restore backup đã thực sự được diễn tập, không chỉ được cấu hình
Bản dịch tiếng Việt của bài học cùng tên. Tổng hợp từ series Transaction & Consistency của TechCraft, phần P1–P3 và P6 (collection), và đối chiếu với Database Internals phần P7 và P10 (collection). Cách đặt vấn đề "transaction thành công là lời nói dối", lập luận transaction dở dang còn tệ hơn, sự phân biệt ACID như một hợp đồng bảo hiểm, và cơ chế WAL/redo/undo/checkpoint là của TechCraft; cấu trúc review và checklist là của repository này.