Bài viết gần đây
-
Tối ưu tham số cBot & tránh Overfitting khi Backtest (Optimization)
Tháng 8 26, 2026 -
Tối ưu tham số cBot & tránh Overfitting khi Backtest (Optimization)
Tháng 8 26, 2026 -
Tối ưu tham số cBot & tránh Overfitting khi Backtest (Optimization)
Tháng 8 26, 2026
Trang chủ → Bài Viết → Cách xây dựng Cache TTL: Giảm tải Database cho ứng dụng thực tế (kèm code FastAPI)
| Cách xây dựng Cache TTL: Giảm tải Database cho ứng dụng thực tế (kèm code FastAPI)
Được viết bởi Đặng Trí Thanh vào ngày 29/08/2026 lúc 00:21 | 24 lượt xem
Bạn có bao giờ tự hỏi: “Sao ứng dụng của mình chỉ có vài chục người dùng mà database đã ‘gào thét’?” Một trong những nguyên nhân kinh điển nhất là: mỗi lần người dùng load trang / gọi API, chúng ta lại đọc database dù dữ liệu chẳng khác gì cách đây 2 giây.
Trong bài này, mình sẽ không nói lý thuyết suông. Mình sẽ kể lại một bài toán thực tế 100%: một dashboard giao dịch (trading dashboard) đọc database mỗi lần browser load, bị nghẽn, và cách mình xây Cache TTL (Time-To-Live) để giảm hàng chục lần số lần query database — kèm toàn bộ code chạy được.
1. Bài toán thực tế: Dashboard cứ “load” là đọc database
Hãy tưởng tượng bạn có một dashboard theo dõi tài khoản giao dịch realtime. Kiến trúc đơn giản như thế này:
EA trên MT5 (39 tài khoản)
│ POST /mt5/snapshot (mỗi vài giây / tài khoản)
▼
Collector API (FastAPI) ──► Database (SQLite / SQL Server)
▲
│ GET /api/dashboard (browser poll liên tục, nhiều client)
│
Dashboard Web (JavaScript)
Có 2 luồng truy cập database:
- EA (Expert Advisor trên MetaTrader) liên tục GHI snapshot (balance, equity, floating…) vào database, có thể vài chục request mỗi giây.
- Browser mở dashboard thì JavaScript poll
GET /api/dashboard— refresh liên tục, mỗi lần là một request. Mở nhiều tab, nhiều máy thì request càng dày.
Vấn đề nằm ở request thứ 2: mỗi lần browser poll, backend lại query database để lấy:
– Snapshot mới nhất của từng tài khoản (1 query).
– Baseline equity quanh các mốc giờ (04h, 05h, 07h) — đây là query rất nặng, phải quét lịch sử cả 24 giờ cho từng tài khoản (3 query).
Hậu quả: database phải phục vụ gấp nhiều lần số request thật sự cần thiết. Trong khi đó, dữ liệu giữa 2 lần poll gần như không đổi — người dùng đâu có thấy gì khác!
Câu hỏi đặt ra: Làm sao để đọc database ít đi mà vẫn giữ được cảm giác “realtime”?
Câu trả lời: Cache. Cụ thể ở đây là In-Memory Cache + TTL.
2. Cache là gì? Có những loại cache nào?
Cache là kỹ thuật lưu lại kết quả tính toán / dữ liệu đắt đỏ vào một nơi đọc nhanh hơn, để lần sau không phải tính / đọc lại từ đầu. Trong web, có nhiều tầng cache:
| Tầng cache | Ví dụ | Đọc nhanh nhất |
|---|---|---|
| Browser cache | Browser lưu file ảnh, CSS, JS | Không cần gửi request |
| CDN cache | Cloudflare, CloudFront lưu nội dung tĩnh gần user | Gần user |
| Reverse proxy cache | Nginx, Varnish | Không cần chạy ứng dụng |
| Application cache | Redis, In-memory trong code | Không cần network |
| Database cache | Buffer pool, query cache | Không cần parse SQL |
Ở bài này, mình tập trung vào Application Cache dạng In-Memory — lưu dữ liệu ngay trong RAM của tiến trình Python. Đây là cách đơn giản, không cần thêm hạ tầng nào, phù hợp với hệ thống chạy trên 1 máy (LAN/on-premise), lại cực kỳ hiệu quả.
Nguyên lý cốt lõi: “Nếu dữ liệu chưa đổi thì đừng đọc lại. Nếu nó đổi thì sau một khoảng thời gian ngắn hãy đọc lại.”
3. Hai “nút thắt” cần cache
Trong dashboard của mình có 2 hàm đọc database nặng:
# Hàm 1: lấy snapshot mới nhất của TẤT CẢ tài khoản
def latest_snapshots_by_login() -> dict[int, dict]:
# SELECT ... GROUP BY login ... (1 query)
...
# Hàm 2: tổng equity baseline quanh mốc giờ (04h/05h/07h)
def baseline_hour_totals(hour: int, logins: set[int] | None = None) -> dict:
# Query quét lịch sử 24h cho từng login (RẤT NẶNG)
...
Mỗi request /api/dashboard gọi 1 lần hàm 1 + 3 lần hàm 2. Browser poll liên tục → database bị “đánh” liên tục. Mục tiêu: cache cả hai hàm này.
4. Ý tưởng: Cache TTL là gì?
TTL (Time-To-Live) là “tuổi thọ” của một bản cache. Ý tưởng cực kỳ đơn giản:
Mỗi khi đọc dữ liệu từ database, ta lưu kết quả + thời điểm đọc vào RAM. Khi có request mới:
– Nếu thời gian trôi qua < TTL → trả dữ liệu trong RAM (không đụng database).
– Nếu ≥ TTL → đọc lại database, cập nhật cache.
Về bản chất, cache TTL là một dictionary với timestamp:
cache = {
"ts": 0.0, # thời điểm đọc database lần cuối (dùng time.monotonic())
"data": None, # dữ liệu đã đọc
}
Thời điểm đo bằng time.monotonic() — đồng hồ chỉ tăng, không bị đổi giờ hệ thống, không bị sai khi đổi timezone. Đây là chi tiết nhỏ nhưng quan trọng!
5. Mô hình Cache-Aside (Lazy Loading) — mô hình phổ biến nhất
Trước khi viết code, bạn cần biết mình đang dùng mô hình cache nào. Có 4 mô hình ghi cache kinh điển:
| Mô hình | Cách hoạt động | Khi nào dùng |
|---|---|---|
| Cache-Aside (Lazy) | Đọc: check cache → miss thì đọc DB → ghi cache. Ghi: ghi DB rồi xoá cache | Phổ biến nhất, dễ nhất |
| Read-Through | App gọi cache, cache tự đọc DB khi miss | Cần tách lớp storage |
| Write-Through | Ghi cache TRƯỚC, cache ghi DB sau | Cần dữ liệu luôn nhất quán |
| Write-Back | Ghi cache, gom rồi ghi DB theo lô | Hiệu năng cao, chấp nhận rủi ro mất dữ liệu |
Code trong bài này chính là mô hình Cache-Aside: “Check cache trước, miss thì đọc database, rồi lưu vào cache cho lần sau.” Đây là mô hình được khuyên dùng nhiều nhất vì:
– Dễ hiểu, dễ debug — bạn luôn biết dữ liệu đến từ đâu.
– An toàn — cache hỏng cũng không mất dữ liệu (chỉ chậm).
– Dễ nâng cấp — từ in-memory lên Redis chỉ đổi “tầng lưu”.
Request ──► Cache có data? ── có ──► trả ngay (CACHE HIT)
│
không
▼
Đọc Database ──► lưu vào Cache ──► trả về (CACHE MISS)
6. Bước 1 — Cache đơn giản: Snapshot (4 giây)
Áp dụng cho hàm latest_snapshots_by_login(). Toàn bộ code:
import time
# Cache trong bộ nhớ (module-level)
_snap_cache: dict = {"ts": 0.0, "data": None}
SNAP_CACHE_TTL = 4.0 # giây
def latest_snapshots_by_login() -> dict[int, dict]:
now = time.monotonic()
# ── CACHE HIT: dữ liệu còn "mới" → trả thẳng, không đụng DB ──
if _snap_cache["data"] is not None and now - _snap_cache["ts"] < SNAP_CACHE_TTL:
return _snap_cache["data"]
# ── CACHE MISS: quá TTL → đọc database 1 lần ──
rows = db.query("SELECT ... GROUP BY login ...") # query thật
out = {}
for r in rows:
out[r["login"]] = dict(r)
# Ghi lại cache để lần sau khỏi đọc
_snap_cache["ts"] = now
_snap_cache["data"] = out
return out
Giải thích logic:
- Lần đầu gọi: cache trống (
data is None) → đọc DB → lưu cache. - Các lần sau trong 4 giây: trả cache ngay, 0 query.
- Sau 4 giây: đọc DB lại, làm mới cache.
Kết quả: dù browser poll 100 lần trong 4 giây, database chỉ bị đọc 1 lần. Giảm ~100 lần số query snapshot!
7. Bước 2 — Cache theo KEY: Baseline (60 giây)
Hàm baseline_hour_totals(hour, logins) khó hơn vì kết quả phụ thuộc tham số: khác hour (4, 5, 7) và khác danh sách logins → kết quả khác. Không thể cache “1 ô chung” được.
Giải pháp: cache là một dict, key = bộ tham số đã chuẩn hoá:
_baseline_cache: dict = {} # key -> (timestamp, data)
BASELINE_CACHE_TTL = 60.0 # baseline ít đổi, TTL dài
def baseline_hour_totals(hour: int, logins: set[int] | None = None) -> dict:
# Chuẩn hoá key: set không hash được → dùng frozenset
key = (hour, frozenset(logins) if logins is not None else None)
now = time.monotonic()
hit = _baseline_cache.get(key)
if hit is not None and now - hit[0] < BASELINE_CACHE_TTL:
return hit[1] # CACHE HIT
data = _baseline_hour_totals_core(hour, logins) # query nặng
_baseline_cache[key] = (now, data) # CACHE MISS → lưu lại
return data
Chi tiết quan trọng: logins là một set — mà set không hash được (unhashable) nên không thể làm key dict. Phải chuyển thành frozenset(...). Nếu không làm bước này, chương trình sẽ ném TypeError: unhashable type: 'set'.
Với cache theo key này, 3 query baseline nặng (mốc 04h/05h/07h) thay vì chạy mỗi lần poll thì chỉ chạy 1 lần / 60 giây cho mỗi bộ key.
8. Bước 3 — Tách hàm gốc: Wrapper Pattern
Để không phải sửa lung tung bên trong hàm query cũ (dễ gây bug), mình dùng Wrapper Pattern: đổi tên hàm gốc thành _baseline_hour_totals_core (phần “core” chỉ làm đúng 1 việc: đọc DB), rồi viết hàm baseline_hour_totals phía trên chỉ lo phần cache:
def baseline_hour_totals(hour, logins=None):
"""Wrapper: lo phần cache, không đọc DB trực tiếp."""
key = (hour, frozenset(logins) if logins is not None else None)
now = time.monotonic()
hit = _baseline_cache.get(key)
if hit is not None and now - hit[0] < BASELINE_CACHE_TTL:
return hit[1]
data = _baseline_hour_totals_core(hour, logins) # delegate cho hàm gốc
_baseline_cache[key] = (now, data)
return data
def _baseline_hour_totals_core(hour, logins=None):
"""Hàm gốc: chỉ đọc database, không biết gì về cache."""
... # toàn bộ code query cũ giữ nguyên
return {...}
Lợi ích to lớn: mọi nơi khác trong code (kể cả hàm khác gọi baseline_hour_totals) đều không cần sửa — chúng vẫn gọi tên hàm cũ, tự động được hưởng cache. Đây là nguyên tắc “đóng gói” (encapsulation): caller không cần biết dữ liệu đến từ DB hay từ cache.
9. Bài học XƯƠNG MÁU: Tại sao KHÔNG invalidate cache khi dữ liệu mới đến?
Đây là phần quan trọng nhất, mình đã vấp và phải sửa lại.
Suy nghĩ ban đầu (sai): “EA POST snapshot mới liên tục → mỗi lần nhận snapshot thì xoá cache để luôn realtime.”
def post_snapshot(...):
db.insert(snapshot)
_snap_cache["data"] = None # ❌ invalidate cache mỗi lần POST
return {"ok": True}
Vì sao SAI? Vì EA POST liên tục (nhiều lần mỗi giây). Mỗi lần POST là một lần xoá cache → cache không bao giờ tồn tại được quá vài mili-giây → request kế tiếp luôn “CACHE MISS” → lại đọc DB → chẳng giảm được gì, thậm chí còn thêm chi phí quản lý cache!
Cách đúng: Với dữ liệu được ghi liên tục (write-heavy), ta không cần invalidate — chỉ cần TTL ngắn. Vì:
– EA vẫn ghi database bình thường (dữ liệu luôn mới trong DB).
– Cache chỉ “giữ” bản đọc gần nhất tối đa TTL giây.
– Với TTL = 4s, số liệu hiển thị trễ tối đa 4s — hoàn toàn chấp nhận được cho dashboard theo dõi.
def post_snapshot(...):
db.insert(snapshot)
# ✅ KHÔNG xoá cache. Để TTL tự làm việc.
return {"ok": True}
Nguyên tắc vàng cần nhớ:
Nếu dữ liệu ghi liên tục → dùng TTL thuần, đừng invalidate.
Nếu dữ liệu ghi thưa, người dùng cần thấy NGAY → invalidate khi ghi.
Ví dụ minh hoạ:
– Dashboard trading (EA ghi mỗi giây) → TTL thuần ✅
– Bảng cấu hình admin (sửa hiếm, cần thấy ngay sau khi sửa) → invalidate khi sửa ✅
10. Thêm vấn đề: Single-flight & Thread safety
Có một “lỗ hổng” mà mình muốn bạn biết: Cache Stampede (đoàn xô đổ). Khi cache vừa hết hạn, nhiều request cùng lúc thấy CACHE MISS → đồng loạt đọc database → database lại bị tấn công, đúng lúc mình muốn tránh!
Cách xử lý phổ biến: Single-flight — chỉ cho 1 request đi đọc DB, các request còn lại chờ / dùng cache cũ. Trong dashboard MT5, với endpoint /api/positions (quét 39 tài khoản MT5 tuần tự rất nặng), mình dùng lock như thế này:
import threading
_positions_cache = {"ts": 0.0, "data": None}
_positions_scanning = threading.Lock()
POSITIONS_TTL = 20.0
def get_positions():
now = time.monotonic()
if _positions_cache["ts"] and now - _positions_cache["ts"] < POSITIONS_TTL:
return _positions_cache["data"] # cache còn mới
if _positions_scanning.locked(): # đã có request khác đang scan
if _positions_cache["data"] is not None:
return _positions_cache["data"] # trả cache cũ thay vì xếp hàng
# không có cache → chờ
with _positions_scanning:
return _positions_cache["data"]
with _positions_scanning: # đảm bảo chỉ 1 luồng scan
if now - _positions_cache["ts"] < POSITIONS_TTL:
return _positions_cache["data"] # ai đó vừa scan xong
_positions_cache["data"] = slow_scan_all_accounts()
_positions_cache["ts"] = time.monotonic()
return _positions_cache["data"]
Lưu ý: FastAPI với def (đồng bộ) chạy trong ThreadPoolExecutor → nhiều thread cùng truy cập biến cache. Nếu không khoá, hai request có thể cùng đọc DB (thừa) hoặc tệ hơn, ghi dict cùng lúc. threading.Lock giúp đảm bảo chỉ 1 thread thực hiện việc nặng. Với cache TTL đơn giản (đọc-then-ghi dict), chi phí lock không đáng kể.
11. Đo lường: Cache “ăn” được bao nhiêu?
Sau khi triển khai, mình đếm hiệu quả:
| Hạng mục | Trước (không cache) | Sau (cache TTL) |
|---|---|---|
| Query snapshot / giây | Mỗi poll → 1 query | 1 query / 4 giây |
| Query baseline / giây | Mỗi poll → 3 query | 3 query / 60 giây |
| Query nặng (quét 24h) | Vô số lần | 1 lần / phút |
| Cảm giác realtime | Tốt | Vẫn tốt (trễ ≤ 4s) |
Với dashboard poll mỗi 2 giây:
– Trước: 60 request → 60 snapshot query + 180 baseline query / phút.
– Sau: 60 request → chỉ 15 snapshot query + 3 baseline query / phút.
Giảm hơn 90% số lần đọc database — chỉ với ~30 dòng code, không thêm hạ tầng nào. Đó là sức mạnh của cache.
12. Những “cái bẫy” cần nhớ khi xây cache
-
Cache nằm trong RAM của tiến trình → restart server là cache trống, lần load đầu sẽ đọc lại DB bình thường (không mất dữ liệu, chỉ chậm 1 nhịp). Nếu chạy nhiều tiến trình (multi-worker / nhiều máy), mỗi tiến trình có cache riêng → cân nhắc Redis để dùng chung.
-
age_sectrong cache bị “đóng băng”: nếu bên trong cache có timestamp “cách đây 2 giây”, nhưng cache sống 4 giây, thì sau 3 giây nó vẫn báo “2 giây”. Chấp nhận được khi TTL ngắn. Nếu cần chính xác, hãy cache dữ liệu thô và tínhagetại thời điểm trả về. -
Đừng cache dữ liệu nhạy cảm sai chỗ: cache theo user, đừng để user A nhìn thấy data của user B. Với dashboard nội bộ thì đơn giản, nhưng với web công khai phải cẩn thận.
-
TTL ngắn ≠ realtime tuyệt đối: nếu nghiệp vụ cần số liệu chính xác tức thời (ví dụ tính tiền), đừng cho qua cache dài. Hãy giữ 1 API “không cache” cho những thao tác quan trọng.
-
Giới hạn kích thước cache: cache theo key (như baseline) có thể lớn dần nếu key đa dạng. Hãy đặt giới hạn số entry (ví dụ xoá entry cũ nhất khi quá N) hoặc dùng
functools.lru_cachecho cache tĩnh. -
Đừng cache mọi thứ: chỉ cache thứ đắt + ít đổi. Cache một query rẻ 1ms chẳng đáng bao nhiêu, còn tốn RAM.7. Cache key trùng nhau: nếu 2 cách biểu diễn khác nhau cho cùng dữ liệu (ví dụ
login=1vslogin="01"), cache sẽ lưu 2 bản — lãng phí RAM và miss nhiều hơn. Luôn chuẩn hoá key ở một nơi duy nhất. - Quên “làm ấm” cache (cache warming): sau khi restart, request đầu tiên của mỗi key sẽ miss (chậm). Với dashboard quan trọng, có thể chạy 1 task nhỏ lúc khởi động để đọc trước các dữ liệu hay dùng — giúp người dùng không bao giờ thấy nhịp chậm đầu tiên.
- Cache trong vòng lặp (nested): đừng để hàm A cache kết quả đã bao gồm cache của hàm B — khi B bị invalidate, A vẫn trả dữ liệu cũ. Hãy cache ở đúng một tầng (thường là tầng đọc DB sâu nhất).
13. Khi nào nên dùng Redis thay vì In-Memory?
In-Memory cache (dạng này) miễn phí, zero hạ tầng, cực nhanh — phù hợp với hệ thống chạy trên 1 máy. Nhưng khi nào thì phải lên Redis?
- Ứng dụng chạy nhiều worker / nhiều server → cần cache dùng chung giữa các tiến trình.
- Cần cache tồn tại sau restart (ví dụ cache qua đêm).
- Cần các cấu trúc phức tạp: sorted set, pub/sub, rate limit.
- Muốn TTL tự động xoá (Redis tự xoá key hết hạn, không cần code kiểm tra).
Một mẹo nhỏ: bạn có thể đổi code in-memory → Redis chỉ bằng cách đổi “tầng lưu trữ” nếu ngay từ đầu bạn tách cache thành một lớp riêng (function wrapper) — giống hệt Wrapper Pattern ở mục 8. Đó là lý do mình luôn khuyên tách cache ra khỏi logic đọc DB.
Ví dụ, với redis-py, việc đổi cache chỉ còn vài dòng nhờ đã tách lớp từ trước:
import redis
import json
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
class RedisCache:
def __init__(self, ttl: float):
self.ttl = ttl
def get(self, key):
raw = r.get(str(key))
return json.loads(raw) if raw else None
def set(self, key, value):
r.set(str(key), json.dumps(value, default=str), ex=self.ttl)
Thấy không — chỉ cần class RedisCache có cùng 2 phương thức get/set là phần còn lại của ứng dụng không đổi một dòng. Đó chính là sức mạnh của việc tách lớp cache ngay từ đầu.
14. Checklist triển khai cache cho ứng dụng của bạn
- Tìm query đắt nhất — dùng log query / profiler, đừng đoán.
- Hỏi 2 câu: dữ liệu có đổi thường xuyên không? Người dùng có cần thấy ngay không? → chọn TTL ngắn/dài, chọn invalidate hay không.
- Cache ở tầng đúng: tầng hàm đọc DB (repository) là chỗ an toàn nhất.
- Chuẩn hoá key (nhớ vụ
frozenset!). - Bọc bằng wrapper để caller không đổi.
- Cân nhắc single-flight nếu query rất nặng và có nhiều request đồng thời.
- Đo lại hiệu quả sau khi cài (đếm query, so sánh thời gian phản hồi).
- Có kế hoạch khi restart: chấp nhận cache trống, hoặc “làm ấm” cache lúc khởi động.
15. Một lớp Cache tổng quát: TTLCache (tái sử dụng)
Ở trên mình viết cache “thủ công” để bạn hiểu bản chất. Nhưng trong dự án thật, bạn sẽ có nhiều hàm cần cache — viết đi viết lại if cache is not None and now - ts < TTL rất mệt. Giải pháp: gói gọn thành một class TTLCache tái sử dụng:
import time
from typing import Any, Callable
class TTLCache:
def __init__(self, ttl: float, max_entries: int = 1000):
self.ttl = ttl
self.max_entries = max_entries
self._data: dict[Any, tuple[float, Any]] = {} # key -> (ts, value)
def get(self, key: Any) -> Any | None:
item = self._data.get(key)
if item is None:
return None
ts, value = item
if time.monotonic() - ts >= self.ttl:
del self._data[key] # hết hạn → xoá luôn
return None
return value
def set(self, key: Any, value: Any) -> None:
if len(self._data) >= self.max_entries:
# xoá entry hết hạn (hoặc cũ nhất) để tránh cache phình
self._evict()
self._data[key] = (time.monotonic(), value)
def invalidate(self, key: Any) -> None:
self._data.pop(key, None)
def _evict(self) -> None:
# đơn giản: xoá entry cũ nhất theo timestamp
if self._data:
oldest = min(self._data.items(), key=lambda kv: kv[1][0])
del self._data[oldest[0]]
def __len__(self) -> int:
return len(self._data)
# Dùng chung cho nhiều hàm
snapshot_cache = TTLCache(ttl=4.0)
baseline_cache = TTLCache(ttl=60.0)
def get_snapshot_or_load(login: int):
cached = snapshot_cache.get(login)
if cached is not None:
return cached
data = db.load_snapshot(login) # đọc database
snapshot_cache.set(login, data)
return data
Điểm mạnh của cách này:
– Tái sử dụng — mọi hàm chỉ cần TTLCache(ttl=...) + get/set, không lặp logic.
– Chống phình bộ nhớ — giới hạn max_entries, tự xoá entry cũ.
– Sẵn sàng nâng cấp Redis — sau này chỉ cần viết RedisCache cùng 2 phương thức get/set, phần còn lại của code không đổi.
16. Theo dõi & đo lường cache (Hit/Miss)
Cache mà không đo lường thì không biết nó có đang giúp gì không. Bạn nên đếm hit (đọc từ cache) và miss (phải đọc DB):
class TTLCache:
def __init__(self, ttl: float, max_entries: int = 1000):
...
self.hits = 0
self.misses = 0
def get(self, key: Any) -> Any | None:
item = self._data.get(key)
if item is None or time.monotonic() - item[0] >= self.ttl:
self.misses += 1
if item is not None:
del self._data[key]
return None
self.hits += 1
return item[1]
@property
def hit_rate(self) -> float:
total = self.hits + self.misses
return (self.hits / total * 100) if total else 0.0
Thêm 1 endpoint debug để xem tỷ lệ hit:
@app.get("/api/cache/stats")
def cache_stats():
return {
"snapshot": {"hits": snapshot_cache.hits, "misses": snapshot_cache.misses,
"hit_rate": snapshot_cache.hit_rate},
"baseline": {"hits": baseline_cache.hits, "misses": baseline_cache.misses,
"hit_rate": baseline_cache.hit_rate},
}
Tỷ lệ hit nên đạt 90%+ ở những hệ thống đọc nhiều. Nếu hit rate thấp, nghĩa là TTL quá ngắn hoặc key quá “đa dạng” (mỗi request một key khác nhau — dấu hiệu cache vô dụng). Khi đó hãy:
– Tăng TTL (nếu chấp nhận độ trễ).
– Giảm “độ chi tiết” của key (ví dụ cache theo nhóm thay vì từng item).
– Đảm bảo key được chuẩn hoá (nhớ vụ frozenset ở trên!).
17. Tổng kết
Cache TTL không phải là “thuật toán cao siêu” — nó chỉ là một dict + timestamp + vài dòng kiểm tra tuổi. Nhưng đúng chỗ, đúng cách, nó giảm hơn 90% số lần đọc database, giúp hệ thống thở dễ dàng mà người dùng không hề thấy khác biệt.
Ba điều quan trọng nhất bạn cần nhớ từ bài này:
- Cache = lưu kết quả đắt đỏ + TTL — đơn giản nhưng cực hiệu quả.
- Dữ liệu ghi liên tục thì dùng TTL thuần, ĐỪNG invalidate — đó là bài học xương máu mình đã kể.
- Tách cache thành lớp riêng (wrapper) — để sau này muốn nâng cấp lên Redis chỉ cần đổi 1 chỗ.
Còn chần chừ gì nữa — hãy mở log, tìm query đắt nhất, và cache nó ngay hôm nay! 🚀
Nếu bạn muốn học bài bản từ cơ bản để tự xây những hệ thống như thế này — từ Python, FastAPI, database cho đến tối ưu hiệu năng — thì khóa học Hướng nghiệp Dữ liệu chính là lộ trình dành cho bạn.
Weekly Digest — Nhận Bản Tin Hàng Tuần
Nhận các bài viết phân tích kỹ thuật chuyên sâu, thuật toán giao dịch tự động (Trading Bot) và các giải pháp công nghệ mới nhất từ Hướng Nghiệp Dữ Liệu.
Đặng Trí Thanh
Giám đốc Công nghệ · DNT Digital · Giảng viên HNDLĐặng Trí Thanh — Founder & CTO · Hướng Nghiệp Dữ Liệu - DNT Digital. Chuyên đào tạo và triển khai thực chiến Python, MT5 và hệ thống bot auto trading / IB cho học viên và doanh nghiệp.