Consistency: strong vs eventual, replication lag, CAP, PACELC

4 câu hỏi~30 phútTừ khóa: strong consistency, eventual consistency, read-your-writes, replication lag, CAP, network partition, CP vs AP, PACELC

Tiếp đi: consistency. Bài trước nói báo cáo "eventual, trễ vài giây". Eventual cụ thể là gì và khi nào chấp nhận được?

Ta quay lại shop-app. Khi traffic tăng, đội đã thêm read replica cho DB (cơ chế replication xem Bài replication, partition, sharding):

WRITE ─► Primary ──replication──► Replica 1, 2, 3 ◄─ READ

1. Bài toán

User đổi tên hiển thị trên profile:

10:00:00.100  PUT /profile  name = "Phong Nguyen"  → Primary ✅ → 200 OK
10:00:00.150  GET /profile  → route sang Replica   → name = "Phong" ❌
10:00:00.300  replication tới Replica
10:00:01      refresh → "Phong Nguyen"  (lại đúng 😂)

User: "Ủa? Mình vừa đổi tên mà?"

2. Vấn đề

3. Giải pháp

Các mức consistency (từ mạnh đến yếu):

Cách đạt read-your-writes trong shop-app:

4. Code từng bước

Bước 1: Dependency: spring-boot-starter-data-jpa, PostgreSQL driver.

Bước 2: Config: 2 datasource, credential từ biến môi trường:

app:
  datasource:
    primary:
      url: jdbc:postgresql://pg-primary:5432/shop
      username: ${DB_USER}
      password: ${DB_PASSWORD}
    replica:
      url: jdbc:postgresql://pg-replica:5432/shop
      username: ${DB_USER}
      password: ${DB_PASSWORD}
  read-your-writes-window: 5s

Bước 3: Code Java: routing datasource, read-only đi replica trừ khi user vừa ghi.

public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        return (readOnly && !RecentWrite.isRecent()) ? "replica" : "primary";
    }
}

// Ghi nhận "user này vừa ghi" (bản thật: lưu trong Redis theo userId, hoặc cookie)
public final class RecentWrite {
    private static final ThreadLocal<Boolean> RECENT = ThreadLocal.withInitial(() -> false);
    public static void mark(boolean v) { RECENT.set(v); }
    public static boolean isRecent() { return RECENT.get(); }
    public static void clear() { RECENT.remove(); }
}

@Configuration
public class DataSourceConfig {
    @Bean
    @Primary
    DataSource dataSource(@Qualifier("primaryDs") DataSource primary,
                          @Qualifier("replicaDs") DataSource replica) {
        var routing = new ReadWriteRoutingDataSource();
        routing.setTargetDataSources(Map.of("primary", primary, "replica", replica));
        routing.setDefaultTargetDataSource(primary);
        routing.afterPropertiesSet();
        // Lazy: chỉ lấy connection khi đã biết transaction readOnly hay không
        return new LazyConnectionDataSourceProxy(routing);
    }
    // primaryDs, replicaDs: tạo HikariDataSource từ app.datasource.*
}

@Service
public class ProfileService {
    private final ProfileRepository repo;
    private final RecentWriteStore recentWrites;   // Redis: userId → TTL 5s

    public ProfileService(ProfileRepository repo, RecentWriteStore recentWrites) {
        this.repo = repo;
        this.recentWrites = recentWrites;
    }

    @Transactional
    public Profile rename(long userId, String name) {
        Profile p = repo.findById(userId).orElseThrow();
        p.setName(name);
        recentWrites.mark(userId, Duration.ofSeconds(5));
        return p;
    }

    @Transactional(readOnly = true)
    public Profile get(long userId) {
        return repo.findById(userId).orElseThrow();   // filter đã set RecentWrite từ Redis
    }
}

Bước 4: Test bằng curl

curl -s -X PUT localhost:8081/profile/42 -H 'Content-Type: application/json' -d '{"name":"Phong Nguyen"}'
curl -s localhost:8081/profile/42      # trong 5s: đọc primary → "Phong Nguyen"
curl -s localhost:8081/profile/99      # user khác: đọc replica

5. Cách hoạt động

Không read-your-writes:
user ─PUT─► Primary (Phong Nguyen) ───lag 200ms───► Replica
user ─GET──────────────────────────────────────────► Replica (Phong) ❌

Có read-your-writes:
user ─PUT─► Primary ── mark(user 42, 5s)
user ─GET─► recent? yes ─► Primary (Phong Nguyen) ✅
user khác ─GET─► Replica (có thể còn cũ, chấp nhận được)

6. So sánh

Dữ liệu trong shop-appMức cầnĐọc ở đâu
Danh sách sản phẩm, review, số likeEventualReplica, cache
Profile vừa sửa, đơn vừa đặtRead-your-writesPrimary trong cửa sổ ngắn
Trừ tồn kho, trạng thái thanh toán, số dưStrongPrimary, có lock/version
Báo cáo doanh thuEventual (trễ vài phút được)Reporting DB

7. Khi nào nên dùng / không nên dùng

CAP là chọn 2 trong 3 đúng không? Vậy mình chọn CA, bỏ P, cho shop-app vừa consistent vừa available được không?

Đây là cách hiểu sai phổ biến nhất. Chỉnh lại: P không phải thứ bạn chọn bỏ. Network partition (mạng giữa các node bị đứt/chậm) chắc chắn sẽ xảy ra trong hệ phân tán. CAP nói: khi có partition, bạn buộc phải chọn giữa C và A.

Kho có 1 chiếc iPhone. Inventory chạy 2 node ở 2 datacenter.

   Node HN  ═══X═══ (mạng đứt) ═══X═══  Node HCM
   stock=1                              stock=1
   User A mua                           User B mua

Chọn CP: node không liên lạc được với bên kia → từ chối ghi (503)
         → không bán vượt, nhưng một phía không mua được (mất A)
Chọn AP: cả hai node đều nhận → cả A và B đều mua được
         → bán vượt 1 chiếc, sau khi mạng nối lại phải xử lý xung đột (mất C)

"CA" chỉ có nghĩa với hệ một node (một Postgres đơn): không có mạng giữa các node thì không có partition. Nhưng đó là không phân tán.

Trong shop-app thường chọn theo từng tính năng:

PACELC bổ sung phần CAP bỏ qua: if P then chọn A hoặc C, Else (lúc bình thường) chọn Latency hoặc Consistency. Ví dụ replication đồng bộ (chờ replica xác nhận) thì consistent hơn nhưng mỗi write chậm hơn; async thì nhanh nhưng có replication lag như bài toán ở trên. Thực tế, trade-off latency/consistency gặp hằng ngày, còn partition thì hiếm hơn.

Vậy eventual consistency giữa các service, ví dụ order-service và inventory-service qua Kafka, có bị replication lag giống vậy không?

Đúng, cùng một bản chất, chỉ khác tầng. Database-per-service (bài 14.4) làm mỗi service giữ bản sao dữ liệu của service khác, cập nhật qua event:

catalog-service: price 900 → 800 ──event──► Kafka ──► order-service.product_view
                                    lag: ms đến vài giây (consumer chậm, rebalance...)
Trong khoảng lag, order-service vẫn thấy price = 900

Cách xử lý giống tư duy ở trên:

// Chỉ cập nhật khi event mới hơn
@Modifying
@Query("update ProductView v set v.price = :price, v.version = :version " +
       "where v.productId = :id and v.version < :version")
int applyIfNewer(String id, BigDecimal price, long version);

Còn khi nhiều service phải cùng thay đổi trạng thái (đặt hàng = trừ kho + trừ tiền) mà một bước fail thì sao? Đó là Saga, bài tiếp theo.

Phỏng vấn hỏi về consistency và CAP thì trả lời sao?

Q1. Strong consistency và eventual consistency khác nhau thế nào?

🇻🇳 Tiếng Việt:

Strong consistency bảo đảm mọi read thấy write mới nhất đã được xác nhận, như thể chỉ có một bản dữ liệu. Eventual consistency chỉ hứa rằng nếu không có write mới thì các bản sao cuối cùng sẽ giống nhau, trong lúc đó read có thể thấy dữ liệu cũ. Strong đổi lấy latency và availability; eventual thì scale tốt và chịu lỗi tốt hơn. Em chọn theo từng loại dữ liệu: tồn kho và thanh toán cần strong, catalog và review thì eventual.

🇬🇧 English:

Strong consistency guarantees every read sees the latest acknowledged write, as if there were a single copy. Eventual consistency only promises that replicas converge if writes stop, so reads may return stale data in the meantime. Strong consistency costs latency and availability, while eventual consistency scales and tolerates failures better. I choose per data type: inventory and payments need strong consistency, catalog and reviews can be eventual.

Q2. Giải thích CAP theorem.

🇻🇳 Tiếng Việt:

Trong hệ phân tán, network partition là điều không tránh được. CAP nói khi có partition, hệ thống phải chọn giữa consistency, tức từ chối request để không trả dữ liệu sai, và availability, tức vẫn trả lời nhưng có thể là dữ liệu cũ hoặc xung đột. Vì vậy "chọn CA" không có ý nghĩa với hệ phân tán. PACELC bổ sung: khi không có partition, vẫn phải đánh đổi giữa latency và consistency.

🇬🇧 English:

In a distributed system, network partitions are unavoidable. CAP says that during a partition the system must choose between consistency, rejecting requests rather than returning wrong data, and availability, answering anyway with possibly stale or conflicting data. So "choosing CA" is meaningless for a distributed system. PACELC adds that even without a partition, there is a trade-off between latency and consistency.

Q3. User sửa profile xong reload lại thấy dữ liệu cũ. Nguyên nhân và cách sửa?

🇻🇳 Tiếng Việt:

Write đi vào primary nhưng read đi vào replica đang bị replication lag, nên vi phạm read-your-writes. Cách sửa: sau khi user ghi, route read của chính user đó về primary trong một khoảng ngắn hoặc cho các màn hình nhạy cảm, hoặc so version/LSN để chỉ đọc replica đã bắt kịp. Read không nhạy cảm vẫn đọc replica.

🇬🇧 English:

The write went to the primary but the read hit a lagging replica, which breaks read-your-writes consistency. To fix it, route that user's reads to the primary for a short window after a write or for sensitive screens, or track the written version or LSN and only read from replicas that have caught up. Non-sensitive reads can still go to replicas.

Cách nhớ ngắn gọn


Tự kiểm tra

Replication lag gây ra lỗi gì cho user?

Vừa ghi xong đọc lại thấy dữ liệu cũ (vi phạm read-your-writes), hoặc dữ liệu nhảy qua lại giữa mới và cũ.

Vì sao "chọn CA" không có nghĩa với hệ phân tán?

Partition chắc chắn xảy ra; khi đó phải chọn C hoặc A. CA chỉ đúng với hệ một node.

Inventory chạy 2 datacenter, mạng đứt. CP và AP khác nhau thế nào?

CP: từ chối ghi để không bán vượt. AP: cả hai bên nhận đơn, có thể bán vượt, xử lý xung đột sau.

PACELC thêm gì so với CAP?

Khi không có partition vẫn phải đánh đổi giữa latency và consistency (ví dụ replication sync vs async).

Consumer nhận event sai thứ tự thì tránh ghi đè dữ liệu mới bằng cũ thế nào?

So version: chỉ áp dụng event có version lớn hơn bản đang lưu, không so timestamp.