Chương 31. Cache
Ở chương trước, ta nói về transaction và tính đúng.
Chương này nói về một thứ gần như hệ thống nào cũng đụng đến khi muốn nhanh hơn:
> Cache.
Nói đơn giản:
> Cache là bản nhớ tạm của dữ liệu để lần sau đọc nhanh hơn.
Ví dụ:
Thay vì mỗi lần khách mở trang chủ đều query database để lấy danh sách bánh bán chạy, hệ thống có thể lưu tạm kết quả đó trong cache.
Lần sau có người hỏi:
Cho tôi danh sách bánh bán chạy.
Hệ thống trả từ cache.
Nhanh hơn.
Ít tốn database hơn.
Nghe rất tuyệt.
Nhưng cache có một mặt trái:
> Cache có thể cũ.
Database đã đổi, nhưng cache vẫn giữ dữ liệu cũ.
Khi đó hệ thống nhanh hơn, nhưng có thể trả sai.
Thông điệp chính của chương:
> Cache là bản sao tạm để đọc nhanh, không phải nguồn sự thật. Dùng cache tốt giúp hệ thống nhanh hơn. Dùng cache sai làm dữ liệu khó hiểu và bug rất mệt.
---
31.1. Ví dụ quán bánh: bảng menu photo
Quán bánh có một cuốn sổ chính trong quầy:
Bánh nào đang bán.
Giá bao nhiêu.
Còn hàng không.
Đó là nguồn sự thật.
Nhưng để khách xem nhanh, quán in menu dán ngoài cửa.
Menu ngoài cửa giống cache.
Khách nhìn menu nhanh hơn việc hỏi nhân viên từng món.
Nhưng nếu hôm nay bánh dâu hết, nhân viên cập nhật sổ chính mà quên thay menu ngoài cửa, khách vẫn thấy bánh dâu còn bán.
Đó là cache stale.
Cache giúp nhanh.
Nhưng cache có thể cũ.
Vì vậy luôn phải biết:
Sổ chính là database.
Menu ngoài cửa là cache.
Đừng nhầm hai thứ.
---
31.2. Cache giải quyết vấn đề gì?
Cache thường giải quyết ba vấn đề:
Vấn đề 1: Đọc nhanh hơn
Dữ liệu nằm gần hơn hoặc đã được tính sẵn.
Ví dụ:
Product detail
User session
Homepage config
Top courses
Vấn đề 2: Giảm tải cho hệ thống phía sau
Nếu 10.000 request cùng hỏi một dữ liệu giống nhau, cache giúp database đỡ bị hỏi 10.000 lần.
Vấn đề 3: Tránh tính toán lặp lại
Một số kết quả tốn công tính.
Ví dụ:
Dashboard summary
Recommendation list
Pricing rules đã tính
Permission snapshot
Cache lưu kết quả để không tính lại quá thường xuyên.
---
31.3. Cache nằm ở đâu?
Cache có thể nằm ở nhiều tầng.
Browser cache
Trình duyệt lưu file tĩnh:
- CSS.
- JS.
- Image.
- Font.
CDN cache
CDN lưu dữ liệu gần người dùng:
- Ảnh sản phẩm.
- Video.
- File tĩnh.
- Trang public.
Application cache
Backend lưu tạm dữ liệu:
- Redis.
- Memcached.
- In-memory cache.
Database cache
Database tự cache page/index trong memory.
ORM/query cache
Một số framework cache query hoặc object.
Điểm chính:
> Cache không phải chỉ là Redis. Redis chỉ là một loại cache phổ biến ở tầng application.
---
31.4. Cache khác database thế nào?
Database là nơi lưu sự thật chính.
Cache là bản sao tạm.
Database ưu tiên:
- Tính đúng.
- Lưu bền.
- Query.
- Transaction.
- Constraint.
Cache ưu tiên:
- Nhanh.
- Gần.
- Giảm tải.
- Tạm thời.
Nếu cache mất, hệ thống nên có thể dựng lại từ database hoặc nguồn sự thật.
Nếu database mất hoặc sai, vấn đề nghiêm trọng hơn nhiều.
Một câu nhớ:
> Database là sổ cái. Cache là giấy ghi chú dán ngoài bàn.
Giấy ghi chú mất thì viết lại được.
Sổ cái sai thì đau.
---
31.5. Khi nào nên cache?
Nên cache khi:
- Dữ liệu được đọc nhiều.
- Dữ liệu ít thay đổi.
- Tính toán tốn kém.
- Query gây tải lớn.
- Chấp nhận dữ liệu cũ trong một khoảng thời gian.
- Có cách invalidate hoặc expire rõ.
Ví dụ:
Danh sách khóa học public.
Chi tiết sản phẩm.
Ảnh/video/file tĩnh.
Homepage config.
Top bài học phổ biến.
Permission snapshot ngắn hạn.
Kết quả dashboard không cần realtime tuyệt đối.
Cache rất hợp với dữ liệu đọc nhiều, đổi ít.
---
31.6. Khi nào không nên cache?
Không nên cache bừa khi:
- Dữ liệu thay đổi liên tục.
- Dữ liệu cực kỳ nhạy về tính đúng.
- Không biết invalidate thế nào.
- Query thật ra chưa tối ưu.
- Dữ liệu riêng tư dễ bị leak.
- Cache key khó thiết kế.
- Nguồn sự thật chưa rõ.
Ví dụ cần cẩn thận:
- Số dư ví.
- Quyền truy cập bảo mật.
- Trạng thái payment.
- Kết quả chấm chính thức nếu vừa cập nhật.
- Dữ liệu cá nhân.
- Giá cuối cùng tại checkout.
Không có nghĩa tuyệt đối không cache.
Nhưng nếu cache, phải biết dữ liệu cũ có thể gây hậu quả gì.
---
31.7. Cache làm dữ liệu sai thế nào?
Ví dụ:
Admin đổi giá bánh từ:
100.000 -> 120.000
Database đã cập nhật.
Nhưng cache vẫn giữ:
100.000
Khách thấy giá cũ.
Nếu checkout dùng giá cache cũ, hệ thống bán sai giá.
Hoặc:
User bị thu hồi quyền truy cập khóa học.
Database đã cập nhật.
Cache permission vẫn nói:
user có quyền
User vẫn xem được nội dung.
Đây là lỗi nghiêm trọng hơn.
Cache stale không phải lúc nào cũng ngang nhau.
Với menu public, stale vài phút có thể ổn.
Với quyền truy cập hoặc tiền, stale vài phút có thể không ổn.
---
31.8. TTL là gì?
TTL là Time To Live.
Nghĩa là cache sống trong bao lâu trước khi tự hết hạn.
Ví dụ:
cache product detail 5 phút
cache homepage 60 giây
cache user permission 30 giây
cache static asset 1 năm
TTL là cách đơn giản để tránh cache cũ mãi.
Nếu không invalidate được chính xác, TTL ngắn là cách giảm rủi ro.
Nhưng TTL cũng là trade-off:
TTL dài:
cache hiệu quả hơn, nhưng stale lâu hơn.
TTL ngắn:
stale ít hơn, nhưng cache hit thấp hơn.
Không có TTL đúng cho mọi thứ.
Phải dựa vào dữ liệu và hậu quả stale.
---
31.9. Cache invalidation là gì?
Cache invalidation là xóa hoặc làm mới cache khi dữ liệu gốc thay đổi.
Ví dụ:
Admin update product_123
-> xóa cache product_123
Lần sau user đọc product_123, hệ thống query database và cache lại dữ liệu mới.
Nghe đơn giản, nhưng thực tế khó vì:
- Một dữ liệu có thể nằm trong nhiều cache.
- Cache key có thể phức tạp.
- Có cache ở browser/CDN/app.
- Event invalidate có thể mất.
- Dữ liệu liên quan nhiều nơi.
Ví dụ đổi giá product:
Cần xóa:
- Product detail cache.
- Category list cache.
- Search result cache.
- Homepage "best sellers" cache nếu có.
- CDN page cache nếu render sẵn.
Đây là lý do người ta hay nói cache invalidation khó.
---
31.10. Cache-aside là gì?
Cache-aside là pattern phổ biến nhất.
Luồng đọc:
App hỏi cache.
Nếu có -> trả cache.
Nếu không có -> query database.
Lưu kết quả vào cache.
Trả response.
Luồng ghi:
Update database.
Xóa cache liên quan.
Ví dụ:
GET /products/123
-> Redis có product:123?
-> không có
-> query database
-> set Redis product:123 TTL 5 phút
-> trả response
Cache-aside dễ hiểu, dễ bắt đầu.
Nhưng cần làm tốt invalidation khi dữ liệu thay đổi.
---
31.11. Write-through và write-behind
Ngoài cache-aside, có vài pattern khác.
Write-through
Khi ghi dữ liệu, ghi vào cache và database cùng lúc hoặc qua cache layer.
Ưu điểm:
- Cache thường mới.
Nhược điểm:
- Ghi phức tạp hơn.
- Cache layer quan trọng hơn.
Write-behind
Ghi vào cache trước, database cập nhật sau.
Ưu điểm:
- Ghi nhanh.
Nhược điểm:
- Rủi ro mất dữ liệu nếu cache chết.
- Khó giữ tính đúng.
Với hệ thống nghiệp vụ thông thường, cache-aside thường dễ hiểu và an toàn hơn để bắt đầu.
Write-behind chỉ nên dùng khi hiểu rất rõ rủi ro.
---
31.12. Cache key là gì?
Cache key là tên dùng để lưu và lấy dữ liệu trong cache.
Ví dụ:
product:123
user:456:permissions
course:789:lessons
grading_job:job_123:status
Key tốt phải:
- Rõ nghĩa.
- Có namespace.
- Chứa đủ tham số ảnh hưởng kết quả.
- Không chứa dữ liệu nhạy cảm nếu không cần.
Ví dụ nguy hiểm:
dashboard
Nếu dashboard phụ thuộc user, tenant, filter, time range, key này quá chung.
Có thể user này thấy dữ liệu user khác.
Key đúng hơn:
dashboard:tenant_1:user_5:from_2026-05-01:to_2026-05-31
Cache key sai có thể tạo bug bảo mật rất nặng.
---
31.13. Cache dữ liệu riêng tư
Cache dữ liệu riêng tư rất cần cẩn thận.
Ví dụ:
GET /users/me
Không được cache public theo URL chung nếu URL giống nhau cho mọi user.
Nếu cache sai:
User A nhận profile User B.
Thảm họa.
Với dữ liệu riêng tư:
- Key phải chứa user/tenant.
- Không cache ở CDN public nếu không hiểu rõ.
- Header cache-control phải đúng.
- Không log dữ liệu nhạy cảm.
- TTL ngắn nếu rủi ro.
Cache bug về privacy thường rất nghiêm trọng.
---
31.14. Cache public data
Dữ liệu public dễ cache hơn.
Ví dụ:
- Ảnh sản phẩm.
- CSS/JS.
- Trang blog public.
- Danh mục khóa học public.
- Landing page.
Với public data, có thể dùng:
- Browser cache.
- CDN cache.
- Reverse proxy cache.
- App cache.
TTL có thể dài hơn nếu dữ liệu ít thay đổi.
Với file tĩnh có version/hash trong tên:
app.abc123.js
có thể cache rất lâu.
Khi file đổi, tên file đổi.
Đây là cách cache rất hiệu quả.
---
31.15. CDN cache
CDN cache dữ liệu ở gần người dùng.
Ví dụ user ở Việt Nam tải ảnh sản phẩm.
Nếu ảnh được cache ở edge gần Việt Nam, tải nhanh hơn nhiều so với server gốc ở xa.
CDN rất hợp cho:
- Image.
- Video.
- CSS/JS.
- File download.
- Public pages.
CDN không phù hợp nếu:
- Response cá nhân hóa mà không cấu hình đúng.
- Dữ liệu nhạy cảm.
- Nội dung cần đổi ngay lập tức nhưng không có purge/invalidation.
CDN sẽ được nhắc lại ở chương hạ tầng, nhưng về bản chất nó cũng là cache.
---
31.16. Redis dùng để cache gì?
Redis thường dùng cho application cache.
Ví dụ:
- Session.
- Rate limit counter.
- Short-lived computed result.
- User permission snapshot.
- Product detail cache.
- Job status cache nếu có source of truth ở DB.
- Distributed lock trong một số trường hợp.
Redis rất nhanh vì dữ liệu ở memory.
Nhưng Redis không nên bị xem như database chính nếu bạn chưa thiết kế durability, backup, persistence, recovery rõ.
Với cache, Redis mất dữ liệu thì hệ thống nên tự phục hồi bằng cách đọc lại từ database.
Nếu Redis mất mà hệ thống mất sự thật, có thể bạn đang dùng cache như database.
---
31.17. Cache stampede là gì?
Cache stampede xảy ra khi cache hết hạn và rất nhiều request cùng lúc đổ về database để tính lại cache.
Ví dụ:
Homepage cache hết hạn lúc 10:00.
10.000 request đến.
Tất cả thấy cache miss.
Tất cả query database.
Database bị dồn tải.
Cách giảm:
- Lock khi rebuild cache.
- Stale-while-revalidate.
- Random TTL jitter.
- Pre-warm cache.
- Background refresh.
Cache stampede là lỗi rất thực tế ở hệ thống traffic cao.
---
31.18. Stale-while-revalidate
Stale-while-revalidate nghĩa là:
> Nếu cache hơi cũ, vẫn trả bản cũ ngay, rồi âm thầm cập nhật cache ở nền.
Ví dụ:
Homepage cache hết hạn.
Request đến.
Hệ thống trả bản cũ thêm một chút để user không chờ.
Một worker/background task cập nhật cache mới.
Pattern này hợp với dữ liệu public hoặc dữ liệu chấp nhận stale ngắn.
Không hợp với dữ liệu cần đúng ngay như payment, permission nhạy, số dư ví.
---
31.19. Negative caching
Negative caching là cache cả kết quả "không có".
Ví dụ:
User hỏi product id không tồn tại:
product:999 not found
Nếu rất nhiều request hỏi product 999, cache kết quả not found trong 30 giây giúp giảm tải database.
Nhưng cần TTL ngắn.
Vì product có thể được tạo sau đó.
Negative caching hữu ích cho:
- Not found.
- Permission denied tạm thời.
- External API không có dữ liệu.
Nhưng phải cẩn thận để không giữ "không có" quá lâu khi dữ liệu mới xuất hiện.
---
31.20. Cache warming
Cache warming là làm nóng cache trước khi traffic đến.
Ví dụ:
Sau deploy hoặc sau khi xóa cache, hệ thống chủ động load:
- Homepage.
- Top products.
- Popular courses.
- Config.
Nếu không warm cache, request đầu tiên của user phải tự gánh việc tính cache.
Cache warming hữu ích khi:
- Dữ liệu public phổ biến.
- Tính toán đắt.
- Sau deploy cache trống.
- Traffic lớn ngay từ đầu.
Không phải hệ thống nào cũng cần, nhưng đáng biết.
---
31.21. Cache và consistency
Cache luôn đặt ra câu hỏi consistency:
> Cache và database có khớp nhau không?
Có vài mức:
Strong consistency
Đọc phải thấy dữ liệu mới ngay.
Cache khó dùng hơn.
Eventual consistency
Cache có thể cũ một lúc rồi sẽ được cập nhật.
Phù hợp với nhiều dữ liệu đọc.
Best-effort
Cache có thể mất hoặc cũ, không quá nghiêm trọng.
Phù hợp với analytics, recommendation, view count.
Trước khi cache, hãy hỏi:
> Dữ liệu này được phép cũ bao lâu?
Nếu không trả lời được, đừng cache vội.
---
31.22. Cache và source of truth
Cache không nên là source of truth.
Ví dụ:
grading_job status thật nằm ở database.
Redis cache có thể chứa bản copy để đọc nhanh.
Nếu Redis và database khác nhau, phải biết tin ai.
Thông thường:
Tin database.
Xóa/rebuild cache.
Nếu thiết kế buộc phải tin cache, cache đã trở thành database.
Khi đó cần thiết kế như database:
- Persistence.
- Backup.
- Replication.
- Recovery.
- Consistency.
Đừng vô tình biến cache thành nguồn sự thật mà không nhận ra.
---
31.23. Cache và AI Judge
Trong AI Judge, cache có thể dùng cho:
- Rubric public hoặc rubric ít thay đổi.
- Assignment config.
- User permission snapshot ngắn.
- Grading job status đọc nhiều.
- Result summary nếu nhiều người xem.
- Model pricing/config.
Nhưng cần cẩn thận với:
- Kết quả chấm chính thức.
- Quyền xem bài/điểm.
- Quota còn lại.
- Trạng thái payment/subscription.
Ví dụ:
Frontend polling:
GET /grading-jobs/{id}
Nếu quá nhiều request polling, có thể cache status rất ngắn:
1-3 giây
Nhưng khi job completed, nên invalidate hoặc cập nhật cache.
Và database vẫn là source of truth.
---
31.24. Cache và permission
Permission cache rất phổ biến nhưng nhạy.
Ví dụ:
user:123:permissions
Cache giúp không query quyền quá nhiều.
Nhưng nếu admin thu hồi quyền, cache cũ có thể cho user tiếp tục truy cập.
Cách giảm rủi ro:
- TTL ngắn.
- Invalidate khi role/permission đổi.
- Permission version.
- Recheck ở hành động nhạy cảm.
- Không cache quá lâu với quyền quan trọng.
Với bảo mật, cache stale có thể là bug nghiêm trọng.
Đừng cache quyền như cache ảnh sản phẩm.
---
31.25. Cache và rate limit
Rate limiter thường dùng cache/memory store như Redis để đếm request.
Ví dụ:
rate:user_123:submissions
Mỗi request tăng counter.
Counter có TTL 1 phút hoặc 1 ngày.
Đây là dùng Redis như store tạm có TTL.
Nếu Redis mất, rate limit có thể reset.
Tùy nghiệp vụ, có thể chấp nhận hoặc cần cơ chế bền hơn.
Rate limit nghiêm ngặt về tiền/quota có thể cần lưu database song song.
---
31.26. Cache và database load
Cache thường được thêm khi database chịu tải đọc cao.
Nhưng trước khi cache, hãy kiểm tra:
- Query có index chưa?
- Có N+1 không?
- Có pagination không?
- Có lấy quá nhiều cột không?
- Có đọc quá nhiều lần cùng dữ liệu không?
Cache không nên là miếng băng dán lên query quá tệ.
Tối ưu query trước, cache sau.
Nếu query đã hợp lý mà vẫn quá tải do đọc nhiều, cache rất đáng dùng.
---
31.27. Cache invalidation bằng event
Khi dữ liệu thay đổi, có thể phát event để xóa cache.
Ví dụ:
ProductUpdated
-> invalidate product:123
-> invalidate category products
Hoặc:
UserRoleChanged
-> invalidate user:123:permissions
Event-based invalidation hữu ích khi nhiều service/module có cache.
Nhưng cần nhớ:
- Event có thể trễ.
- Event có thể xử lý lỗi.
- Consumer phải idempotent.
- TTL vẫn nên có như lớp an toàn.
Không nên chỉ dựa vào event nếu event mất là cache sai mãi.
---
31.28. Cache observability
Cache cũng cần theo dõi.
Cần biết:
- Hit rate.
- Miss rate.
- Latency.
- Eviction count.
- Memory usage.
- Key count.
- Error rate.
- Stale data incidents nếu đo được.
Hit rate cao không phải lúc nào cũng tốt.
Nếu cache hit cao nhưng dữ liệu stale sai, đó là vấn đề.
Miss rate cao có thể nghĩa là:
- TTL quá ngắn.
- Key quá phân mảnh.
- Cache không hiệu quả.
- Dữ liệu không phù hợp để cache.
Cache không quan sát được thì rất khó debug.
---
31.29. Những lỗi phổ biến
Lỗi 1: Cache dữ liệu chưa tối ưu query
Query chậm do thiếu index/N+1 nhưng lại thêm cache trước.
Lỗi 2: Không có TTL
Cache cũ sống mãi.
Lỗi 3: Cache key quá chung
User thấy dữ liệu của user khác.
Lỗi 4: Nhầm cache với source of truth
Redis mất là mất dữ liệu thật.
Lỗi 5: Cache permission quá lâu
User bị thu hồi quyền nhưng vẫn truy cập được.
Lỗi 6: Không invalidate đủ nơi
Product đổi nhưng list/search/homepage vẫn cũ.
Lỗi 7: Cache stampede
Cache hết hạn, mọi request cùng đổ về database.
Lỗi 8: Không theo dõi hit/miss/stale
Không biết cache có giúp hay gây hại.
---
31.30. Checklist trước khi cache
Trước khi cache một dữ liệu, hãy hỏi:
- Source of truth nằm ở đâu?
- Dữ liệu này đọc nhiều không?
- Dữ liệu đổi thường xuyên không?
- Dữ liệu được phép cũ bao lâu?
- Nếu cache sai/cũ thì hậu quả gì?
- Cache key gồm những gì?
- Có user/tenant/filter trong key không?
- TTL bao lâu?
- Khi dữ liệu gốc đổi, invalidate thế nào?
- Có cần event invalidation không?
- Có nguy cơ cache stampede không?
- Có dữ liệu nhạy cảm không?
- Có metrics hit/miss không?
- Nếu cache mất, hệ thống có rebuild được không?
Nếu chưa trả lời được, đừng thêm cache chỉ vì "cho nhanh".
---
31.31. Bảng chọn nhanh
| Tình huống | Cache thế nào? | |---|---| | Ảnh/CSS/JS public | Browser/CDN cache dài | | Product/course public ít đổi | App/CDN cache TTL vừa | | Permission user | Cache ngắn, invalidate khi đổi quyền | | Số dư ví | Rất cẩn thận, thường đọc source of truth | | Grading job status polling nhiều | Cache rất ngắn hoặc optimize query | | Dashboard summary | Cache TTL theo mức realtime cần | | Search result | Search index/read model, không phải cache đơn giản | | Rate limit counter | Redis TTL counter | | Payment status | Cẩn thận, source of truth rõ | | Homepage popular list | Cache tốt, có refresh/invalidate |
---
31.32. Tóm tắt bằng AI Judge
AI Judge có thể dùng cache để giảm tải:
Rubric config ít đổi.
Assignment settings.
Model pricing/config.
Job status trong vài giây khi polling nhiều.
User permission snapshot ngắn.
Nhưng không nên để cache trở thành sự thật chính cho:
Kết quả chấm cuối cùng.
Quota tính tiền.
Payment/subscription.
Quyền truy cập nhạy cảm.
Thiết kế tốt:
Database giữ sự thật.
Cache giúp đọc nhanh.
TTL giới hạn stale.
Event/update xóa cache khi dữ liệu đổi.
Frontend vẫn có thể fetch lại state thật.
---
31.33. Kết luận của chương
Cache là công cụ giúp hệ thống nhanh hơn bằng cách nhớ tạm.
Nó có thể giảm latency, giảm tải database, và tránh tính toán lặp lại.
Nhưng cache cũng làm hệ thống phức tạp hơn:
- Dữ liệu có thể cũ.
- Invalidation khó.
- Key sai có thể leak dữ liệu.
- Cache stampede có thể làm sập database.
- Permission/payment/quota cache sai có thể gây lỗi nghiêm trọng.
Thông điệp quan trọng nhất:
> Cache không phải nguồn sự thật. Cache chỉ là bản nhớ tạm. Trước khi cache, hãy biết dữ liệu được phép cũ bao lâu và nếu nó cũ thì hậu quả là gì.
Ở chương tiếp theo, ta sẽ nói về search: khi nào database search là đủ, khi nào cần search engine như Elasticsearch/OpenSearch, và vì sao search index cũng không phải source of truth.