change data capture với debezium: giải quyết bài toán cache invalidation & outbox pattern

- Published on
- /7 mins read/
Có hai bài toán khó nhất trong Khoa học Máy tính: Đặt tên biến và Xóa bộ nhớ đệm (Cache Invalidation). Và Change Data Capture sinh ra để giải bài toán thứ hai một cách chuẩn mực nhất.
Trong hầu hết các ứng dụng web và microservice, chiến lược tăng tốc phổ biến nhất là đặt một lớp Redis Cache phía trước cơ sở dữ liệu (PostgreSQL/MySQL).
Mọi thứ bắt đầu bằng một đoạn code tưởng chừng như rất hiển nhiên:
// Mô hình Dual-Write ngây thơ:
public void updateUser(User user) {
userRepository.save(user); // 1. Ghi vào Database
redisTemplate.delete(user.getId()); // 2. Xóa Cache để lần sau đọc dữ liệu mới
}Đây chính là mẫu thiết kế Dual-Write (Ghi kép) — và nó là khởi nguồn của hàng loạt các lỗi bất đồng bộ dữ liệu (Data Inconsistency) bí ẩn mà các kỹ sư backend phải thức trắng đêm để truy tìm!
Tại sao Dual-Write lại nguy hiểm? Làm thế nào để áp dụng Change Data Capture (CDC) với Debezium và Transactional Outbox Pattern để đảm bảo dữ liệu luôn nhất quán 100%?
Bài viết này là đúc kết từ những bài học kiến trúc phân tán thực chiến được thảo luận sôi nổi trong cộng đồng vnJUG.
# cái bẫy chết người của mô hình dual-write
Khi bạn cố gắng ghi dữ liệu vào hai hệ thống độc lập (Database và Redis) mà không có Distributed Transaction (2PC), bạn chắc chắn sẽ đâm vào hai bức tường vật lý sau:
Kịch bản 1: Mất kết nối mạng một phần (Partial Failure)
- Bạn ghi Database thành công (
COMMIT). - Trước khi kịp gọi lệnh xóa Redis, mạng bị ngắt hoặc tiến trình JVM bị crash/kill.
- Hậu quả: Dữ liệu mới đã nằm trong Database, nhưng Redis vẫn ôm khư khư dữ liệu cũ. Khách hàng sẽ nhìn thấy thông tin sai lệch cho đến khi cache hết hạn (TTL)!
Kịch bản 2: Race Condition do Concurrency (Nghịch đảo thứ tự ghi)
Chỉ cần một chút trễ mạng (Network Jitter) hoặc một đợt tạm dừng Garbage Collection (GC Pause), thứ tự ghi vào Cache có thể bị đảo ngược so với thứ tự commit trong Database.
# giải pháp chuẩn mực: change data capture (cdc)
Thay vì để code ứng dụng tự tay đi cập nhật Cache, tại sao chúng ta không coi Cơ sở dữ liệu là Nguồn Sự Thật Duy Nhất (Single Source of Truth) và để một công cụ chuyên trách lắng nghe trực tiếp mọi biến động dữ liệu từ gốc?
Đó chính là nguyên lý của Change Data Capture (CDC):
Cách thức vận hành:
- Ứng dụng của bạn chỉ làm một việc duy nhất: Ghi vào Database và commit transaction. Không cần biết Redis hay Elasticsearch là gì!
- Mọi hệ quản trị RDBMS (PostgreSQL, MySQL, Oracle) đều ghi nhận mọi thay đổi vào một cuốn nhật ký nhị phân tuần tự gọi là WAL (Write-Ahead Log) hoặc Binlog trước khi ghi vào đĩa.
- Debezium giả lập thành một node Replica, âm thầm đọc dòng chảy log này theo thời gian thực (độ trễ chỉ vài mili-giây) và biến từng dòng INSERT, UPDATE, DELETE thành một JSON/Avro event đẩy vào Kafka.
- Các consumer phía sau đọc event từ Kafka theo đúng thứ tự (guaranteed ordering theo partition key) và thực hiện xóa cache hoặc cập nhật Elasticsearch.
Ưu điểm vượt trội:
- Zero Race Condition: Dữ liệu trong Kafka phản ánh chính xác 100% thứ tự commit trong Database.
- Tách rời hoàn toàn (Decoupled): Tầng ghi của ứng dụng trở nên siêu nhanh và thanh thoát, không bị chậm lại bởi các hệ thống phụ trợ.
# transactional outbox pattern: cứu cánh cho microservices
Trong kiến trúc Microservice, một bài toán kinh điển khác là: "Khi tạo một Đơn hàng mới trong Order Service, làm sao đảm bảo 100% sự kiện OrderCreatedEvent sẽ được gửi sang Kafka để Payment Service xử lý?".
Nếu dùng code thường:
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // Ghi DB
kafkaTemplate.send("orders", order); // Gửi Kafka: Nếu Kafka chết thì sao?
}Nếu Kafka gặp sự cố hoặc timeout, transaction DB sẽ bị rollback hoặc bị sai lệch.
Giải pháp Transactional Outbox với Debezium:
- Bạn tạo thêm một bảng
outbox_tabletrong cùng database của Order Service:CREATE TABLE outbox_table ( id UUID PRIMARY KEY, aggregate_type VARCHAR(255), aggregate_id VARCHAR(255), event_type VARCHAR(255), payload JSONB NOT NULL, created_at TIMESTAMP NOT NULL ); - Khi tạo đơn hàng, bạn lưu dữ liệu vào bảng
ordersVÀ chèn một bản ghi vàooutbox_tabletrong cùng một transaction duy nhất. Điều này đảm bảo: hoặc cả hai cùng thành công, hoặc cả hai cùng rollback! - Debezium Outbox Event Router đọc bảng
outbox_tablequa WAL và tự động chuyển tiếp sang Kafka topic tương ứng với độ tin cậy At-Least-Once Delivery tuyệt đối mà không cần đến giao thức Two-Phase Commit (2PC) cồng kềnh.
# kinh nghiệm thực chiến khi triển khai debezium
- Cấu hình Slot & WAL Retention trong PostgreSQL:
- Debezium sử dụng Replication Slot trong PostgreSQL. Nếu Debezium bị sập trong vài ngày mà không khởi động lại, PostgreSQL sẽ giữ lại toàn bộ các file WAL trên đĩa cứng để chờ Debezium đọc
\toDễ làm đầy 100% dung lượng ổ cứng của Database! - Luôn thiết lập
max_slot_wal_keep_sizevà giám sát kích thước Replication Slot trên Grafana.
- Debezium sử dụng Replication Slot trong PostgreSQL. Nếu Debezium bị sập trong vài ngày mà không khởi động lại, PostgreSQL sẽ giữ lại toàn bộ các file WAL trên đĩa cứng để chờ Debezium đọc
- Chiến lược Schema Evolution:
- Khi bảng database thêm hoặc sửa cột, hãy sử dụng Schema Registry (Confluent hoặc Apicurio) với định dạng Avro/Protobuf để các consumer phía sau không bị crash khi cấu trúc event thay đổi.
- Chỉ xóa Cache (Cache Eviction) thay vì Cập nhật Cache (Cache Update):
- Trong luồng Consumer của Kafka, hãy luôn gọi
redis.delete(key)thay vìredis.set(key, newValue). Việc xóa cache sẽ kích hoạt cơ chế Lazy Loading ở request đọc tiếp theo, loại bỏ hoàn toàn nguy cơ ghi đè dữ liệu cũ.
- Trong luồng Consumer của Kafka, hãy luôn gọi
# tổng kết
Mô hình Dual-Write là một món nợ kỹ thuật hấp dẫn vì nó dễ làm lúc ban đầu, nhưng sẽ đòi hỏi cái giá rất đắt khi hệ thống scale lên.
Bằng cách áp dụng Change Data Capture (CDC) với Debezium, bạn nâng tầm hệ thống lên tiêu chuẩn Enterprise:
- Tôn trọng nguyên lý Single Source of Truth của Database.
- Đảm bảo tính nhất quán dữ liệu cho Cache Invalidation và Search Engine.
- Giải quyết bài toán giao dịch phân tán giữa các Microservices bằng Transactional Outbox Pattern một cách thanh lịch và bền bỉ.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # cái bẫy chết người của mô hình dual-write
- Kịch bản 1: Mất kết nối mạng một phần (Partial Failure)
- Kịch bản 2: Race Condition do Concurrency (Nghịch đảo thứ tự ghi)
- # giải pháp chuẩn mực: change data capture (cdc)
- Cách thức vận hành:
- # transactional outbox pattern: cứu cánh cho microservices
- Giải pháp Transactional Outbox với Debezium:
- # kinh nghiệm thực chiến khi triển khai debezium
- # tổng kết