# 🗑️ TTL và invalidation: chọn thời điểm xóa

By Better Wise

Bài trước bạn đã dựng xong luồng cache-aside: đọc cache trước, miss thì đọc database rồi ghi lại vào cache, còn khi ghi thì cập nhật database rồi xóa key. Nhưng luồng đó chỉ đúng khi bạn trả lời được một câu hỏi: dữ liệu trong cache được phép cũ bao lâu? Bài này đưa ra ba công cụ để trả lời câu đó, và tiêu chí để chọn công cụ nào cho loại dữ liệu nào.

Sau bài này bạn có thể:

- Đặt TTL dựa trên mức dữ liệu cũ mà người dùng còn chấp nhận được
- Phân biệt ba cách xử lý: hết hạn tự nhiên, xóa chủ động khi ghi, và version key
- Chọn chiến lược phù hợp cho từng loại dữ liệu cụ thể
- Nhận ra lỗi thứ tự khi invalidate, kiểu lỗi làm dữ liệu cũ sống lâu hơn dự kiến

## TTL: chiếc đồng hồ hẹn giờ gắn trên tủ lạnh

Hãy tưởng tượng cache là tủ lạnh, mỗi key là một hộp sữa có in hạn sử dụng. TTL (time to live) chính là hạn sử dụng đó. Hết hạn thì hộp sữa tự động bị bỏ đi, không ai phải nhớ.

Trong Redis, TTL được đặt ngay lúc ghi:

```
SET product:42 '{"price":199000}' EX 60
```

Con số 60 nghĩa là key này sống tối đa 60 giây. Sau đó `GET product:42` trả về nil, và lần đọc tiếp theo sẽ đi xuống database.

Điểm quan trọng nhất của cả bài: **TTL không làm dữ liệu đúng, nó chỉ chặn mức độ sai**. TTL 60 giây không đảm bảo cache khớp database, nó chỉ đảm bảo cache không lệch quá 60 giây.

## Chọn TTL từ câu hỏi "cũ bao lâu thì chấp nhận được"

Đừng bắt đầu từ một con số đẹp như 300 hay 3600. Bắt đầu từ câu hỏi nghiệp vụ: nếu người dùng thấy dữ liệu cũ, họ mất gì?

| Loại dữ liệu | Cũ bao lâu thì chấp nhận được | Chiến lược |
| --- | --- | --- |
| Giá, tồn kho | gần như không, sai giá là mất tiền | TTL 30 đến 60 giây, cộng xóa khi ghi |
| Profile người dùng | vài phút | TTL 5 đến 15 phút, cộng xóa khi user sửa |
| Danh mục, listing | vài phút đến vài giờ | Version key, TTL 10 đến 30 phút |
| Cấu hình, feature flag | 30 giây | TTL 10 đến 30 giây, không cần xóa |
| Số dư ví, quyền truy cập | không chấp nhận | Không cache |

Quy tắc: TTL phải nhỏ hơn hoặc bằng khoảng thời gian cũ tối đa mà bạn chấp nhận. Nếu không trả lời được câu hỏi đó, bạn chưa nên đặt TTL.

Ví dụ code đọc có TTL:

```python
import json, redis
r = redis.Redis()

def get_product(pid):
    key = f"product:{pid}"
    raw = r.get(key)
    if raw:
        return json.loads(raw)
    data = db.load_product(pid)
    r.set(key, json.dumps(data), ex=60)
    return data
```

Không có gì mới so với bài trước, chỉ thêm tham số `ex=60`. Nhưng 60 giây này là một quyết định thiết kế, không phải con số tùy tiện.

## Xóa chủ động khi ghi: khi TTL ngắn vẫn chưa đủ

Người dùng vừa đổi giá, chờ 60 giây là quá lâu. Vậy sau khi ghi database, ta xóa luôn key cache. Đây là invalidation theo sự kiện.

```python
def update_price(pid, new_price):
    db.update_price(pid, new_price)   # 1. ghi database, commit
    r.delete(f"product:{pid}")        # 2. xóa cache sau khi commit
```

Thứ tự này không được đảo. Nếu xóa cache trước rồi mới commit, một request khác có thể chen vào giữa, đọc database chưa commit (vẫn là giá cũ) và ghi lại giá cũ vào cache. Kết quả: cache vừa xóa lại chứa dữ liệu cũ.

```mermaid
flowchart TD
    A["✍️ Client ghi dữ liệu"] --> B["💾 UPDATE database"]
    B --> C{"Commit thành công?"}
    C -- Không --> D["↩️ Trả lỗi, cache giữ nguyên"]
    C -- Có --> E["🗑️ DELETE cache key"]
    E --> F["👀 Request sau miss, đọc DB mới"]
```

> **Warning:** 
> Lệnh xóa cache có thể thất bại: timeout, mất kết nối, hoặc lập trình viên quên gọi ở một nhánh code. Vì vậy luôn đặt TTL song song với invalidation. TTL là lưới an toàn, invalidation là cách giữ dữ liệu tươi.

## Version key: invalidate hàng loạt mà không quét key

Vấn đề thực tế: một category có 10.000 key listing (theo page, theo filter). Xóa từng key rất chậm và dễ sót. Version key giải quyết bằng cách đưa số version vào chính tên key.

```python
def cat_key(cid, page):
    v = r.get(f"catver:{cid}") or "0"
    return f"cat:{cid}:v{v}:p{page}"

def invalidate_category(cid):
    r.incr(f"catver:{cid}")   # một lệnh duy nhất
```

Sau `INCR`, mọi request tính ra key mới, key cũ không ai đọc nữa và chết dần theo TTL. Ưu điểm: invalidate hàng loạt bằng một lệnh, không có race giữa xóa và ghi lại. Nhược điểm: key cũ vẫn chiếm memory đến hết TTL, và mỗi lần đọc tốn thêm một lệnh lấy version.

## Ba chiến lược, một quyết định

- Hết hạn tự nhiên: đơn giản nhất, dùng khi dữ liệu đổi chậm và cũ vài phút không sao.
- Xóa chủ động khi ghi: dùng khi cần đúng ngay sau khi ghi, số key nhỏ và biết rõ key nào.
- Version key: dùng khi một thay đổi ảnh hưởng nhiều key cùng lúc.

Trong thực tế bạn thường dùng cả ba trên cùng một hệ thống. Ví dụ: giá dùng TTL 60 giây cộng xóa khi ghi, listing dùng version key, feature flag chỉ dùng TTL.

## Ba câu hỏi trước khi chốt một con số TTL

1. Dữ liệu này cũ bao lâu thì người dùng vẫn chấp nhận được? Đó là trần của TTL.
2. Nếu lệnh xóa cache thất bại, dữ liệu cũ sống bao lâu? Đó là lý do phải có TTL.
3. Có một thay đổi nào ảnh hưởng nhiều key cùng lúc không? Nếu có, cân nhắc version key.

Lỗi thường gặp: đặt TTL 24 giờ cho giá sản phẩm vì "cho nhẹ database", rồi chiến dịch giảm giá không hiển thị và khách phàn nàn. Lỗi thứ hai: xóa cache trước khi commit, tạo ra cache cũ ngay sau khi ghi. Lỗi thứ ba: dựa hoàn toàn vào invalidation mà không có TTL, nên một lệnh xóa lỗi là key cũ sống mãi.

Một mẹo nhỏ: khi chưa chắc, chọn TTL ngắn trước rồi nới ra. Cache ngắn làm database chịu tải cao hơn một chút, nhưng sai dữ liệu thì khó sửa hơn nhiều.

## Ghi nhớ nhanh trước khi sang HTTP cache

- TTL là giới hạn trên của độ cũ, không phải lời hứa dữ liệu đúng.
- Chọn TTL bằng câu hỏi về mức cũ chấp nhận được, không bằng cảm tính.
- Luôn ghi database, commit, rồi mới xóa cache.
- Version key để invalidate hàng loạt; TTL luôn là lưới an toàn phía sau.

Bài tiếp theo đi lên một tầng cao hơn trong cùng ý tưởng này: thay vì tự đặt TTL trong code, ta để trình duyệt và proxy cache làm việc đó qua `Cache-Control`, `ETag` và `Vary`.

TTL là viết tắt của _____, khoảng thời gian một cache entry được sống trước khi bị _____.
Khi ghi dữ liệu, thứ tự đúng là ghi database, _____, rồi mới _____.
Version key invalidate hàng loạt bằng cách _____ thay vì xóa từng key.

Front: TTL là gì?
Back: Thời gian sống của một cache entry; hết hạn thì key tự bị xóa.

Front: TTL đảm bảo điều gì?
Back: Dữ liệu không cũ quá X giây, nhưng không đảm bảo dữ liệu luôn đúng.

Front: Câu hỏi đầu tiên khi chọn TTL là gì?
Back: Dữ liệu này cũ bao lâu thì người dùng vẫn chấp nhận được.

Front: Vì sao vẫn cần TTL khi đã có invalidation chủ động?
Back: TTL là lưới an toàn khi lệnh xóa thất bại hoặc bị bỏ sót.

Front: Thứ tự đúng khi ghi dữ liệu có cache là gì?
Back: Ghi database, commit, rồi xóa cache key.

Front: Vì sao không được xóa cache trước khi commit?
Back: Request khác có thể đọc dữ liệu cũ chưa commit và ghi lại vào cache.

Front: Version key là gì?
Back: Đưa số version vào tên key và invalidate bằng cách tăng version thay vì xóa từng key.

Front: Ưu điểm chính của version key?
Back: Invalidate hàng loạt bằng một lệnh duy nhất, không cần quét và xóa nhiều key.

Front: Nhược điểm của version key?
Back: Key cũ vẫn chiếm memory đến hết TTL, và tốn thêm một lần đọc version.

Front: Loại dữ liệu nào không nên cache?
Back: Số dư ví, quyền truy cập, dữ liệu không chấp nhận cũ dù chỉ vài giây.

Front: TTL gợi ý cho giá sản phẩm?
Back: Khoảng 30 đến 60 giây, kèm xóa cache mỗi khi cập nhật giá.

Front: TTL gợi ý cho cấu hình hoặc feature flag?
Back: Khoảng 10 đến 30 giây, để tự hết hạn, không cần xóa chủ động.

Front: Nếu lệnh xóa cache thất bại sau khi ghi database thì sao?
Back: Dữ liệu cũ tồn tại cho đến khi TTL hết hạn.

---

## Contents

Part of [🗄️ Caching căn bản: Redis, TTL và CDN cho người mới](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi.md)

- **🧱 Nền tảng cache: vì sao và khi nào**
  - [💡 Cache là gì và khi nào nên dùng](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cache-la-gi-va-khi-nao-nen-dung.md)
  - [🔁 Cache-aside: luồng đọc ghi kèm ví dụ](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cache-aside-luong-doc-ghi-voi-vi-du.md) (sign-in required)
  - [✅ Checkpoint: Nền tảng cache](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/checkpoint-nen-tang-cache.md) (sign-in required)
- **⏱️ TTL, invalidation và cache phía HTTP**
  - [🗑️ TTL và invalidation: chọn thời điểm xóa](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/ttl-va-invalidation-chon-thoi-diem-xoa.md) — this lesson
  - [📨 HTTP cache headers: Cache-Control, ETag, Vary](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/http-cache-headers-cache-control-etag-vary.md)
  - [🌐 CDN cho static assets và cache busting](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cdn-cho-static-assets-va-cache-busting.md)
  - [✅ Checkpoint: TTL và cache HTTP](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/checkpoint-ttl-va-cache-http.md)
- **⚠️ Redis và các lỗi cache thường gặp**
  - [🧰 Redis nhập môn: GET, SET, TTL và key naming](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/redis-nhap-mon-get-set-ttl-va-key-naming.md)
  - [🚨 Stale data và cache stampede](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/stale-data-va-cache-stampede.md)
  - [🤖 Yêu cầu AI thêm cache mà không phá logic](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/yeu-cau-ai-them-cache-khong-pha-logic.md) (premium content)
  - [✅ Checkpoint: Redis và lỗi cache](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/checkpoint-redis-va-loi-cache.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cheatsheet.md) (premium content)
