
Giải bài toán “Vừa ghi xong, đọc không thấy”: Read-Your-Writes Consistency & Session Sticky
Bạn vừa triển khai Database Replication để giảm tải cho Primary DB và tối ưu latency cho người dùng ở các vùng địa lý khác nhau. Mọi thứ có vẻ ổn cho đến khi người dùng than phiền: “Tôi vừa nhấn lưu bài viết, nhưng F5 lại thì thấy nội dung cũ”.
Chào mừng bạn đến với thế giới của Replication Lag.
1. Bản chất của vấn đề: Khi Eventual Consistency “phản chủ”
Trong mô hình Asynchronous Replication (Sao chép không đồng bộ), khi bạn ghi dữ liệu vào Primary, nó cần một khoảng thời gian (từ vài miligiây đến vài giây) để đồng bộ sang các bản Replica. Khoảng trễ này gọi là Replication Lag.
Nếu User vừa thực hiện lệnh Write vào Primary, ngay sau đó thực hiện lệnh Read nhưng Load Balancer lại điều hướng yêu cầu này tới một Replica chưa kịp cập nhật, người dùng sẽ thấy dữ liệu cũ. Điều này vi phạm nguyên tắc Read-Your-Writes (RYW) Consistency.
2. Các chiến lược giải quyết: Từ đơn giản đến phức tạp
Cách 1: Pinning User to Primary (Session Sticky)
Đây là cách tiếp cận phổ biến nhất. Sau khi một User thực hiện thao tác ghi, hệ thống sẽ “đánh dấu” và ép tất cả các yêu cầu đọc của User đó về Primary trong một khoảng thời gian nhất định (ví dụ 10-30 giây).
- Ưu điểm: Cực kỳ dễ triển khai ở tầng Application hoặc Load Balancer.
- Nhược điểm: Nếu ứng dụng có lượng Write lớn, Primary vẫn bị stress. Replicas có thể bị “bỏ rơi” hoặc không tận dụng được hết công suất.
- Biến thể (Fragmented Pinning): Chỉ ép các “Critical Reads” (những bảng dữ liệu vừa ghi) về Primary, còn các dữ liệu tĩnh khác vẫn đọc từ Replica.
Cách 2: Bitbucket’s Approach - Smart Routing dựa trên LSN
Khi lướt trên internet, tôi đã tìm thấy một solution khá hay đến từ Bitbucket Link
Bitbucket đã giải quyết vấn đề này một cách rất thông minh thông qua việc theo dõi LSN (Log Sequence Number) hoặc Timestamp.
-
Cơ chế: Khi User ghi dữ liệu thành công, Primary DB trả về một mã định danh của bản ghi đó (LSN). Ứng dụng lưu LSN này vào Cookie hoặc Session của User.
-
Khi Read: Ứng dụng gửi kèm LSN này. Hệ thống sẽ kiểm tra: Replica đã cập nhật đến LSN này chưa?
-
Nếu rồi: Cho phép đọc từ Replica.
-
Nếu chưa: Điều hướng về Primary hoặc bắt Replica phải đợi cho đến khi bắt kịp LSN đó.
-
Lợi ích: Tối ưu hóa tối đa việc sử dụng Replica mà vẫn đảm bảo 100% người dùng thấy dữ liệu mới nhất của chính họ.
Cách 3: Sử dụng Cache Layer (Redis) làm “Vùng đệm”
Thay vì tin tưởng hoàn toàn vào DB, ta ghi dữ liệu mới vào Redis với TTL ngắn (bằng hoặc lớn hơn một chút so với Max Replication Lag).
- Logic: Read từ Redis trước -> Không có thì mới Read từ DB.
- Cẩn thận: Đây là giải pháp “con dao hai lưỡi”. Nếu không quản lý tốt, bạn sẽ gặp tình trạng Cache Invalidation phức tạp, dẫn đến code sẽ cực kỳ “smell” và cực kỳ khó debug khi dữ liệu giữa Redis và DB không khớp.
Cách 4: Database-Level Solutions (AWS Aurora Write Forwarding)
Một số dịch vụ Cloud hiện đại như AWS Aurora hỗ trợ Write Forwarding. Bạn có thể gửi lệnh Write thẳng vào bản Read (Replica), bản Read này sẽ tự chuyển tiếp lên Primary và tự động giữ cho Session đó luôn thấy dữ liệu mới nhất. Đây là giải pháp “nhà giàu” vì nó tốn kém nhưng cực kỳ nhàn cho Engineer.
3. Những quan điểm trái chiều: Có nên dùng Replica cho Read Traffic?
Có nhiều luồng quan điểm mạnh mẽ cho rằng: Đừng đẩy Read Traffic từ App lên Replica chỉ để giảm tải. Việc xử lý RYW Consistency quá phức tạp. Nếu hệ thống của bạn đòi hỏi dữ liệu phải “tươi” 100%, việc cố đấm ăn xôi dùng Replica sẽ tạo ra hàng tá bug tiềm ẩn.
Thay vào đó, hãy đo lường primary instance của bạn, nếu CPU chỉ ở ngưỡng 60-70% trở xuống một cách ổn định, nghĩa là nó vẫn chịu tải tốt, và cân nhắc vertical scale đầu tiên, tăng sức mạnh của primary instance trong mức cho phép (vì đây là cách đơn giản nhất và hiệu quả ngay lập tức)
Hãy cân nhắc sử dụng replicate cho các trường hợp như làm report, analytics, hoặc ứng dụng của bạn không cần dữ liệu tươi 100%
Nếu cần hiệu suất cao, hãy tập trung vào Caching (Redis/Memcached) thay vì tìm cách tối ưu Replication Lag.
4. Tổng kết & Lời khuyên triển khai
Việc chọn giải pháp nào phụ thuộc vào “khẩu vị” rủi ro và độ phức tạp của hệ thống:
- Hệ thống nhỏ/vừa: Dùng Session Sticky (Pinning to Master). Sau khi ghi, cho user đọc từ Master trong 20s. Đơn giản, hiệu quả.
- Hệ thống Scale lớn (như Bitbucket/GitHub): Cần triển khai LSN Tracking qua Cookie để tận dụng Replica mà không hi sinh trải nghiệm người dùng.
- Hệ thống ưu tiên tốc độ cực cao: Dùng Redis làm lớp đệm phía trước, nhưng phải có pattern clear để tránh lỗi logic.
Lời khuyên cuối cùng: Trước khi áp dụng bất kỳ kỹ thuật phức tạp nào, hãy đo đạc (Benchmark) lại Primary DB. Đôi khi chỉ cần một cái Index tốt hoặc nâng cấp cấu hình DB là đủ để giải quyết vấn đề mà không cần đến hàng chục node Replica.