Hệ thống Dữ liệu Phân tán/Multi-leader — ghi ở nhiều nơi và bài toán xung đột
10/38
Bài 10 / 38~12 phútNhân bản (replication)Miễn phí lượt xem

Multi-leader — ghi ở nhiều nơi và bài toán xung đột

Ghi ở nhiều leader (đa datacenter, offline) đổi lấy độ trễ ghi thấp và chịu lỗi — nhưng sinh write conflict. Cách phát hiện và các chiến lược giải quyết xung đột.

TL;DR: Multi-leader cho phép nhiều node cùng nhận ghi, mỗi leader vừa nhận ghi cục bộ vừa replicate cho các leader khác. Lợi: ghi có độ trễ thấp (ghi vào leader gần nhất), chịu được một datacenter chết, và hoạt động cả khi tạm mất kết nối (client offline, cộng tác thời gian thực). Giá phải trả — và là lý do multi-leader khó — là write conflict: hai leader chấp nhận hai ghi mâu thuẫn lên cùng một dữ liệu trước khi biết về nhau. Xung đột phải được phát hiện rồi giải quyết: last-write-wins (đơn giản nhưng mất dữ liệu), theo dõi nhân quả bằng vector clock, gộp giá trị, hoặc để ứng dụng/người dùng quyết. Không có cách giải quyết nào miễn phí — chọn cách nào là quyết định theo bản chất dữ liệu.

Single-leader tránh xung đột bằng cách ép mọi ghi qua một điểm. Multi-leader gỡ ràng buộc đó để lấy độ trễ và khả dụng — và ngay lập tức phải đối mặt câu hỏi cốt lõi của dữ liệu phân tán: khi hai nơi thay đổi cùng một thứ độc lập, sự thật là gì? Bài này phân tích xung đột sinh ra thế nào và các cách sống với nó.

1. Analogy — hai chi nhánh cùng sửa một hồ sơ

Một công ty có hai chi nhánh, mỗi nơi giữ bản hồ sơ khách và đều được sửa. Sáng nay, chi nhánh Hà Nội đổi số điện thoại khách thành A; cùng lúc, chi nhánh Sài Gòn đổi thành B. Mỗi bên ghi vào bản của mình rồi tối gửi bản cập nhật cho bên kia. Đến tối, hai bản mâu thuẫn: một bên nói A, một bên nói B — và không có "sổ gốc" nào để phân xử.

Ai đúng? Không có câu trả lời tự nhiên. Phải có quy tắc: lấy cái ghi sau (theo đồng hồ — nhưng đồng hồ hai chi nhánh có thể lệch), hay giữ cả hai để người phụ trách chọn, hay gộp (nếu là danh sách thì union). Mọi quy tắc đều có cái giá — LWW thì mất một số, giữ cả hai thì phải có ai đó quyết.

Hai chi nhánhMulti-leader
Mỗi chi nhánh đều sửa được hồ sơNhiều leader đều nhận ghi
Tối gửi bản cập nhật cho nhauReplicate bất đồng bộ giữa các leader
HN đổi A, SG đổi B cùng lúcWrite conflict
"Lấy cái ghi sau"Last-write-wins (mất một bản)
Giữ cả hai, người phụ trách chọnGhi nhận conflict, để app/user giải
💡 Cách nhớ

Nhiều nơi cùng ghi = có lúc mâu thuẫn = phải có luật phân xử. LWW rẻ mà mất dữ liệu; theo dõi nhân quả (vector clock) đắt hơn nhưng gộp đúng.

2. Vì sao (và khi nào) cần multi-leader?

Single-leader ổn cho phần lớn trường hợp; multi-leader chỉ đáng cái phức tạp thêm khi:

  • Nhiều datacenter: mỗi datacenter có leader riêng → ghi vào leader gần (độ trễ thấp), và một datacenter chết không chặn ghi ở nơi khác. Với single-leader xuyên lục địa, mọi ghi phải bay tới một datacenter — chậm và mong manh.
  • Client hoạt động offline: ứng dụng lịch/ghi chú trên điện thoại vẫn cho sửa khi mất mạng — mỗi thiết bị như một "leader" nhỏ, đồng bộ lại khi có mạng.
  • Cộng tác thời gian thực: nhiều người sửa cùng tài liệu; mỗi client ghi ngay (không chờ round-trip tới server) rồi hoà.

Điểm chung: các tình huống mà ép mọi ghi qua một điểm là không chấp nhận được (quá chậm, hoặc bất khả khi offline). Đổi lại, ta nhận về write conflict.

3. Write conflict sinh ra thế nào?

flowchart TB
  UA["User A"] -->|"ghi X = 1"| L1["Leader DC1"]
  UB["User B"] -->|"ghi X = 2"| L2["Leader DC2"]
  L1 -->|"replicate (tre)"| L2
  L2 -->|"replicate (tre)"| L1
  L1 --> C["Xung dot: X = 1 hay 2?"]
  L2 --> C

Hai leader nhận hai ghi lên cùng một khoá trong khoảng chúng chưa biết về nhau (do replication bất đồng bộ, nhớ lag ở bài 02). Khi hai ghi cuối cùng gặp nhau, mỗi leader thấy một giá trị khác — đó là write conflict. Với single-leader điều này không thể xảy ra vì một nơi quyết thứ tự; multi-leader mở ra đúng khả năng đó.

Thử đoán trước

Hai kịch bản, cùng khoá X. (a) User A ghi X=1 ở DC1; đồng thời, chưa DC nào thấy DC kia, User B ghi X=2 ở DC2. (b) User B đọc thấy X=1 rồi mới quyết định ghi đè thành X=2. Thử đoán: kịch bản nào là write conflict thật, kịch bản nào chỉ là ghi đè bình thường — và điều gì phân biệt hai kịch bản?

Lần theo cơ chế: kịch bản (a) là xung đột thật — hai ghi song song (concurrent), không cái nào "biết" cái kia, nên khi gặp nhau mỗi leader thấy một giá trị và không có cơ sở tự nhiên để chọn. Kịch bản (b) không phải xung đột: ghi X=2 xảy ra sau khi đã thấy X=1 (quan hệ nhân quả), nên nó đơn giản đè X=1 — thứ tự đã rõ ràng. Điểm tinh tế nằm đúng ở đây: xung đột chỉ tính khi hai ghi song song; phân biệt "song song" với "nhân quả" chính là việc của vector clock.

4. Giải quyết xung đột — không có cách nào miễn phí

Thử đoán trước

Hai người cùng thêm một sản phẩm vào giỏ hàng chung, ở hai leader khác nhau, đúng lúc chưa đồng bộ. Nếu hệ dùng last-write-wins, chuyện gì xảy ra với một trong hai thao tác? Với "giỏ hàng", kết quả đó chấp nhận được không — và cách nào tốt hơn?

Lần theo cơ chế: với LWW, hai thao tác "thêm sản phẩm" chạm cùng bản ghi giỏ hàng thì chỉ ghi có timestamp lớn hơn sống sót — sản phẩm mà thao tác kia vừa thêm biến mất khỏi giỏ, không lỗi, không cảnh báo. Với giỏ hàng, kết quả đó rõ ràng không chấp nhận được (khách mất món hàng vừa thêm); cách đúng hơn là gộp hai giỏ theo sản phẩm (hợp nhất kiểu CRDT tập hợp) thay vì để một thao tác thắng và xoá thao tác kia.

Tránh xung đột (tốt nhất nếu được): định tuyến mọi ghi của một bản ghi cụ thể về cùng một leader (ví dụ theo user-id). Không có hai nơi ghi cùng khoá thì không có xung đột. Nhiều hệ multi-leader thực chất chỉ tránh xung đột bằng cách này; chỉ khi không thể (offline, cộng tác) mới phải giải quyết thật.

Khi buộc phải giải quyết:

  • Last-write-wins (LWW): gán mỗi ghi một timestamp, giữ ghi có timestamp lớn nhất, vứt phần còn lại. Đơn giản nhưng mất dữ liệu (một ghi hợp lệ bị âm thầm loại) và phụ thuộc đồng hồ (đồng hồ lệch → "sau" sai). Chấp nhận được khi mất một ghi là ổn (ví dụ ghi trạng thái tạm), tệ hại cho dữ liệu quan trọng.
  • Theo dõi nhân quả (vector clock): phân biệt ghi song song (cần gộp) với ghi nhân quả (đè thẳng). Không mất dữ liệu oan, nhưng cần cơ chế gộp cho các ghi song song.
  • Gộp giá trị (merge / CRDT — Conflict-free Replicated Data Types, cấu trúc dữ liệu tự hoà không cần khoá): với cấu trúc phù hợp, gộp tự động đúng ngữ nghĩa — like thì union, bộ đếm thì cộng, giỏ hàng thì hợp nhất item. Đây là cách "đúng" cho nhiều dữ liệu cộng tác.
  • Để ứng dụng / người dùng quyết: lưu cả hai bản, lần đọc/ghi sau đưa ra để giải (như Git conflict).
Nhớ lại Vector clock

Bạn đã học vector clock để phát hiện hai sự kiện có quan hệ nhân quả (cái này xảy ra sau khi thấy cái kia) hay song song (độc lập, không cái nào biết cái nào). Đây chính là công cụ để multi-leader biết một cặp ghi là "đè hợp lệ" hay "xung đột cần gộp" — thay vì đoán mù bằng timestamp.

5. Thiết kế — chọn chiến lược giải xung đột

Bạn đã có đủ bốn lựa chọn (LWW, vector clock, gộp/CRDT, để người dùng quyết) và một nguyên tắc: chọn theo bản chất dữ liệu. Giờ tự áp. Với ba loại dữ liệu dưới, hãy tự quyết chiến lược nào hợp nhất và biện minh bằng cái giá của từng cách — viết ra (trong đầu hoặc ra giấy) trước khi mở đáp án.

Loại dữ liệuNgữ nghĩa khi hai ghi song song
Bộ đếm lượt xem videoHai nơi cùng cộng thêm gần như đồng thời
Danh sách "đã thích" của một bàiHai thiết bị cùng thêm/bỏ like
Tài liệu soạn thảo cộng tácHai người cùng sửa một đoạn văn
Tự kiểm tra
Q1
Với mỗi loại dữ liệu trên, chọn chiến lược conflict resolution (LWW / vector clock / CRDT-merge / để người dùng quyết) và biện minh bằng cái giá của cách đó. Loại nào tuyệt đối KHÔNG nên dùng LWW, vì sao?

Bộ đếm lượt xem → CRDT bộ đếm (kiểu G-Counter, mỗi bản giữ phần đóng góp riêng). Hai ghi song song đều là "cộng thêm" và đều đúng; LWW giữ một cái, vứt cái kia → đếm hụt. Bộ đếm CRDT gộp bằng cách cộng đóng góp của từng bản → không mất lượt xem nào, hội tụ bất kể thứ tự tới.

Danh sách "đã thích" → CRDT tập hợp (kiểu OR-Set, theo dõi add/remove). Thêm và bỏ like là thao tác trên tập; gộp đúng ngữ nghĩa là hợp nhất theo quy tắc, không phải "ghi sau thắng". LWW ở đây làm một lần like/unlike bị nuốt.

Tài liệu cộng tác → CRDT văn bản (hoặc thuật toán hoà kiểu OT). Hai chỉnh sửa song song trên cùng đoạn cần *gộp cả hai*, không chọn một; LWW sẽ xoá trắng công của một người.

Cả ba đều KHÔNG nên dùng LWW vì đều là dữ liệu *tích luỹ / gộp được*: mất một ghi là mất dữ liệu thật, không phải "ghi cũ vô nghĩa". LWW chỉ hợp khi mất một ghi là chấp nhận được (ví dụ trạng thái tạm bị ghi đè liên tục). Lưu ý vector clock đứng *trước* các cách trên: nó cho biết hai ghi có thật sự song song không — nếu nhân quả thì đè thẳng, chỉ khi song song mới cần tới CRDT/merge.

6. Pitfall — chọn LWW vì "đơn giản" cho dữ liệu không được mất

Pitfall — last-write-wins nuốt dữ liệu trong im lặng

Vì LWW dễ hiện thực, nó hay được chọn mặc định — kể cả cho dữ liệu mà mất một ghi là nghiêm trọng.

❌ Hai ghi song song X=1 (t=10), X=2 (t=11) -> LWW giu X=2, VUT X=1
   -> khong log, khong bao -> ghi X=1 bien mat im lang
   -> te hon: dong ho 2 node lech -> "t=11" that ra xay ra TRUOC -> giu nham

❌ Dung LWW cho gio hang / danh sach -> mot lan them item bi nuot

Hệ quả: mất dữ liệu không dấu vết, và phụ thuộc đồng hồ (clock skew — sẽ học kỹ ở Module 5) khiến "cái sau thắng" đôi khi giữ nhầm cái xảy ra trước.

Hướng đúng: chọn resolution theo bản chất dữ liệu, không theo "cái nào dễ code". Dữ liệu được-phép-mất-một-ghi (trạng thái tạm) → LWW chấp nhận được. Dữ liệu tích luỹ (đếm, danh sách, giỏ hàng) → gộp/CRDT. Dữ liệu quan trọng cần con người quyết → lưu cả hai bản để giải. Và nếu có thể, tránh xung đột ngay từ đầu bằng cách ghim mỗi bản ghi về một leader.

7. 📚 Deep Dive

📚 Deep Dive — Multi-leader
  • Designing Data-Intensive Applications (Kleppmann) — Chương 5, mục "Multi-Leader Replication" — use case multi-datacenter/offline/collaborative, write conflict, và các chiến lược resolution (tránh xung đột, LWW, hội tụ, custom).
  • CRDT (Conflict-free Replicated Data Types) — Shapiro et al. (2011): cấu trúc dữ liệu mà mọi bản gộp tự động về cùng kết quả bất kể thứ tự, nền cho cộng tác thời gian thực (bộ đếm, tập hợp, văn bản).
  • Quan hệ song song vs nhân quả ("happens-before" của Lamport 1978) là gốc lý thuyết cho vector clock và cho định nghĩa "xung đột".

Ghi chú: DDIA Ch5 nhấn: "tránh xung đột" luôn tốt hơn "giải quyết xung đột" khi khả thi. LWW là cái bẫy phổ biến nhất vì dễ mà mất dữ liệu.

8. Liên hệ các bài khác

  • Bài 01 — Single-leader: single-leader không có xung đột (một nơi quyết thứ tự) — multi-leader gỡ ràng buộc đó để lấy độ trễ/khả dụng, đổi bằng xung đột.
  • Bài 02 — Replication lag: consistent prefix reads (bài 02) và vector clock ở đây cùng bàn về giữ đúng thứ tự nhân quả.
  • Bài 04 — Leaderless: leaderless cũng đối mặt ghi song song và dùng chính vector clock + read repair để hoà — cùng công cụ, kiến trúc khác.
  • Module 5 — Đồng hồ không tin cậy: vì sao LWW dựa trên timestamp mong manh — clock skew làm "cái sau" sai.

9. Tóm tắt

  • Multi-leader: nhiều node nhận ghi + replicate cho nhau. Đáng dùng khi ép mọi ghi qua một điểm là không chấp nhận được (đa datacenter, offline, cộng tác).
  • Giá là write conflict: hai leader chấp nhận hai ghi mâu thuẫn trên cùng khoá khi chưa biết về nhau (do replication async).
  • Xung đột chỉ tính khi hai ghi song song (không cái nào thấy cái kia); ghi nhân quả thì đè thẳng — phân biệt bằng vector clock.
  • Tránh xung đột (ghim mỗi bản ghi về một leader) tốt hơn giải quyết.
  • Resolution: LWW (đơn giản, mất dữ liệu, phụ thuộc đồng hồ), vector clock (gộp đúng song song/nhân quả), CRDT/merge (tự hoà đúng ngữ nghĩa), để người dùng quyết.
  • Anti-pattern: LWW mặc định cho dữ liệu không được mất → mất dữ liệu im lặng + phụ thuộc clock skew.

10. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao multi-leader sinh ra write conflict còn single-leader thì không? Nêu đúng điều kiện để một xung đột xảy ra.

Single-leader ép mọi ghi qua một điểm tuần tự hoá chúng thành một thứ tự duy nhất → không thể có hai ghi mâu thuẫn được chấp nhận song song. Multi-leader cho nhiều nơi cùng nhận ghi, nên hai leader có thể chấp nhận hai ghi lên cùng khoá.

Điều kiện xung đột: hai ghi lên cùng một dữ liệu, xảy ra song song — tức trong khoảng hai leader chưa biết về nhau (do replication bất đồng bộ, replication lag). Nếu một ghi xảy ra sau khi đã thấy ghi kia (nhân quả), nó chỉ đè, không phải xung đột.

Q2
Nêu ba tình huống multi-leader đáng dùng dù nó phức tạp hơn single-leader. Điểm chung của chúng là gì?

(1) Nhiều datacenter: ghi vào leader gần (độ trễ thấp) + một DC chết không chặn ghi nơi khác. (2) Client offline: app lịch/ghi chú vẫn sửa được khi mất mạng, mỗi thiết bị như leader nhỏ, hoà lại sau. (3) Cộng tác thời gian thực: nhiều người sửa cùng tài liệu, mỗi client ghi ngay rồi hoà.

Điểm chung: ép mọi ghi qua một điểm là không chấp nhận được — quá chậm (xuyên lục địa), hoặc bất khả (offline). Multi-leader đánh đổi sự phức tạp (xung đột) lấy độ trễ ghi thấp và khả năng hoạt động khi phân tách.

Q3
Vì sao 'tránh xung đột' được ưu tiên hơn 'giải quyết xung đột', và cách tránh phổ biến là gì?

Vì mọi cách giải quyết đều có giá: LWW mất dữ liệu, gộp cần cơ chế đúng ngữ nghĩa, để người dùng quyết thì phiền họ. Không giải quyết được thì không có xung đột để trả giá — nên nếu tránh được, luôn tốt hơn.

Cách tránh phổ biến: định tuyến mọi ghi của một bản ghi cụ thể về cùng một leader (ví dụ theo user-id hoặc theo khoá). Khi mỗi khoá chỉ có một nơi ghi, không bao giờ có hai ghi song song lên nó. Nhiều hệ "multi-leader" thực chất chạy như nhiều single-leader phân theo khoá, chỉ giải quyết xung đột thật khi buộc phải (offline/cộng tác).

Q4
Last-write-wins có hai nhược điểm nghiêm trọng. Nêu và giải thích vì sao chúng nguy hiểm.

(1) Mất dữ liệu: LWW giữ một ghi và vứt các ghi còn lại — một ghi hợp lệ biến mất không dấu vết. Với dữ liệu tích luỹ (giỏ hàng, danh sách) hay quan trọng, đây là mất mát im lặng.

(2) Phụ thuộc đồng hồ: "cái sau thắng" dựa trên timestamp, nhưng đồng hồ các node lệch nhau (clock skew — Module 5). Ghi thật sự xảy ra trước có thể mang timestamp lớn hơn và "thắng" nhầm. Nên khái niệm "sau" của LWW không đáng tin trong hệ phân tán.

Vì vậy LWW chỉ hợp dữ liệu được-phép-mất-một-ghi; không dùng cho dữ liệu cần bảo toàn.

Q5
Vector clock giúp gì trong conflict resolution mà timestamp (LWW) không làm được?

Timestamp chỉ cho một con số để so "trước/sau" — và nó sai khi đồng hồ lệch, đồng thời không phân biệt được hai ghi là song song hay nhân quả.

Vector clock theo dõi quan hệ nhân quả thật: nó cho biết ghi B có thấy ghi A trước khi xảy ra không. Nhờ đó hệ phân biệt: nếu B nhân quả sau A → B đè A hợp lệ (không phải xung đột); nếu A và B song song (không cái nào thấy cái kia) → đây là xung đột thật, cần gộp thay vì âm thầm vứt một cái. Kết quả: không mất dữ liệu oan như LWW, và quyết định "đè hay gộp" dựa trên nhân quả thật chứ không đoán bằng đồng hồ.

Bài tiếp theo: Leaderless — mọi bản bình đẳng, dùng quorum

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên