+2

Vừa bấm Update, F5 lại ra dữ liệu cũ: Lỗi tại code hay tại... nhân phẩm?

Chắc hẳn anh em dev nào trong đời cũng từng ít nhất một lần nhận được tin nhắn rợn người từ QA hoặc khách hàng: "Em ơi, sao chị vừa đổi tên sản phẩm, lưu thành công rồi mà ra ngoài trang chủ vẫn thấy tên cũ thế này?"

Lúc đó, phản xạ tự nhiên của chúng ta là: bật DevTools, check lại API response, ngó vào database... Mọi thứ trong DB đã update mới tinh, nhưng API vẫn trả về cái dữ liệu từ đời thuở nào. Bạn vò đầu bứt tai, lẩm bẩm: "Ma làm à?".

Không có ma quỷ nào ở đây cả anh em ạ. Lỗi này là "đặc sản" của việc hệ thống bắt đầu phình to ra. Dưới đây là những thủ phạm chính mà tôi đã đúc kết được sau bao đêm thức trắng gỡ bug.

1. Kẻ thủ ác mang tên "Cache" (Thường gặp nhất)

Trong giới lập trình có câu: "Có 2 thứ khó nhất trong khoa học máy tính: Đặt tên biến và xóa cache". Khi hệ thống bắt đầu có lượng truy cập lớn, bạn không thể cứ mỗi request lại đè đầu cưỡi cổ database ra mà query. Thế là bạn đưa Redis (hoặc Memcached) vào làm tấm khiên chắn. Dữ liệu đọc ra sẽ được lưu tạm vào cache.

Vấn đề xảy ra khi: Bạn update dữ liệu trong Database thành công, nhưng lại quên... xóa (invalidate) hoặc cập nhật lại cái key tương ứng trong Redis. Hệ quả là user F5, hệ thống thấy trong cache vẫn còn dữ liệu nên bê nguyên xi cái cũ trả về.

Cách xử lý thực tế: * Luôn nhớ quy tắc đồng bộ: Write DB -> Update/Delete Cache.

  • Cẩn thận với các chiến lược cache (Cache-Aside, Write-Through). Thà xóa hẳn key cache đi để request sau tự động query DB và lưu cache mới, còn hơn là tự tính toán update lại value trong cache (rất dễ sai sót).

2. Độ trễ đồng bộ Database (Database Replication Lag)

Giả sử dự án của bạn dùng PostgreSQL, database phình to và bạn bắt đầu chia kiến trúc theo kiểu Master - Slave (Primary - Replica). Master chịu trách nhiệm Ghi (Write), còn các con Slave chịu trách nhiệm Đọc (Read) để giảm tải. Khách hàng vừa nhấn nút Update (Ghi vào Master), sau đó màn hình tự động redirect về trang danh sách (Đọc từ Slave). Vì Slave chưa kịp nhận dữ liệu mới từ Master, nó tự tin trả về dữ liệu cũ. Toang!

Cách xử lý thực tế:

Nếu là những tác vụ người dùng vừa cập nhật xong và cần xem ngay (chính bản thân họ sửa), hãy route cái request Read tiếp theo thẳng vào Master.

Hoặc xài mẹo ở phía frontend: Tự động update state hiển thị ở client ngay khi API update trả về success, không gọi lại API Get ngay lập tức.

3. Hệ thống phân tán và Tính nhất quán cuối (Eventual Consistency)

Nếu dự án bạn chạy Microservices, vấn đề này còn "khoai" hơn. Dữ liệu không nằm chung một chỗ mà rải rác ở nhiều service khác nhau.

Ví dụ: Bạn lưu thông tin đơn hàng ở PostgreSQL (Service A), nhưng để phục vụ việc tìm kiếm siêu tốc, bạn lại sync dữ liệu đó sang Elasticsearch (Service B) thông qua Apache Kafka.

Vấn đề xảy ra khi: Khi cập nhật ở DB gốc xong, Kafka bắn event báo cho Elasticsearch update. Nhưng hệ thống message queue hoặc worker đang bị nghẽn, event chạy chậm. User tìm kiếm ngay lúc đó sẽ thấy dữ liệu cũ mèm do Elasticsearch chưa kịp crawl bản ghi mới. Đây chính là khái niệm Eventual Consistency (Tính nhất quán cuối) - dữ liệu KHÔNG đồng bộ ngay lập tức, mà "cuối cùng rồi nó sẽ đồng bộ".

Cách xử lý thực tế:

  • Ở góc độ kỹ thuật: Tối ưu lại consumer, theo dõi lag của Kafka partition.
  • Ở góc độ sản phẩm (Product): Phải đàm phán với khách hàng/PO rằng hệ thống lớn chấp nhận độ trễ vài giây để đổi lấy performance. Hiển thị thông báo kiểu: "Dữ liệu của bạn đang được xử lý và sẽ cập nhật trong ít phút" thay vì để họ hoang mang.

4. Bỏ quên Transaction Isolation Level

Cái này sâu hơn về DB một chút. Đôi khi bạn xài transaction để update nhiều bảng cùng lúc. Data đã update ở dòng lệnh 1, nhưng vì transaction ở dòng lệnh 3 bị lỗi hoặc chưa kịp commit, mà một request read khác lại bay vào query. Tùy thuộc vào Isolation Level (ví dụ mức Read Committed), cái request read kia sẽ không nhìn thấy dữ liệu bạn vừa sửa (vì đã commit đâu).

Tổng kết

Dữ liệu cũ xuất hiện sau khi update đa phần không phải do "nhân phẩm", mà là hệ quả của việc chúng ta áp dụng các kỹ thuật tăng cường hiệu năng (Caching, Replication, Message Broker) nhưng chưa kiểm soát hết luồng sống của dữ liệu.

Lần tới nếu bị sếp gõ đầu vì lỗi này, hãy bình tĩnh lôi những kiến trúc trên ra giải thích nhé. Chúc anh em fix bug vui vẻ và không bị ám ảnh bởi F5!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí