Những suy nghĩ của tôi về giao dịch phân tán - kết quả bóng đá hôm nay

Trước đây, tôi đã đề cập đến các khái niệm liên quan đến giao dịch trong cơ sở dữ liệu MySQL. Vậy giao dịch được dùng để làm gì? Chúng ta cần hiểu rõ hơn về ACID:

ACID là bốn đặc tính mà hệ quản trị cơ sở dữ liệu (DBMS) phải đảm bảo để duy trì tính chính xác và tin cậy của giao dịch (transaction): Tính nguyên tử (atomicity), tính nhất quán (consistency), tính cách ly (isolation) và tính bền vững (durability).

  • Atomicity (Tính nguyên tử): Tất cả các hoạt động trong một giao dịch phải được thực hiện hoàn toàn hoặc không thực hiện gì cả. Nếu xảy ra lỗi trong quá trình thực thi, giao dịch sẽ được hoàn tác (rollback) về trạng thái ban đầu như chưa từng xảy ra.
  • Consistency (Tính nhất quán): Cơ sở dữ liệu phải luôn duy trì tính toàn vẹn trước và sau khi giao dịch kết thúc. Dữ liệu được viết vào phải tuân thủ tất cả ràng buộc, trigger, hay rollback theo chuỗi.
  • Isolation (Tính cách ly): Cơ sở dữ liệu cho phép nhiều giao dịch chạy đồng thời mà không ảnh hưởng lẫn nhau. Có các mức độ cách ly khác nhau như đọc chưa xác nhận (read uncommitted), đọc đã xác nhận (read committed), đọc lặp lại (repeatable read) và tuần tự hóa (serializable).
  • Durability (Tính bền vững): Sau khi giao dịch kết thúc, mọi thay đổi đều là vĩnh viễn, kể cả khi hệ thống gặp sự cố.

Trong MySQL, nhờ vào MVCC, các loại khóa và nhật ký, các đặc tính ACID được đảm bảo. Tuy nhiên, điều này thường áp dụng cho một dịch vụ cơ sở dữ liệu đơn lẻ. Khi ứng dụng phát triển, chúng ta thường chuyển từ cấu trúc đơn sang đa cơ sở dữ liệu với kiến trúc chủ-tiếp (Master-Slave). Lúc này sẽ xuất hiện một số vấn đề như độ trễ đồng bộ giữa chủ và tiếp. Đối với dữ liệu ít nhạy cảm, độ trễ này có thể chấp nhận được miễn là không quá lớn. Nhưng đối với dữ liệu quan trọng như đơn hàng, nếu sau khi đặt hàng và thanh toán xong, người dùng kiểm tra danh sách đơn hàng nhưng không thấy đơn đó hoặc vẫn hiển tải game nổ hũ tặng code thị chưa thanh toán, đây sẽ là lỗi nghiêm trọng. Để giải quyết vấn đề này, chúng ta có thể áp dụng một số biện pháp như không tách đọc/ghi, sử dụng Redis để lưu tạm thời đơn hàng hoặc thêm delay sau khi thanh toán.

Một ví dụ cụ thể hơn là quy trình đặt hàng trên nền tảng thương mại điện tử. Khi đặt hàng, hệ thống cần thực hiện nhiều thao tác phức tạp như: đóng băng mã giảm giá, khấu trừ tiền tài khoản người dùng, xử lý điểm thưởng,... Mỗi thao tác này có thể tương ứng với một dịch vụ backend riêng biệt. Yêu cầu ở đây là tất cả các thao tác phải thành công hoặc không thực hiện gì cả. Vì vậy, chúng ta cần một giải pháp tổng quát, một trong số đó là phương pháp cam kết hai giai đoạn.

Cam kết hai giai đoạn (Two-phase Commit - 2PC) là thuật toán được thiết kế để đảm bảo tính nhất quán trong các hệ thống phân tán. Trong mỗi giao dịch vượt qua nhiều nút, mỗi nút chỉ biết tình trạng thành công hay thất bại của mình nhưng không nắm được thông tin từ các nút khác. Do đó, cần một thành phần gọi là "thẩm phán" (coordinator) để kiểm soát kết quả từ tất cả các nút (gọi là participants) và quyết định có nên áp dụng thay đổi hay không. Thuật toán 2PC bao gồm hai giai đoạn: yêu cầu cam kết và thực hiện cam kết.

Phương pháp này mặc dù hữu ích nhưng cũng tồn tại một số hạn chế:

  1. Các nút tham gia có thể bị khóa tài nguyên trong thời gian chờ phản hồi từ coordinator.
  2. Coordinator là điểm duy nhất, nếu nó gặp sự cố, các nút tham gia sẽ bị treo.

Để khắc phục những hạn chế này, phương pháp cam kết ba giai đoạn (Three-phase Commit - 3PC) được đưa ra. 3PC bổ sung thêm cơ chế thời gian chờ và giai đoạn chuẩn bị. Giai đoạn đầu tiên là can commit, nơi các nút được hỏi xem có đủ tài nguyên để thực hiện thao tác hay không. Đây là giai đoạn không khóa tài nguyên, giúp tăng tỷ lệ thành công. Nếu bất kỳ nút nào trả lời không khả thi, quy trình sẽ dừng ngay lập tức. Giai đoạn thứ hai là pre commit, tương tự như giai đoạn đầu tiên của 2PC. Cuối cùng là giai đoạn do commit.

Dù 3PC cải tiến so với 2PC bằng cách giảm thiểu thời gian khóa tại vin777 tài nguyên và bổ sung cơ chế thời gian chờ, nhưng nó vẫn chưa giải quyết triệt để các vấn đề của 2PC. Ví dụ, trong trường hợp exception xảy ra ở giai đoạn đầu tiên và coordinator bị lỗi, các nút tham gia vẫn có thể tự động giải phóng tài nguyên sau thời gian chờ. Tuy nhiên, việc phục hồi sau sự cố vẫn cần đến các công cụ hỗ trợ như Zookeeper.

Ngày 24 tháng 5 năm 2020 lúc 22:11, sau khi thảo luận với bạn bè, tôi nhận keo da banh ra rằng ý nghĩa lớn nhất của 3PC là chia nhỏ giai đoạn đầu tiên để tránh khóa tài nguyên không cần thiết và bổ sung cơ chế thời gian chờ cho các nút tham gia. Điều này giúp hệ thống linh hoạt hơn và giảm thiểu rủi ro khi xảy ra lỗi. Tuy nhiên, để xử lý tốt các tình huống khôi phục sau lỗi, chúng ta cần nghiên cứu thêm các thuật toán như Paxos và Raft.