Cách Caching trong Cơ sở dữ liệu Hoạt động
Caching là gì?
Caching (Lưu nhanh) lưu trữ dữ liệu được truy cập thường xuyên (Frequently Accessed Data) vào một lớp nhanh hơn và gần hơn (Faster, Closer Layer) để các yêu cầu sau bỏ qua nguồn chậm hơn. Đối với cơ sở dữ liệu, điều này có nghĩa là tránh I/O đĩa thừa (Redundant Disk I/O) và không cần thực hiện lại các truy vấn tốn kém (Expensive Queries).
Tại sao cần Caching?
| Vấn đề | Caching giúp ích như thế nào |
|---|---|
| Đọc đĩa chậm (Slow Disk Reads) | Dữ liệu trong RAM nhanh hơn SSD/HDD nhiều bậc |
| Truy vấn lặp lại (Repeated Queries) | Các truy vấn giống nhau trả về kết quả đã tính toán sẵn mà không cần thực hiện lại |
| Tải DB cao (High DB Load) | Ít truy vấn đến cơ sở dữ liệu → dư dung lượng cho các thao tác phức tạp |
| Đọc nhạy cảm độ trễ (Latency-sensitive Reads) | Thời gian phản hồi giảm từ mili-giây xuống micro-giây |
Thuật ngữ quan trọng (Key Terminology)
- Cache Hit (Trúng Cache) — dữ liệu được tìm thấy trong Cache (đường dẫn nhanh)
- Cache Miss (Trượt Cache) — dữ liệu không có trong Cache; phải lấy từ nguồn (đường dẫn chậm)
- Hit Ratio (Tỷ lệ Trúng) —
hits / (hits + misses)— chỉ số sức khỏe Cache chính yếu - TTL (Time-to-Live - Thời gian sống) — bao lâu một mục được coi là hợp lệ trước khi làm mới
- Eviction Policy (Chính sách Loại bỏ) — quy tắc quyết định dữ liệu nào bị xóa khi Cache đầy (LRU, LFU, TTL)
- Cache Invalidation (Vô hiệu hóa Cache) — xóa hoặc cập nhật dữ liệu cũ (Stale Data) trong Cache
Luồng Hit/Miss
Quan sát cách các yêu cầu đi qua lớp Cache. Hit trả về dữ liệu ngay lập tức; Miss truy vấn cơ sở dữ liệu và lưu kết quả cho lần sau:
Các mức Caching trong Cơ sở dữ liệu
Caching diễn ra ở nhiều lớp (Layers) giữa Client và đĩa:
Client Request
↓
[1] Application Cache (Redis, Memcached, In-process)
↓ miss
[2] Query Cache (tích hợp trong một số RDBMS — hiếm trong hệ thống hiện đại)
↓ miss
[3] Buffer Pool (bộ nhớ DB Engine — luôn có)
↓ miss
[4] Disk / SSD (tệp dữ liệu thực tế)
1. Buffer Pool / Page Cache (Bộ đệm Trang)
Buffer Pool là bộ nhớ đệm nội bộ của Database Engine. Nó lưu trữ các Data Pages (Trang dữ liệu) — các khối kích thước cố định, thường 8–16 KB — vừa được đọc từ hoặc ghi lên đĩa.
- Mọi thao tác
SELECT,INSERT,UPDATE,DELETEđều hoạt động trên các Page trong Buffer Pool - Dirty Pages (Trang bẩn) — đã sửa đổi nhưng chưa ghi lên đĩa — được ghi lại bởi quá trình Checkpoint
- Đây là lớp Caching cơ bản nhất — có mặt trong mọi RDBMS chính
Tỷ lệ Hit của Buffer Pool — mục tiêu Production: > 95%
-- PostgreSQL
SELECT sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS ratio
FROM pg_statio_user_tables;
-- MySQL
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
2. Query Cache (Bộ nhớ nhanh Truy vấn)
Một số cơ sở dữ liệu ánh xạ chuỗi SQL cố định (Literal SQL String) với tập kết quả (Result Set) của nó. Nếu cùng truy vấn thực hiện lại và không có dữ liệu thay đổi, kết quả đã Cache được trả về trực tiếp.
| Cơ sở dữ liệu | Hỗ trợ Query Cache |
|---|---|
| MySQL | Không còn dùng (Deprecated) từ 5.7, đã loại bỏ (Removed) trong 8.0 — Mutex toàn cục gây tranh chấp (Contention) khi ghi |
| PostgreSQL | Không có Query Cache tích hợp; phụ thuộc vào Caching ở mức ứng dụng |
| SQL Server | Plan Cache (lưu Execution Plan, không phải kết quả) |
Mỗi lần ghi vào bất kỳ bảng nào đều vô hiệu hóa tất cả truy vấn đã Cache cho bảng đó dưới một Mutex toàn cục. Chi phí vượt quá lợi ích trong hầu hết workload thực tế.
3. Cache ở mức Ứng dụng (Application-level Cache)
Một Cache bên ngoài nằm giữa ứng dụng và cơ sở dữ liệu. Ứng dụng kiểm tra Cache trước; chỉ khi Miss mới truy vấn DB và điền vào Cache (Populate Cache).
| Cache | Phù hợp nhất cho |
|---|---|
| Redis | Cấu trúc dữ liệu phong phú (Rich Data Structures), Persistence, Pub/Sub, hỗ trợ TTL |
| Memcached | Key-value đơn giản, Đa luồng (Multithreaded), rất nhanh cho tra cứu cơ bản |
| In-process (IMemoryCache) | Không qua mạng (No Network Hop), nhanh nhất, chỉ cho từng Instance |
// Cache-aside Read Pattern
async Task<User> GetUser(int id)
{
var key = $"user:{id}";
var cached = await cache.GetStringAsync(key);
if (cached is not null)
return Deserialize<User>(cached); // Hit
var user = await db.Users.FindAsync(id); // Miss → query DB
await cache.SetStringAsync(key, Serialize(user), new() // Populate Cache
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
return user;
}
So sánh các Chiến lược Cache (Cache Strategies)
Chuyển đổi giữa năm chiến lược — quan sát khi nào Client nhận được ACK và khi nào cơ sở dữ liệu thực sự được truy cập:
The most common pattern. Application checks the cache first; on a miss, it queries the database and stores the result. Next read is a cache hit.
Chiến lược Đọc (Read Strategies)
Cache-Aside (Lazy Loading - Tải chậm)
Ứng dụng tự quản lý Cache (Application manages the cache explicitly). Khi đọc, kiểm tra Cache trước; nếu Miss, truy vấn nguồn và tự điền vào Cache.
- Ưu điểm: Đơn giản, kiểm soát đầy đủ, hoạt động với bất kỳ Cache Store nào
- Nhược điểm: Logic Cache rò rỉ vào mã ứng dụng; khởi động nguội (Cold Start) chậm
- Phù hợp nhất cho: Mục đích chung (General-purpose), workload hỗn hợp Đọc/Ghi
Read-Through
Cache Provider nằm giữa ứng dụng và cơ sở dữ liệu. Khi Miss, Cache Provider tự động tải dữ liệu từ cơ sở dữ liệu — ứng dụng chỉ giao tiếp với lớp Cache.
- Ưu điểm: Mã ứng dụng sạch hơn (không có logic Cache), hành vi Cache nhất quán giữa các dịch vụ
- Nhược điểm: Gắn chặt với khả năng của Cache Provider, ít kiểm soát logic tải
- Phù hợp nhất cho: Workload đọc nhiều (Read-heavy), muốn tách biệt trách nhiệm rõ ràng
- Ví dụ: Spring Cache (
@Cacheable), Hibernate Second-level Cache, Redis với Lua Scripts
Chiến lược Ghi (Write Strategies)
Write-Through
Mọi thao tác ghi đều đến cả Cache và cơ sở dữ liệu đồng bộ trước khi xác nhận Client.
- Ưu điểm: Tính nhất quán mạnh (Strong Consistency) — Cache và DB luôn đồng bộ; đọc luôn nhanh
- Nhược điểm: Độ trễ ghi cao (hai lần ghi đồng bộ)
- Phù hợp nhất cho: Workload đọc nhiều, tính nhất quán quan trọng hơn tốc độ ghi
Write-Around
Ghi trực tiếp đến cơ sở dữ liệu, bỏ qua Cache hoàn toàn. Cache chỉ được điền khi dữ liệu được đọc (Miss → truy vấn DB → lưu vào Cache).
- Ưu điểm: Cache không bị ô nhiễm bởi dữ liệu chỉ ghi; không khuếch đại ghi (No Write Amplification)
- Nhược điểm: Dữ liệu vừa ghi không có trong Cache cho đến khi đọc lần đầu; trượt hàng loạt (Burst of Misses) sau khi ghi
- Phù hợp nhất cho: Workload ghi nhiều, dữ liệu không được đọc lại ngay (Log, Analytics, Bulk Import)
Write-Behind (Write-Back - Ghi ngược)
Ghi chỉ đến Cache; Client được xác nhận ngay lập tức. Cơ sở dữ liệu được cập nhật bất đồng bộ theo lô (Batch).
- Ưu điểm: Ghi rất nhanh; ghi lô giảm tải DB
- Nhược điểm: Nguy cơ mất dữ liệu nếu Cache Crash trước khi Flush; tính nhất quán cuối cùng (Eventual Consistency)
- Phù hợp nhất cho: Workload ghi nhiều, chấp nhận mất dữ liệu (Session State, Telemetry Counters)
Bảng so sánh chiến lược
| Khía cạnh | Cache-Aside | Read-Through | Write-Through | Write-Around | Write-Behind |
|---|---|---|---|---|---|
| Luồng ghi (Write Path) | Ứng dụng cập nhật DB, rồi vô hiệu hóa Cache | Giống Cache-Aside | Cache + DB đồng bộ | Trực tiếp đến DB, bỏ qua Cache | Chỉ Cache; DB bất đồng bộ |
| Luồng đọc (Read Path) | Kiểm tra Cache → Miss → truy vấn DB → lưu | Cache Provider tự tải từ DB khi Miss | Cache luôn được điền sau khi ghi | Miss → DB → lưu (giống Cache-Aside) | Cache luôn được điền sau khi ghi |
| Tính nhất quán (Consistency) | Cuối cùng (Eventual) — cũ cho đến khi vô hiệu hóa | Cuối cùng (Eventual) — giống Cache-Aside | Mạnh (Strong) — luôn đồng bộ | Cuối cùng (Eventual) — Cache điền lười biếng | Cuối cùng (Eventual) — DB trễ hơn Cache |
| Độ trễ ghi (Write Latency) | Bình thường | Không áp dụng (chiến lược chỉ đọc) | Cao (hai lần ghi đồng bộ) | Bình thường (một lần ghi DB) | Rất nhanh (chỉ ghi vào Cache) |
| Nguy cơ mất dữ liệu | Không | Không | Không | Không | Có — Crash Cache mất các ghi chưa Flush |
| Phù hợp nhất cho | Mục đích chung, workload hỗn hợp | Đọc nhiều, mã ứng dụng sạch | Đọc nhiều, tính nhất quán mạnh | Ghi nhiều, dữ liệu không đọc lại ngay | Ghi nhiều, chấp nhận Eventual Consistency |
Kết hợp chiến lược (Combining Strategies)
Chiến lược Đọc và Ghi có thể ghép cặp:
| Đọc + Ghi | Hiệu quả |
|---|---|
| Cache-Aside + Write-Around | Đơn giản, Cache chỉ điền khi đọc |
| Cache-Aside + Write-Through | Nhất quán, nhưng ứng dụng quản lý Cache |
| Read-Through + Write-Through | Mã ứng dụng sạch + tính nhất quán mạnh |
| Read-Through + Write-Behind | Mã ứng dụng sạch + ghi nhanh (Eventual Consistency) |
Chiến lược Vô hiệu hóa Cache (Cache Invalidation Strategies)
Biết khi nào dữ liệu Cache đã cũ (Stale) là bài toán khó nhất trong Caching.
Hết hạn dựa trên TTL (TTL-based Expiration)
Mỗi mục có một Time-to-Live. Khi TTL hết hạn, mục sẽ tự động bị loại bỏ (Evicted).
- Ưu điểm: Đơn giản, không cần phối hợp (Coordination)
- Nhược điểm: Dữ liệu cũ trong khoảng TTL; TTL ngắn giảm độ cũ nhưng tăng Miss
- Phù hợp nhất cho: Dữ liệu ít thay đổi, chấp nhận hơi cũ
Vô hiệu hóa rõ ràng (Explicit Invalidation)
Ứng dụng hoặc Trigger của cơ sở dữ liệu xóa/cập nhật các mục Cache khi dữ liệu thay đổi.
- Ưu điểm: Kiểm soát chính xác
- Nhược điểm: Cần biết khi nào dữ liệu thay đổi
- Kỹ thuật phổ biến: PostgreSQL
LISTEN/NOTIFY, MongoDB Change Streams, SQL Server Change Tracking
Chính sách Loại bỏ (Eviction Policies)
Khi Cache đầy, một thứ gì đó phải bị xóa:
| Chính sách | Loại bỏ | Phù hợp nhất cho |
|---|---|---|
| LRU (Ít dùng gần đây nhất - Least Recently Used) | Mục không dùng lâu nhất | Mục đích chung — giả định truy cập gần đây dự đoán truy cập tương lai |
| LFU (Ít dùng nhất - Least Frequently Used) | Mục được truy cập ít nhất | Mẫu truy cập ổn định, một số dữ liệu thực sự phổ biến hơn |
| FIFO (Vào trước Ra trước - First In, First Out) | Mục cũ nhất | Triển khai đơn giản; hiếm khi tối ưu |
| Dựa trên TTL | Các mục hết hạn bất kể dung lượng | Dữ liệu nhạy cảm thời gian (Session, phản hồi API) |
Các lỗi Thường gặp (Common Pitfalls)
Cache Stampede / Thundering Herd (Đốt bộ nhớ nhanh)
Một mục phổ biến hết hạn và nhiều yêu cầu đồng thời cùng Miss Cache cùng lúc, đánh sập cơ sở dữ liệu.
Cách khắc phục: Khóa phân tán/Mutex (Distributed Lock) để chỉ một yêu cầu xây dựng lại; hoặc làm mới trước khi hết hạn (ví dụ: ở 80% TTL với Jitter).
Cache Penetration (Xuyên bộ nhớ nhanh)
Truy vấn cho dữ liệu không tồn tại không bao giờ được Cache (không có mục để lưu), nên mọi yêu cầu đều đến DB.
Cách khắc phục: Cache kết quả Null với TTL ngắn; hoặc sử dụng Bloom Filter để kiểm tra sự tồn tại trước.
Cache Breakdown (Phá vỡ bộ nhớ nhanh)
Một Hot Key duy nhất hết hạn, gây ra đột biến bất ngờ.
Cách khắc phục: Không bao giờ để Hot Key hết hạn — làm mới chúng bất đồng bộ ở nền (Background).
Over-caching (Lạm dụng Cache)
Không nên Cache: dữ liệu thay đổi thường xuyên, dữ liệu thời gian thực (số dư tài khoản), hoặc Blob có kích thước quá lớn.
Nguyên tắc: Cache dữ liệu được đọc thường xuyên (Read Frequently) và ít thay đổi (Changes Rarely).
Caching trong Thực tế
| Cơ sở dữ liệu | Cấu hình Buffer Pool | Query Cache | Cache Ứng dụng |
|---|---|---|---|
| PostgreSQL | shared_buffers (~25% RAM) | Không có | Redis / mức ứng dụng |
| MySQL | innodb_buffer_pool_size | Đã loại bỏ trong 8.0 | Redis / mức ứng dụng |
| SQL Server | Buffer Pool (tự động quản lý) | Plan Cache (Execution Plan) | Redis / mức ứng dụng |
-- PostgreSQL: tải trước bảng vào Buffer Pool
SELECT pg_prewarm('users');
-- SQL Server: kiểm tra Buffer Pool usage
SELECT COUNT(*) * 8 / 1024 AS total_mb
FROM sys.dm_os_buffer_descriptors
WHERE database_id = DB_ID();
# Redis: các thao tác Cache cơ bản
SET user:42 '{"name":"Alice"}' EX 1800 # Đặt với TTL 30 phút
GET user:42 # Lấy
DEL user:42 # Vô hiệu hóa
Khi nào dùng Chiến lược nào
| Tình huống | Cách tiếp cận được khuyến nghị |
|---|---|
| Đọc nhiều, dữ liệu ít thay đổi | Cache-aside hoặc Read-through với TTL dài |
| Đọc nhiều, dữ liệu thỉnh thoảng thay đổi | Cache-aside + Vô hiệu hóa rõ ràng khi ghi |
| Đọc nhiều, muốn mã ứng dụng sạch | Read-through + Write-through |
| Ghi nhiều, dữ liệu không đọc lại ngay | Write-around (Cache chỉ điền khi đọc) |
| Ghi nhiều, chấp nhận Eventual Consistency | Write-behind Cache |
| Yêu cầu tính nhất quán mạnh (Strong Consistency) | Write-through Cache hoặc không dùng Cache |
| Dữ liệu thời gian thực (số dư, tồn kho) | Không Cache — luôn truy vấn cơ sở dữ liệu |
| Dữ liệu Session, giới hạn tốc độ (Rate Limiting) | Redis với TTL |
Các chủ đề Liên quan
- Indexing — giảm chi phí truy vấn trước khi cần Cache
- Redis — Cache Engine phổ biến nhất
- System Design: Caching — Caching trong Hệ thống phân tán (Distributed Systems)