🎉 Giảm 20% cho đơn hàng đầu tiên!✦🚚 Miễn phí vận chuyển cho đơn trên 500.000₫✦⚡ Flash Sale — Số lượng có hạn!✦🎁 Quà tặng kèm cho đơn từ 1.000.000₫✦🎉 Giảm 20% cho đơn hàng đầu tiên!✦🚚 Miễn phí vận chuyển cho đơn trên 500.000₫✦⚡ Flash Sale — Số lượng có hạn!✦🎁 Quà tặng kèm cho đơn từ 1.000.000₫✦
Redis cache và stale data: database đã update nhưng client còn đọc dữ liệu cũ
Quay lại Blog

Redis cache và stale data: database đã update nhưng client còn đọc dữ liệu cũ

25 tháng 10, 20268 phút đọcVũ Khôi
Tin tức
#Wotech
Vũ Khôi

Vũ Khôi

Tác giả tại Wotech Electronics Blog

Redis stale cache — dữ liệu cũ trong cache

Bạn vừa cập nhật giá sản phẩm trong database, kiểm tra bản ghi đã đổi đúng — nhưng khách hàng refresh trang mấy lần vẫn thấy giá cũ. Không phải "lỗi ma": rất có thể Redis cache đang giữ dữ liệu cũ (stale data) — một trong những vấn đề khó xưa nhất của kỹ thuật phần mềm. Bài viết này giúp bạn hiểu nguyên nhân và các giải pháp chuẩn ngành.

Stale data trong cache là gì?

Cache (bộ nhớ đệm) là lớp lưu trữ nhanh đặt giữa ứng dụng và database nhằm giảm tải và rút ngắn thời gian phản hồi; Redis là phần mềm cache in-memory (lưu trên RAM) phổ biến nhất hiện nay. Vấn đề nảy sinh khi database đã thay đổi nhưng bản sao trong cache thì chưa — bản sao "quá hạn" đó chính là stale data (dữ liệu cũ, lỗi thời). Đáng sợ ở chỗ: database đúng, service chạy tốt, không một dòng log nào báo lỗi — nhưng người dùng liên tục đọc thấy thông tin cũ.

Phil Karlton (Netscape) từng đùa rằng:

"Trong khoa học máy tính chỉ có hai việc khó: vô hiệu hóa cache (cache invalidation) và đặt tên cho biến."

Đến nay câu đùa vẫn đúng: gắn cache vào thì dễ — giữ cache nhất quán với database mới khó.

Kịch bản kinh điển: DB update xong nhưng cache chưa được xóa

Luồng xử lý tưởng như đơn giản:

DB UPDATE → DELETE REDIS KEY (cập nhật database → xóa key tương ứng trong Redis)

Lần đọc sau không thấy key, ứng dụng load dữ liệu mới từ database và ghi lại vào cache — trên giấy chỉ hai bước, không chỗ để sai.

Nhưng câu hỏi đắt giá nhất của bài toán cache invalidation (vô hiệu hóa bộ nhớ đệm) là: điều gì xảy ra nếu database đã update xong mà lệnh xóa key không được thực thi, hoặc thực thi không đúng lúc? Ngay khoảnh khắc đó, database và cache trở nên không nhất quán (inconsistent) — và stale data được phục vụ cho người dùng, có khi trong nhiều giờ.

Năm nguyên nhân phổ biến khiến cache "tồn kho" dữ liệu cũ

  1. Ứng dụng gặp lỗi giữa hai bước. Database đã commit, nhưng tiến trình ứng dụng crash hoặc gặp exception trước khi kịp gửi lệnh DELETE sang Redis. Giao dịch với hai hệ thống riêng biệt không tự nguyên tử (atomic) — gốc rễ kỹ thuật của vấn đề.
  2. Tranh chấp thời điểm (race condition). Một request đọc dữ liệu cũ từ database, bị treo do tải cao, rồi mới ghi dữ liệu cũ ấy vào cache — ghi đè lên key mà request ghi vừa xóa. Dữ liệu cũ "sống lại" dù quy trình đã tuân thủ đúng.
  3. Nhiều instance ứng dụng, invalidation không đến nơi. Hệ thống nhân bản ngang có nhiều bản sao service; nếu chỉ bản sao ghi xóa key mà các bản còn lại giữ cache tại chỗ (local cache), chúng vẫn mù thông tin thay đổi.
  4. Dữ liệu bị thay đổi ngoài ứng dụng. Lệnh SQL chạy tay, job ETL nạp lại dữ liệu ban đêm… đều ghi vào database mà không biết tới cache. Sáng hôm sau, ứng dụng vẫn hiển thị thế giới của hôm qua.
  5. Không đặt TTL. TTL (Time-To-Live — thời gian sống của key) là "lưới an toàn" khiến key tự hết hạn. Key không TTL rơi vào một trong bốn tình huống trên sẽ giữ dữ liệu cũ vô thời hạn.

Hệ quả với doanh nghiệp

Stale data trượt thẳng xuống trải nghiệm khách hàng và vận hành:

  • Thương mại điện tử: khách thấy giá cũ, tồn kho sai — chốt đơn mới phát hiện hết hàng hoặc chênh giá.
  • Báo cáo nội bộ: dashboard hiển thị số liệu cũ, quản lý ra quyết định trên thông tin không còn đúng.
  • Chăm sóc khách hàng: nhân viên thấy trạng thái đơn mới nhất, khách hàng thấy trạng thái cũ — hai bên "cãi nhau" vì hai nguồn sự thật.
  • Mất niềm tin: lỗi lặp lại vài lần, người dùng nội bộ bắt đầu không tin hệ thống — thứ tốn kém nhất để xây lại.

Các giải pháp chuẩn ngành

Không có giải pháp duy nhất; thực tế là kết hợp nhiều lớp phòng thủ theo mức độ nhất quán cần thiết.

1. TTL cho mọi key — lưới an toàn bắt buộc

Đặt TTL cho toàn bộ key, kể cả khi đã có invalidation chủ động. TTL không tạo nhất quán tức thì, nhưng dữ liệu cũ chỉ sống thêm tối đa đúng bằng TTL. Dữ liệu không nhạy cảm thời gian thường đủ với TTL vài phút; dữ liệu nhạy cảm (giá, tồn kho, trạng thái đơn) cần TTL ngắn hơn.

2. Cache-aside + xóa key sau khi ghi

Pattern (mẫu thiết kế) phổ biến nhất: ứng dụng tự quản lý đọc/ghi cache (cache-aside), và khi cập nhật dữ liệu thì xóa key thay vì ghi đè giá trị. Xóa an toàn hơn ghi đè vì tránh hai request ghi đè lẫn nhau và tránh ghi dữ liệu sai vào cache.

3. Xóa hai lần (delayed double delete)

Chống race condition: sau khi update database, xóa key lần một; chờ một khoảng ngắn (vượt quá thời gian một request đọc có thể treo); rồi xóa lần hai để dọn trường hợp request cũ kịp ghi lại dữ liệu cũ giữa hai bước.

4. Ghi xuyên cache (write-through / write-behind)

Ứng dụng chỉ ghi vào một lớp: cache chịu trách nhiệm đồng bộ xuống database. Write-through ghi đồng bộ — nhất quán cao, độ trễ ghi tăng; write-behind ghi bất đồng bộ — nhanh hơn, nhưng có nguy cơ mất dữ liệu nếu cache hỏng trước khi kịp ghi xuống.

5. Vô hiệu hóa theo sự kiện (event-driven eviction)

Khi database thay đổi, hệ thống phát sự kiện qua message queue (hàng đợi tin nhắn như Kafka, RabbitMQ) hoặc Redis Pub/Sub (cơ chế xuất bản/đăng ký tin nhắn của Redis) tới toàn bộ instance để xóa key tương ứng. Triệt để nhất cho hệ nhiều service — đổi lại là độ phức tạp vận hành tăng đáng kể.

Bảng tóm tắt: chọn giải pháp nào?

Chiến lược Mức nhất quán Độ phức tạp Phù hợp khi
TTL ngắn Cuối cùng hội tụ Thấp Dữ liệu chấp nhận trễ vài phút
Xóa key sau ghi Cao (một nguồn ghi) Thấp Ứng dụng đơn, ít instance
Xóa hai lần Cao hơn, chặn race condition Trung bình Lưu lượng đọc cao
Write-through Cao Trung bình Chấp nhận độ trễ ghi
Event-driven eviction Cao nhất Cao Nhiều service, nhiều instance

Nguyên tắc chung: TTL là bắt buộc với mọi key; invalidation chủ động được chồng lên trên tùy mức độ nhạy cảm của dữ liệu.

Góc nhìn cho doanh nghiệp Việt Nam

Với đa số hệ thống của doanh nghiệp vừa và nhỏ tại Việt Nam:

  • Bắt đầu bằng cache-aside + xóa key sau ghi + TTL cho mọi key — đơn giản, phủ được phần lớn tình huống.
  • Với dữ liệu ảnh hưởng tiền bạc (giá, tồn kho, số dư), thêm xóa hai lần hoặc bỏ hẳn cache với trường hợp cực nhạy cảm.
  • Chỉ đầu tư event-driven eviction khi hệ thống đã thực sự nhiều service, nhiều đội — không phải vì "thời đại", mà vì bài toán đã đến ngưỡng cần nó.

Kiểm tra nhanh: mọi key Redis của bạn đều có TTL chưa? Rất nhiều dự án phải trả lời "chưa" — và đó chính là mồi cho stale data.

Kết luận

Stale data là cái giá của tốc độ mà cache mang lại — không thể loại bỏ hoàn toàn, chỉ có thể thu hẹp cửa sổ không nhất quán xuống mức chấp nhận được: TTL làm lưới an toàn, xóa key chủ động làm việc hằng ngày, event-driven làm nền tảng dài hạn cho hệ thống lớn. Nắm rõ năm nguyên nhân và năm giải pháp trên là nắm phần lõi của bài toán cache consistency.

Nguồn: tổng hợp từ chia sẻ kỹ thuật trên Facebook, biên tập bởi Wotech Electronics.

Hệ thống của bạn đang gặp tình trạng database đã update nhưng ứng dụng vẫn hiển thị dữ liệu cũ? Liên hệ Wotech để được rà soát kiến trúc cache và tối ưu độ nhất quán dữ liệu cho hệ thống của doanh nghiệp.