Khi nào nên dùng cache (và khi nào không)

Khi nào nên dùng cache (và khi nào không)

Table of Contents

Có một bug production mà đến giờ mình vẫn nhớ như in: user A hủy đơn hàng, nhưng app vẫn hiển thị “đang giao” trong 10 phút tiếp theo. Support nhận 30 ticket trong 1 buổi sáng. Nguyên nhân: cache TTL 10 phút, nhưng không ai invalidate khi order status thay đổi.

Đó là lần đầu mình hiểu: cache không phải lúc nào cũng là bạn. Dùng sai, nó là kẻ thù số 1 của data consistency.

Bài này không nói về LRU, LFU hay eviction policy. Mình chỉ tập trung vào câu hỏi quan trọng nhất mà ít người dành thời gian nghĩ: khi nào nên cache, khi nào không, và làm sao để không hối hận 1 tháng sau.

![Cache Strategy(https://blog-bucket.luandnh.com/images/covers/cache.jpg)

Khi nào nên dùng cache

1. Đọc nhiều, ghi ít - “món nấu sẵn”

Profile user, danh mục sản phẩm, cấu hình hệ thống - ít thay đổi nhưng truy xuất liên tục. Đây là loại dữ liệu lý tưởng nhất cho cache.

// Pattern: cache-aside
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
    // Check cache trước
    if data, err := s.cache.Get(ctx, "user:"+id); err == nil {
        return decodeUser(data), nil
    }

    // Cache miss → query DB
    user, err := s.db.GetUser(ctx, id)
    if err != nil {
        return nil, err
    }

    // Set cache cho lần sau
    s.cache.Set(ctx, "user:"+id, encodeUser(user), 10*time.Minute)
    return user, nil
}

Cache-aside là pattern đơn giản nhất và mình dùng nhiều nhất. Ưu điểm: code dễ hiểu, không có magic. Nhược điểm: cache miss đầu tiên vẫn chậm.

2. Điểm nóng - 20% dữ liệu nhận 80% request

Flash sale, bài viết viral, merchant nổi bật. Khi 80% traffic chỉ hit 20% data, cache lập tức giảm tải DB đáng kể. Đây là quick win lớn nhất về ROI trong performance optimization.

// Pattern: cache top-N hot items
func (s *Service) GetHotMerchants(ctx context.Context) ([Merchant, error) {
    if data, err := s.cache.Get(ctx, "merchants:hot"); err == nil {
        return decodeMerchants(data), nil
    }

    merchants, err := s.db.TopByOrders(ctx, 50) // top 50 merchants
    if err != nil {
        return nil, err
    }

    s.cache.Set(ctx, "merchants:hot", encodeMerchants(merchants), 5*time.Minute)
    return merchants, nil
}

3. Chấp nhận stale data trong vài giây

View count, like count, ranking - lệch 2-3 giây thì user không nhận ra. Nhưng nếu COUNT(*) mỗi lần refresh, DB sẽ khóc.

4. Query đắt - aggregation, report, dashboard

Những câu query chạy 5-10 giây nên cache kết quả và refresh định kỳ:

func (s *Service) GetDashboard(ctx context.Context) (*Dashboard, error) {
    key := "dashboard:daily"
    if data, err := s.cache.Get(ctx, key); err == nil {
        return decodeDashboard(data), nil
    }

    dash, err := s.computeDashboard(ctx) // query nặng 8 giây
    if err != nil {
        return nil, err
    }

    s.cache.Set(ctx, key, encodeDashboard(dash), 15*time.Minute)
    return dash, nil
}

Khi nào KHÔNG nên cache

1. Dữ liệu biến động liên tục - “cache là thảm họa”

Số dư tài khoản, inventory real-time, trạng thái đơn hàng. Cache ở đây → user thấy số dư sai, mua hàng hết stock. Đọc trực tiếp từ DB hoặc dùng event-driven update.

2. Cardinality quá cao - key space bùng nổ

// ❌ Sai: cache key quá chi tiết → triệu key, hit rate gần 0
cacheKey := fmt.Sprintf("products:c=%s&p_min=%d&p_max=%d&sort=%s&page=%d",
    category, minPrice, maxPrice, sortBy, page)
// ✅ Đúng: cache ở mức cao hơn, filter/paginate ở app layer
cacheKey := "products:popular" // cache top 200, filter ở memory

Số key càng nhiều, hit rate càng thấp, cache thành overhead thay vì optimization.

3. Upstream đã nhanh - đừng optimize thứ không cần

Query DB trả về 2ms với index đúng. Thêm cache chỉ tạo complexity và thêm một điểm fail. Mình từng thấy team cache response của endpoint đã nhanh sẵn - latency tăng từ 2ms lên 3ms vì Redis network hop.

4. Không có chiến lược invalidation

Nếu bạn không biết khi nàocách nào để xóa cache khi data thay đổi - đừng cache. Dữ liệu cũ hiển thị cho user còn tệ hơn không cache.

Chiến lược invalidation

StrategyKhi dùngCảnh báo
TTLData ít thay đổi, chấp nhận stale vài phútStale data trong window TTL
Write-throughCần sync ngay, write ítWrite latency tăng
Event-drivenData consistency quan trọng nhấtPhức tạp nhất để implement đúng

Code write-through pattern mình dùng:

func (s *Service) UpdateProduct(ctx context.Context, p *Product) error {
    if err := s.db.Update(ctx, p); err != nil {
        return err
    }

    // Invalidate các cache key liên quan
    s.cache.Delete(ctx, "product:"+p.ID)
    s.cache.Delete(ctx, "products:popular")
    s.cache.Delete(ctx, "products:category:"+p.CategoryID)
    return nil
}

Đừng quên invalidate tất cả cache key chứa dữ liệu cũ. Mình từng xóa product:123 nhưng quên products:popular → list page vẫn hiển thị giá cũ. Bug này tốn 4 tiếng.

Quy tắc ra quyết định

Tiêu chí✅ Cache❌ Không cache
Read/Write ratioĐọc nhiều hơn ghi 10x+Ghi nhiều hơn đọc
Data freshnessChấp nhận stale OKCần real-time
Complexity benefitLợi ích > chi phí maintainDB đủ nhanh rồi
Invalidation planCó chiến lược rõ ràng“Để đó tính sau”

“Cache giống như bình giữ nhiệt - siêu hữu ích cho món bán chạy, nhưng để lâu quá thì cà phê cũng chua.”

Bottom line

Cache là con dao 2 lưỡi. Dùng đúng: p99 latency giảm 10x. Dùng sai: data inconsistency, bug khó debug nhất đời. Trước khi cache bất cứ thứ gì, hỏi 3 câu: (1) Data này thay đổi bao lâu một lần? (2) Tôi có plan invalidate chưa? (3) Lợi ích thực sự là bao nhiêu ms?

Nếu không trả lời được cả 3 - giữ nguyên, query DB, ngủ ngon hơn.

Bạn đã từng gặp bug “cache hiển thị dữ liệu cũ” kiểu gì? Có pattern invalidate nào hay mà mình chưa thử không? Kể mình nghe war story về cache của bạn đi - mấy bài học từ bug cache bao giờ cũng đắt giá nhất. 🍵

Share :

Related Posts

API Filtering: gọi dữ liệu như dân sành cà phê

API Filtering: gọi dữ liệu như dân sành cà phê

Năm đầu đi làm, mình từng viết một endpoint GET /api/menus trả về… toàn bộ menu. 200 items mỗi lần gọi. JSON nặng 1.2MB. Frontend chỉ cần tên + giá của 10 món đang active. Mình nhớ câu đầu tiên anh lead nói: “Mày đang gửi cả cái kho hàng cho người ta chỉ cần xem menu đấy à?”

Read More
Context trong Go: truyền dữ liệu, deadline, cancel - dùng đúng hay chết hiệu năng

Context trong Go: truyền dữ liệu, deadline, cancel - dùng đúng hay chết hiệu năng

Hồi mới code Go, mình có một bug production kỳ lạ: cứ sau 3-4 ngày chạy, service tự dưng ngốn 8GB RAM rồi OOM. Mò suốt chiều thứ Sáu mới phát hiện: một cái goroutine không bao giờ kết thúc vì mình quên truyền context xuống gRPC call. Mỗi request leak một goroutine, sau 100K request thì server “đội mồ” luôn.

Read More