---
id: case-cache
title: "Cache và Redis gặp sự cố"
shortTitle: "Cache & Redis"
track: cases
trackTitle: "Tình huống vận hành"
order: 10
level: core
status: published
estimatedMinutes: 40
prerequisites:
  - caching-redis
  - case-distributed
next: case-rabbitmq
reviewedAgainst: 2026-10-12
sources:
  - title: "Redis key eviction"
    url: https://redis.io/docs/latest/develop/reference/eviction/
  - title: "Redis pipelining"
    url: https://redis.io/docs/latest/develop/using-commands/pipelining/
  - title: "Caching guidance"
    url: https://learn.microsoft.com/en-us/azure/architecture/best-practices/caching
---

# Cache và Redis gặp sự cố

Cache có nhiệm vụ làm app nhanh hơn, chứ không phải là điều kiện để app sống. Nếu Redis chết mà cả API chết theo, thì hoặc team đang dùng cache như database, hoặc code chưa có fallback.

## Để làm gì

Bạn sẽ xử lý cache miss, Redis timeout, cache stampede, hot key và stale data mà không dồn toàn bộ tải xuống database chỉ trong một giây.

## Gồm những gì

- Cache-aside, TTL có jitter, negative cache.
- Stampede, hot key và cache penetration.
- Redis đầy RAM: policy `noeviction` trả lỗi cho lệnh ghi chứ không tự xóa key.

Bài không nói về Redis Cluster mà tập trung vào cách app xử lý.

## Dùng khi nào

API đang cache product, session hoặc kết quả query. Redis latency tăng, hit rate giảm, hoặc database đột nhiên quá tải ngay sau một lần deploy.

Đừng cache dữ liệu không tái tạo được nếu chưa chủ động bật persistence. Nếu mất Redis là mất luôn đơn hàng, thì thứ đó không còn là cache nữa.

## Kiến thức cốt lõi

Luồng cache-aside:

1. GET từ cache.
2. Hit thì trả về luôn.
3. Miss thì đọc từ database.
4. Ghi vào cache kèm TTL.
5. Trả dữ liệu.

Timeout gọi Redis phải ngắn. Nếu database trả lời trong 200 ms mà app chờ Redis tới 2 giây, cache đang làm request chậm hơn cả nguồn thật. Khi Redis lỗi, app bỏ qua cache và đọc thẳng từ nguồn, nhưng phải giới hạn concurrency. Nếu mọi instance cùng fallback xuống database mà không có giới hạn, database sẽ chết ngay sau Redis.

Khi điều tra, hãy đọc các con số theo thứ tự này: latency và tỉ lệ lỗi của Redis trước, hit rate sau, rồi mới tới tải của database. Thứ tự đó cho biết sự cố bắt đầu ở đâu. Database quá tải chỉ là hệ quả nếu Redis đã chậm hoặc hit rate đã rơi trước đó.

Stampede xảy ra khi một key nóng hết TTL, và hàng nghìn request cùng lúc query database để lấy lại dữ liệu. Các cách xử lý:

- Cộng jitter vào TTL để các key không hết hạn cùng một giây.
- Single-flight hoặc distributed lock: chỉ một request đi refresh, các request khác dùng bản stale hoặc đợi một chút.
- Stale-while-revalidate: trả bản cũ trong cửa sổ cho phép, đồng thời refresh ở nền.

Cache penetration là khi request hỏi những key không tồn tại, luôn miss, và lần nào cũng đập xuống database. Cách xử lý là negative cache: cache luôn giá trị "không có" với TTL ngắn. Đừng cache 404 tới một giờ nếu object đó có thể vừa mới được tạo.

Hot key là một key nhận phần lớn traffic, khiến một shard hay một Redis node phải gánh hết. Cache thêm vài giây ở local của từng app instance có thể giảm tải, đổi lại dữ liệu sẽ stale hơn.

Về invalidation: hãy update database trước, rồi mới xóa cache. Nếu xóa cache trước, một request chen vào giữa sẽ đọc dữ liệu cũ từ database và ghi ngược lại vào cache. Với transaction phức tạp, hãy dùng outbox hoặc đưa version vào key, thay vì cố đồng bộ hai hệ bằng hai call không atomic.

## Lệnh và code mẫu

Pseudo-code Node.js:

```js
async function getProduct(id) {
  const key = `product:${id}`;
  try {
    const cached = await withTimeout(redis.get(key), 50);
    if (cached !== null) return JSON.parse(cached);
  } catch (error) {
    metrics.increment("cache_error");
  }

  const product = await db.product.findById(id);
  const ttl = 300 + Math.floor(Math.random() * 60);
  redis.set(key, JSON.stringify(product), { EX: ttl }).catch(() => {
    metrics.increment("cache_write_error");
  });
  return product;
}
```

Đây mới là fallback cơ bản, chưa chống được stampede. Trên production, bạn cần thêm single-flight theo key và giới hạn số query database chạy đồng thời.

Quan sát Redis trong lab:

```bash
docker exec lab-redis redis-cli INFO stats
docker exec lab-redis redis-cli INFO memory
docker exec lab-redis redis-cli CONFIG GET maxmemory-policy
```

Không chạy `KEYS *` trên production. Lệnh này quét toàn bộ keyspace và chặn event loop của Redis. Hãy dùng `SCAN`.

## Thực hành

1. Viết ra hành vi mong muốn khi Redis timeout: metric tăng, app đọc database, response vẫn đúng.
2. Cho 1.000 request cùng miss key `product:1`. Thêm single-flight để chỉ một query đi xuống database.
3. Đặt TTL trong khoảng 300–359 giây, không để mọi key đều đúng 300.
4. Mô tả kết quả khi policy là `noeviction` và RAM đầy: lệnh ghi cache bị lỗi, còn GET các key cũ vẫn chạy.

## Kết quả mong đợi

Redis chết không làm API trả dữ liệu sai. Database không phải nhận fallback vô hạn. TTL không hết hạn đồng loạt.

## Checklist

- [ ] Timeout Redis ngắn hơn timeout database.
- [ ] Fallback có giới hạn concurrency.
- [ ] TTL có jitter; key nóng có single-flight.
- [ ] Không dùng `KEYS *` trên production.
- [ ] Cache không phải bản duy nhất của dữ liệu.

## Lỗi thường gặp

**Redis vừa hồi lại, app cùng lúc warm toàn bộ cache.** Database và Redis lại quá tải lần nữa. Hãy warm theo batch, có rate limit, và ưu tiên những key thật sự nóng.

**Xóa cache trước khi update database.** Request chen vào giữa sẽ ghi lại dữ liệu cũ. Hãy update nguồn trước rồi mới invalidate; nếu yêu cầu chặt hơn, dùng version hoặc event.

**Session chỉ nằm trong Redis, không có persistence, không có fallback.** Redis restart là toàn bộ user bị đăng xuất. Điều đó có thể chấp nhận được, nhưng phải là một quyết định đã được ghi lại, không phải một bất ngờ.

## Nguồn

1. [Key eviction](https://redis.io/docs/latest/develop/reference/eviction/) — `noeviction` và các policy.
2. [Redis pipelining](https://redis.io/docs/latest/develop/using-commands/pipelining/) — giảm round trip, không thay transaction.
3. [Caching guidance](https://learn.microsoft.com/en-us/azure/architecture/best-practices/caching) — cache-aside, expiration và dữ liệu stale.

## Tiếp theo

[RabbitMQ: queue, retry và DLQ](./11-rabbitmq.md)
