Trang dashboard tải chậm ngay sau giờ ăn trưa, và cả team cảm thấy trước khi ai kịp giải thích. Hôm qua mọi thứ vẫn ổn, hôm nay biểu đồ quay vòng tám giây. Một người mở network tab. Một người nhìn database chart. Rồi câu quen thuộc xuất hiện trong chat: chắc mình nên thêm cache.
Phản xạ đó không sai. Caching là một trong những cách dễ hiểu nhất để làm hệ thống nhanh hơn. Nếu nhiều người cùng hỏi một câu trả lời đắt, hệ thống không nên tính lại từ đầu mỗi lần. Cache giống như để một tập tài liệu hay dùng ngay trên bàn thay vì mỗi câu hỏi nhỏ đều phải đi xuống kho lưu trữ. Cái bàn nhanh hơn. Kho lưu trữ vẫn là source of truth. Rắc rối bắt đầu khi team quên cái nào là bản thật.
Chiến lược nhiều team dùng đầu tiên là cache-aside. Application hỏi cache trước. Nếu có value, nó dùng luôn. Nếu không có, application đọc từ database, lưu câu trả lời vào cache, rồi trả về cho người dùng. Pattern này dễ hiểu và dễ thêm từng bước. Nó hợp với product detail, profile summary, permission, configuration và những read path được lặp lại đủ nhiều để đáng giữ một bản gần hơn.
Nhưng cache-aside cũng bắt application chịu những phần khó xử. Nó phải chọn cache key tốt. Nó phải quyết định value được sống bao lâu. Nó phải xử lý cache miss mà không làm người dùng chờ quá lâu. Nó phải remove hoặc refresh value khi source thay đổi. Một lỗi nhỏ trong các lựa chọn đó có thể biến cải thiện performance thành bug về correctness. Cache không nguy hiểm vì nó nhanh. Nó nguy hiểm khi được tin quá mức mà thiếu boundary.
Read-through caching đẩy một phần trách nhiệm đó vào cache layer. Application hỏi cache, và cache biết cách load value nếu miss. Write-through caching ghi vào cache và backing store trong cùng một flow. Write-behind caching nhận write rất nhanh rồi cập nhật backing store sau. Mỗi pattern mua một kiểu đơn giản khác nhau và trả một kiểu rủi ro khác nhau. Read-through gom logic load về một chỗ. Write-through giúp read tươi hơn. Write-behind giảm latency nhưng làm durability và recovery trở nên nghiêm túc hơn.
Time to live, thường gọi là TTL, là thỏa thuận lặng lẽ giữa tốc độ và độ mới. TTL năm phút có thể hoàn hảo cho danh sách bài viết công khai, chấp nhận được cho product catalog, nhưng rủi ro với account permission. TTL một giờ có thể vô hại với footer menu và nguy hiểm với pricing. Câu hỏi quan trọng không chỉ là cache nên sống bao lâu. Câu hỏi tốt hơn là trải nghiệm này chịu được stale data bao nhiêu, và điều gì xảy ra nếu stale value đó sai.
Invalidation là nơi nhiều team học sự khiêm tốn. Database đã thay đổi, nhưng cache entry nào cần xóa? Một user đổi tên, nhưng tên đó xuất hiện trong profile card, team list, audit view, search index, notification template và mention autocomplete. Nếu key không được thiết kế theo read path thật, invalidation trở thành một cuộc đi tìm dấu vết rối rắm trong hệ thống.
Stale data không phải lúc nào cũng là failure. Một report có thể ghi rõ rằng dữ liệu cập nhật mỗi vài phút. Một feed có thể trễ một chút. Recommendation list không cần phản ánh cú click gần nhất ngay lập tức. Nhưng có loại dữ liệu mang theo niềm tin. Permission check, account balance, seat reservation, checkout price, hoặc privacy setting không nên âm thầm dựa vào bản stale trừ khi hệ thống có safety model rất rõ.
Một cái bẫy khác là cache stampede. Hãy tưởng tượng một trang rất nhiều người xem có cached value hết hạn lúc đúng 12 giờ trưa. Lúc 12:00:01, hàng ngàn request cùng miss cache và cùng lao vào database để rebuild một câu trả lời giống nhau. Cache vốn để bảo vệ database, nhưng thời điểm expiry lại biến thành một hàng người dồn vào một quầy nhỏ. Team thường giảm việc này bằng request coalescing, TTL có jitter, background refresh, soft expiry, hoặc lock để một request làm phần việc đắt còn request khác chờ hoặc nhận value cũ hơn một chút.
Memory pressure thì yên hơn nhưng vẫn rất thật. Cache có giới hạn. Nếu key quá rộng, value quá lớn, hoặc eviction policy không khớp với usage, hệ thống có thể dành nhiều thời gian để nhét dữ liệu vào rồi vứt ra. Hit rate thấp có thể nghĩa là cache chỉ đang thêm network call, serialization cost và operational complexity. Trước khi vui vì có cache, team nên hỏi cache có đang được dùng tốt không: hit rate, miss latency, eviction rate, object size, error rate và tuổi của value trả về đều là tín hiệu cần nhìn.
Một thiết kế caching khỏe bắt đầu từ lời hứa với người dùng. Câu trả lời nào đang chậm? Vì sao chậm? Nó đổi thường xuyên không? Ai được phép thấy nó? Nó có thể sai bao nhiêu và trong bao lâu? Từ đó team chọn cache nhỏ nhất có ích. Có thể là in-process cache cho reference data. Có thể là Redis cho shared hot reads. Có thể là CDN caching cho public asset. Cũng có thể câu trả lời đúng là thêm database index, làm query nhỏ hơn, hoặc tạo read model trước thay vì thêm cache.
Caching không phải món trang trí gắn vào sau khi architecture đã xong. Nó trở thành một phần của architecture vì nó tạo thêm một nơi mà sự thật có thể được nhìn thấy, bị trì hoãn, hoặc bị hiểu nhầm. Dùng cẩn thận, cache biến công việc lặp lại thành tốc độ yên tĩnh. Dùng vội, nó giấu bug sau response nhanh. Lần tới khi một trang chậm làm ai đó nói hãy thêm cache, có lẽ câu hỏi bình tĩnh hơn là: mình đang copy sự thật nào, và mình đang hứa gì trong lúc bản copy đó tồn tại?