Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là PM của một sàn thương mại điện tử Việt Nam. Đúng 12 giờ trưa thứ Sáu, marketing chạy flash sale, traffic tăng gấp 20 lần trong vài phút. Trang chủ load chậm dần, rồi timeout, rồi sập. Khách hàng bỏ giỏ hàng, doanh thu bốc hơi. Khi bạn ngồi xuống làm postmortem với đội kỹ thuật, câu đầu tiên engineer nói sẽ là: "Database không chịu nổi tải, chúng ta thiếu cache layer ở chỗ này."
Caching (bộ nhớ đệm) là một trong những "vũ khí" rẻ và hiệu quả nhất để hệ thống vừa nhanh vừa chịu tải tốt. Vấn đề là caching không miễn phí: nó đánh đổi giữa tốc độ và độ tươi của dữ liệu (data freshness). Một PM kỹ thuật không cần tự cấu hình Redis, nhưng bắt buộc phải hiểu cache hoạt động ra sao để: (1) ra quyết định đúng khi nào chấp nhận dữ liệu cũ vài giây để đổi lấy tốc độ, (2) lý giải được vì sao "khách hàng thấy giá cũ" hay "đã cập nhật mà UI chưa đổi", và (3) ước lượng được chi phí hạ tầng khi scale.
Bài này nằm trong nhóm về hiệu năng và khả năng mở rộng. Ở Bài 47 bạn đã học về latency, throughput và availability — những chỉ số đo lường. Caching chính là một trong những đòn bẩy mạnh nhất để cải thiện chúng. Sau bài này bạn sẽ ngồi họp với engineer mà không bị "ngợp" mỗi khi họ nhắc tới cache hit ratio, TTL hay invalidation.
Khái niệm cốt lõi
Vì sao caching lại quyền lực đến vậy
Cache là việc lưu sẵn kết quả của một phép tính hoặc một truy vấn tốn kém, để lần sau lấy ra ngay thay vì làm lại từ đầu. Ba lý do khiến nó trở thành công cụ kinh điển:
- Giảm latency (độ trễ). Đọc từ bộ nhớ RAM (in-memory) mất khoảng vài micro giây (µs), trong khi một truy vấn database trên đĩa có thể mất vài mili giây (ms) — chênh nhau cả nghìn lần. Với người dùng, đó là khác biệt giữa "tức thì" và "ì ạch".
- Giảm tải cho database. Mỗi request lấy được từ cache là một request không chạm vào DB. Database thường là "nút thắt cổ chai" đắt đỏ và khó scale nhất trong hệ thống, nên giảm tải cho nó chính là giảm rủi ro sập hệ thống.
- Tiết kiệm chi phí. Khi CDN cache ảnh, CSS, video tĩnh ở biên mạng (edge), bạn không phải trả băng thông và compute từ server gốc cho từng lượt tải. Với sản phẩm hàng triệu user, đây là khoản tiền rất lớn.
Các tầng cache (cache layers)
Cache không nằm ở một chỗ duy nhất. Dữ liệu đi từ trình duyệt người dùng tới database phải qua nhiều "trạm", và mỗi trạm đều có thể đặt cache. Hiểu các tầng này giúp bạn biết "đặt cache ở đâu cho đúng vấn đề".
- Browser cache (cache trình duyệt). Trình duyệt của người dùng lưu lại ảnh, file JS/CSS, thậm chí cả phản hồi API. Được điều khiển bằng HTTP header như
Cache-ControlvàETag. Đây là tầng gần người dùng nhất, nhanh nhất, và miễn phí — nhưng bạn không kiểm soát được khi nào user xóa cache.
- CDN / Edge cache. Mạng phân phối nội dung (CDN như Cloudflare, AWS CloudFront, Bunny) đặt bản sao nội dung ở nhiều điểm gần người dùng về mặt địa lý. User ở TP.HCM lấy ảnh từ server đặt tại Singapore thay vì tận Mỹ. Cực kỳ hiệu quả cho nội dung tĩnh và cả nội dung động ít thay đổi.
- Application / server cache. Cache đặt ngay trong tầng ứng dụng, thường dùng Redis hoặc Memcached — đây là các kho lưu trữ key-value chạy trong RAM, tốc độ truy xuất tính bằng micro giây. Đây là tầng PM hay nghe nhắc nhất khi bàn về tối ưu backend.
- Database cache. Bản thân database cũng có cache nội bộ (query cache, buffer pool) giữ những trang dữ liệu hay dùng trong RAM. Tầng này engineer quản lý, PM ít can thiệp trực tiếp.
Cache hit, cache miss và TTL
- Cache hit: dữ liệu cần đã có sẵn trong cache, trả về ngay. Đây là điều ta muốn tối đa hóa.
- Cache miss: không có trong cache, phải đi xuống tầng dưới (DB) lấy, rồi lưu lại cho lần sau. Lần miss đầu tiên luôn chậm.
- TTL (Time To Live): thời gian một mục được phép sống trong cache trước khi bị coi là hết hạn. TTL = 60 giây nghĩa là sau 1 phút, dữ liệu đó sẽ được làm mới từ nguồn. TTL ngắn = dữ liệu tươi hơn nhưng hit ratio thấp hơn; TTL dài = nhanh và rẻ hơn nhưng nguy cơ phục vụ dữ liệu cũ cao hơn. Việc chọn TTL chính là một quyết định sản phẩm, không thuần kỹ thuật — và PM nên có ý kiến.
Các chiến lược ghi cache (write strategies)
Khi dữ liệu thay đổi, làm sao cache và database đồng bộ? Đây là phần PM hay bị rối nhất, nhưng chỉ cần nắm ba mô hình chính:
- Cache-aside (lazy loading): Phổ biến nhất. Ứng dụng tự hỏi cache trước; nếu miss thì đọc DB, ghi vào cache, rồi trả về. Khi dữ liệu đổi, ta xóa mục trong cache để lần sau nạp lại. Đơn giản, linh hoạt, nhưng lần miss đầu luôn chậm.
- Write-through: Mỗi lần ghi, ghi đồng thời vào cả cache và DB. Cache luôn tươi, nhưng mỗi thao tác ghi chậm hơn một chút.
- Write-back (write-behind): Ghi vào cache trước, trả về cho user ngay, rồi sau đó mới ghi xuống DB theo lô. Cực nhanh nhưng rủi ro mất dữ liệu nếu cache chết trước khi kịp ghi xuống. Dùng cho dữ liệu chấp nhận mất mát nhỏ (ví dụ đếm lượt view).
Cache invalidation — vấn đề khó nhất
Có một câu nói nổi tiếng trong ngành: "Trong khoa học máy tính chỉ có hai việc khó: đặt tên biến và invalidate cache." Cache invalidation là việc đảm bảo khi dữ liệu gốc thay đổi, các bản sao trong cache được cập nhật hoặc xóa kịp thời. Làm sai sẽ dẫn tới cảnh "đã sửa giá rồi mà khách vẫn thấy giá cũ". PM cần nhận diện đây là nguồn gốc của rất nhiều bug "kỳ lạ" và biết đặt câu hỏi đúng cho đội kỹ thuật.
Tình huống thực tế
Ví dụ 1: Sàn TMĐT chịu flash sale — đặt cache đúng tầng
Một sàn TMĐT giả định kiểu Tiki/Shopee chuẩn bị flash sale lúc 12h trưa. Trước đây, mỗi lần mở trang sản phẩm, hệ thống truy vấn DB để lấy thông tin sản phẩm, giá, tồn kho, đánh giá — khoảng 8 truy vấn cho một trang. Khi traffic tăng 20 lần, DB nhận 160 lần lượng truy vấn bình thường và sập.
Đội kỹ thuật áp dụng cache phân tầng: thông tin sản phẩm (tên, mô tả, ảnh) gần như không đổi nên cache ở Redis với TTL 5 phút và đẩy ảnh lên CDN. Riêng số lượng tồn kho thì không cache lâu — vì nếu cache giá tồn kho cũ, khách có thể đặt món đã hết hàng, dẫn tới hủy đơn hàng loạt và mất uy tín. Họ để tồn kho TTL chỉ 2 giây, hoặc đọc trực tiếp với cơ chế khóa.
Kết quả: cache hit ratio cho phần thông tin sản phẩm đạt ~97%, tải DB giảm còn 1/15, hệ thống trụ vững qua flash sale.
Bài học: Không phải mọi dữ liệu đều cache giống nhau. Vai trò PM là phân loại dữ liệu theo độ nhạy cảm với "độ tươi": mô tả sản phẩm chấp nhận cũ 5 phút, nhưng tồn kho và giá thì gần như phải tươi tức thời. Quyết định này là quyết định sản phẩm/kinh doanh, không phải thuần kỹ thuật.
Ví dụ 2: App đọc báo VnExpress — sức mạnh của CDN
Hãy hình dung một trang tin tức lớn như VnExpress. Vào giờ cao điểm sáng, hàng triệu người cùng mở một bài báo hot. Nếu mỗi request đều chạm server gốc để render trang, hệ thống sẽ quá tải ngay.
Giải pháp kinh điển: đẩy trang bài viết lên CDN với TTL ngắn (ví dụ 30–60 giây). Triệu người đọc cùng một bài chỉ tạo ra một lần "miss" chạm server gốc mỗi phút cho mỗi điểm edge; phần còn lại được phục vụ từ cache đặt gần người dùng. Băng thông và compute ở server gốc giảm tới hơn 95%. Khi tòa soạn sửa bài (chính tả, cập nhật diễn biến), họ chủ động "purge" (xóa) cache bài đó trên CDN để bản mới xuất hiện ngay, thay vì chờ 60 giây.
Bài học: Với nội dung công khai và giống nhau cho mọi người (ảnh, bài viết, trang landing), CDN là đòn bẩy rẻ và mạnh nhất. PM nên hỏi: "Nội dung này có giống nhau cho mọi user không? Nếu có, đã cache ở CDN chưa?" Và luôn yêu cầu có cơ chế purge thủ công cho tình huống khẩn cấp.
Ví dụ 3: Ví điện tử và cái bẫy cache số dư
Một ví điện tử kiểu MoMo/ZaloPay từng gặp phàn nàn: user nạp tiền xong, mở app vẫn thấy số dư cũ trong vài giây, tưởng tiền "bốc hơi" và hoảng loạn gọi tổng đài. Nguyên nhân: màn hình số dư đọc từ một cache với TTL 10 giây để giảm tải, nhưng sự kiện nạp tiền lại không invalidate cache đó.
Đội kỹ thuật sửa bằng cách: khi có giao dịch làm thay đổi số dư, lập tức xóa (invalidate) cache số dư của user đó, để lần đọc tiếp theo lấy con số mới nhất từ DB. Đồng thời họ phân loại lại: số dư là dữ liệu nhạy cảm về niềm tin, không nên cache lâu, hoặc chỉ cache theo từng user kèm invalidation chủ động.
Bài học: Với dữ liệu liên quan tiền bạc và niềm tin, hiển thị sai (dù chỉ trong vài giây) gây thiệt hại lớn về tâm lý người dùng. Đây là trường hợp PM phải ưu tiên correctness (đúng) hơn performance (nhanh), và yêu cầu chiến lược invalidation chủ động thay vì chỉ dựa vào TTL.
Hướng dẫn từng bước
Khi tham gia một quyết định liên quan đến caching, bạn có thể đi theo quy trình tư duy sau:
- Xác định dữ liệu nào đang gây tải hoặc chậm. Hỏi đội kỹ thuật: endpoint nào chậm nhất, truy vấn nào tốn tài nguyên nhất? Caching chỉ nên áp dụng vào chỗ thực sự có vấn đề, không phải mọi nơi.
- Phân loại dữ liệu theo độ nhạy cảm với độ tươi. Tự đặt câu hỏi: "Nếu user thấy dữ liệu này cũ 1 phút, chuyện gì xảy ra?" Mô tả sản phẩm cũ 5 phút thì không sao; số dư hay tồn kho cũ thì nguy hiểm. Đây là đầu vào quan trọng nhất từ phía PM.
- Chọn TTL phù hợp với từng loại dữ liệu. Cân bằng giữa hit ratio (cao = rẻ và nhanh) và độ tươi. Không có con số chuẩn; nó là quyết định đánh đổi mà bạn cùng đội thống nhất.
- Quyết định cơ chế invalidation khi dữ liệu đổi. Chỉ dựa vào TTL hết hạn, hay cần xóa cache chủ động ngay khi có thay đổi? Dữ liệu càng nhạy thì càng cần invalidation chủ động.
- Xác định tầng cache phù hợp. Nội dung tĩnh, công khai → CDN. Dữ liệu động dùng chung nhiều → Redis ở tầng app. Dữ liệu riêng từng user → cache có khóa theo user ID.
- Định nghĩa chỉ số đo lường thành công. Cache hit ratio mục tiêu, độ giảm latency p95, mức giảm tải DB. Đưa vào acceptance criteria để biết tính năng có thực sự hiệu quả.
- Lên kịch bản cho lỗi và edge case. Nếu cache (Redis) chết thì sao — hệ thống có "ngã" về đọc DB an toàn không (cache as optional)? Có rủi ro cache stampede khi nhiều request cùng miss một lúc không?
Lỗi thường gặp & mẹo
- Cache mọi thứ một cách bừa bãi. Cache làm hệ thống phức tạp hơn và sinh ra cả lớp bug mới về tính nhất quán. Chỉ cache những gì thực sự tốn kém và được đọc nhiều lần. Đừng cache dữ liệu siêu nhạy cảm (số dư, quyền hạn) mà không có invalidation chặt chẽ.
- Quên cơ chế invalidation. Đây là lỗi kinh điển dẫn tới "đã sửa mà không thấy đổi". Mỗi khi thiết kế cache, luôn hỏi: "Khi dữ liệu này đổi, cache được cập nhật bằng cách nào?"
- Cache stampede (thundering herd). Khi một mục cache phổ biến hết hạn cùng lúc, hàng nghìn request cùng miss và đồng loạt đập vào DB, gây sập. Mẹo: hỏi đội đã có cơ chế chống chưa (như khóa để chỉ một request nạp lại, hoặc làm mới cache trước khi hết hạn).
- Nhầm "cache miss chậm" là bug. Lần đầu truy cập sau khi cache hết hạn (cold cache) luôn chậm hơn. Đừng kết luận hệ thống lỗi chỉ vì lần đầu chậm; hãy nhìn vào p95/p99 trên lượng lớn request.
- Bỏ quên CDN purge trong vận hành. Khi xảy ra sự cố nội dung (đăng nhầm giá, sai thông tin), nếu không có nút "purge cache" thì bạn phải chờ TTL hết hạn. Luôn yêu cầu cơ chế purge thủ công như một phần của tính năng.
- Mẹo vàng cho PM: Khi nghe "trang chậm" hoặc "DB quá tải", câu hỏi đầu tiên nên là "phần nào có thể cache được, và đang cache ở tầng nào?". Khi nghe "dữ liệu hiển thị sai/cũ", câu hỏi nên là "cache có được invalidate khi dữ liệu đổi không?".
Bài tập thực hành
- Phân loại dữ liệu. Lấy một sản phẩm bạn đang làm hoặc quen thuộc (app gọi xe, ví điện tử, sàn TMĐT...). Liệt kê 6 loại dữ liệu khác nhau hiển thị trên màn hình chính. Với mỗi loại, ghi: (a) chấp nhận cũ tối đa bao lâu, (b) hậu quả nếu hiển thị sai, (c) đề xuất TTL và tầng cache. Đây chính là tài liệu bạn sẽ mang ra thảo luận với engineer.
- Viết acceptance criteria. Giả sử team muốn thêm cache cho trang danh sách sản phẩm. Viết 3–4 tiêu chí nghiệm thu, trong đó có ít nhất một chỉ số đo (ví dụ: cache hit ratio ≥ 90% sau 1 giờ chạy thật; latency p95 giảm còn dưới 200ms; khi đổi giá, giá mới xuất hiện trong vòng tối đa 30 giây).
- Tình huống xử lý sự cố. Khách hàng báo: "Tôi vừa đổi địa chỉ giao hàng, lưu thành công, nhưng vào lại vẫn thấy địa chỉ cũ." Viết 3 câu hỏi bạn sẽ hỏi đội kỹ thuật để khoanh vùng nguyên nhân liên quan tới caching, và nêu giả thuyết nguyên nhân gốc.
Tóm tắt
Caching là đòn bẩy mạnh nhất để hệ thống vừa nhanh, vừa rẻ, vừa chịu tải tốt — bằng cách lưu sẵn kết quả tốn kém để khỏi tính lại. Nó hoạt động qua nhiều tầng: browser cache, CDN/edge, application cache (Redis/Memcached), và database cache; mỗi tầng giải một loại bài toán khác nhau. Hai khái niệm cốt lõi PM cần nắm là TTL (dữ liệu sống bao lâu trước khi làm mới) và cache invalidation (cập nhật/xóa cache khi dữ liệu gốc đổi) — đây cũng là nguồn gốc của vô số bug "đã sửa mà không thấy đổi".
Điều quan trọng nhất: chọn cái gì để cache, TTL bao lâu, và có cần invalidation chủ động không — đều là những quyết định sản phẩm, vì chúng đánh đổi trực tiếp giữa tốc độ và độ chính xác/độ tươi của thông tin mà người dùng nhìn thấy. PM không cần cấu hình Redis, nhưng cần phân loại được dữ liệu theo độ nhạy cảm, đặt đúng câu hỏi, và đưa các chỉ số như cache hit ratio vào tiêu chí nghiệm thu. Nắm vững caching, bạn sẽ tự tin hơn rất nhiều trong những cuộc họp về hiệu năng và scale.