Mục lục
- Mục Tiêu Bài Học
- Bài Toán Thực Tế: Khi Database Bắt Đầu Hụt Hơi
- Vì Sao Chỉ Có Database Là Không Đủ
- Redis Là Gì?
- Memory vs Disk: Bảng So Sánh Latency
- Tư Duy Latency-First Và Hot Path
- Tổng Quan Các Bài Toán Redis Giải Quyết
- Code Minh Hoạ: SET/GET Và Đo DB vs Cache
- Trade-off Khi Dùng Redis
- Pitfalls Và Anti-patterns
- Tổng Kết, Quiz Và Bài Tiếp Theo
Mục Tiêu Bài Học
Đây là bài mở đầu của series. Mục tiêu không phải gõ lệnh Redis ngay, mà là xây dựng một mental model đúng để các bài sau bám vào. Sau bài này bạn sẽ:
- Nhận diện được các dấu hiệu cho thấy database đang trở thành điểm nghẽn về latency và throughput.
- Giải thích được vì sao đọc từ RAM nhanh hơn đọc từ disk vài bậc độ lớn, và con số đó đến từ đâu.
- Phát biểu chính xác Redis là gì: in-memory data store dạng key-value với nhiều data structure, latency cỡ single-digit millisecond.
- Hình thành tư duy latency-first: thiết kế hệ thống quanh độ trễ và đặt Redis vào hot path.
- Nắm overview các bài toán Redis giải quyết: caching, session, rate limiting, distributed lock, queue, pub/sub, leaderboard, analytics.
- Viết được đoạn
SET/GETđầu tiên bằngioredis(TypeScript) vàredis-py(Python), và đo được chênh lệch thời gian giữa truy vấn DB và đọc cache. - Hiểu trade-off và các pitfall để không lạm dụng Redis.
Bài Toán Thực Tế: Khi Database Bắt Đầu Hụt Hơi
Hãy hình dung một trang chi tiết sản phẩm. Mỗi lần người dùng mở trang, backend chạy một câu query kiểu:
SELECT * FROM products WHERE id = 12345;
Khi có vài chục người dùng, mọi thứ ổn. Nhưng khi trang lên top hoặc có chiến dịch marketing, hàng nghìn request mỗi giây cùng đọc một sản phẩm. Lúc này xuất hiện một loạt vấn đề:
- Đọc lặp lại tốn kém: cùng một dòng dữ liệu (gần như không đổi) bị đọc lại hàng nghìn lần. Mỗi lần database vẫn phải parse query, lập kế hoạch thực thi, tìm index và đọc page từ disk hoặc buffer pool.
- Read-heavy workload: phần lớn ứng dụng web có tỉ lệ đọc/ghi rất lệch, nhiều hệ thống đọc gấp 10–100 lần ghi. Database bị bão hoà vì đọc chứ không phải vì ghi.
- Latency tăng theo tải: khi số kết nối đồng thời tăng, mỗi query phải xếp hàng chờ CPU, lock, I/O. Một query bình thường 2
mscó thể vọt lên 50–200msdưới tải cao. - Connection limit: mỗi kết nối tới database tốn bộ nhớ và tài nguyên. PostgreSQL mặc định
max_connectionsthường khoảng 100; vượt ngưỡng là từ chối kết nối hoặc phải dựng connection pooler. - Traffic đột biến: flash sale, viral, đẩy notification — tải tăng 10x trong vài giây. Database scale dọc (thêm CPU/RAM) thì chậm và đắt; scale ngang thì phức tạp vì ràng buộc consistency.
Điểm chung của tất cả các vấn đề trên: chúng ta đang bắt một hệ thống tối ưu cho bền vững và đúng đắn đi gánh thêm việc trả lời thật nhanh, thật nhiều lần cho cùng một dữ liệu. Đó là hai mục tiêu khác nhau.
Vì Sao Chỉ Có Database Là Không Đủ
Database quan hệ được thiết kế để đảm bảo ACID, lưu trữ bền vững và phục vụ truy vấn phức tạp với join, aggregate, transaction. Để đạt được điều đó, nó phải trả giá ở vài điểm:
Disk I/O là nút cổ chai vật lý
Dữ liệu cuối cùng nằm trên disk để không mất khi mất điện. Ngay cả khi có buffer pool/cache trong RAM, một phần truy cập vẫn chạm disk, và write phải fsync để đảm bảo durability. Truy cập disk được tính bằng ms, trong khi truy cập RAM tính bằng ns tới µs — chênh nhau nhiều bậc độ lớn (xem bảng ở mục sau).
Chi phí của một query không chỉ là đọc dữ liệu
Mỗi câu lệnh phải đi qua: parse cú pháp, phân tích quyền, lập query plan, kiểm tra index, lấy lock cần thiết, đọc các page, rồi serialize kết quả trả về. Với dữ liệu nóng và gần như tĩnh, toàn bộ pipeline này lặp lại một cách lãng phí.
Khó hấp thụ traffic đột biến
Database vốn được tinh chỉnh cho throughput ổn định. Khi tải nhảy vọt, hàng đợi query dài ra, latency dâng lên và có thể kéo theo timeout dây chuyền (cascading failure). Thêm read replica giúp được phần đọc nhưng tốn kém và có độ trễ replication.
Kết luận: database không sai, nó vẫn là source of truth. Vấn đề là dùng đúng công cụ cho đúng việc. Một lớp lưu trữ siêu nhanh, đặt trước database, có thể trả lời phần lớn các lần đọc lặp lại mà không cần đánh thức database. Đó chính là chỗ Redis bước vào.
Redis Là Gì?
Redis (REmote DIctionary Server) là một in-memory data store: nó giữ toàn bộ dataset chính trong RAM thay vì trên disk. Vì dữ liệu nằm trong bộ nhớ và cấu trúc lưu trữ được tối ưu cho truy cập trực tiếp, Redis trả lời các thao tác đọc/ghi cơ bản trong cỡ single-digit millisecond, thường là dưới một mili giây ở phần server tính từ lúc nhận lệnh.
Mô hình dữ liệu cốt lõi là key-value: mỗi giá trị gắn với một key dạng chuỗi. Nhưng "value" của Redis không chỉ là chuỗi — nó là nhiều data structure phong phú, mỗi loại có tập lệnh riêng được tối ưu:
- String: chuỗi/số/blob nhị phân — dùng cho cache giá trị, đếm.
- Hash: map field → value, hợp để lưu object.
- List: danh sách có thứ tự — làm queue/stack đơn giản.
- Set và Sorted Set: tập hợp, và tập hợp có điểm số (score) — nền tảng cho leaderboard.
- Stream, Bitmap, HyperLogLog, Geospatial: cho log/event, analytics xấp xỉ, dữ liệu vị trí.
Vài đặc điểm nền tảng (sẽ đào sâu ở các bài sau):
- Redis xử lý lệnh theo mô hình single-threaded cho phần thực thi lệnh, dựa trên một event loop — nhờ vậy mỗi lệnh chạy nguyên tử và dễ suy luận về thứ tự. Redis 7.x có I/O threads để tăng tốc phần đọc/ghi socket, nhưng việc thực thi logic lệnh vẫn tuần tự.
- Giao tiếp qua giao thức văn bản gọn nhẹ tên là RESP.
- Redis có cơ chế persistence (RDB snapshot và AOF) để không hoàn toàn mất dữ liệu khi restart, nhưng bản chất "nguồn nhanh" của nó nằm ở RAM.
Một cách phát biểu ngắn gọn: Redis là một dictionary cực nhanh, chạy qua mạng, biết nhiều kiểu dữ liệu hơn dictionary thông thường.
Memory vs Disk: Bảng So Sánh Latency
Để hiểu vì sao in-memory nhanh đến vậy, hãy nhìn các con số latency điển hình của phần cứng và hệ thống. Đây là những con số mang tính bậc độ lớn (order of magnitude) — không phải benchmark tuyệt đối, nhưng tỉ lệ giữa chúng mới là điều quan trọng:
| Thao tác | Latency điển hình | Quy đổi tương đối |
|---|---|---|
| Truy cập L1 cache (CPU) | ~1 ns | 1x (mốc gốc) |
| Truy cập RAM chính | ~100 ns | ~100x |
| Đọc tuần tự 1 MB từ RAM | ~3 µs | nghìn lần ns |
| Đọc ngẫu nhiên từ SSD | ~16 µs | ~16.000x so với L1 |
| Round-trip trong cùng datacenter | ~0,5 ms | ~500.000x |
| Một query DB điển hình (chạm disk/buffer) | ~1–10 ms | cỡ triệu lần ns |
| Đọc ngẫu nhiên từ HDD quay | ~2–10 ms | ~triệu lần ns |
| Round-trip mạng giữa hai vùng địa lý xa | ~50–150 ms | cỡ trăm triệu lần ns |
Đọc bảng này theo bậc độ lớn: RAM nhanh hơn disk khoảng 1.000 lần; một câu query DB tính bằng ms, trong khi một lần đọc Redis (đã ở trong RAM) tính bằng µs tại phía server, cộng thêm round-trip mạng trong datacenter cỡ vài chục đến vài trăm µs.
Vì sao in-memory nhanh?
- Không có cơ học quay/đầu đọc: RAM truy cập điện tử trực tiếp, không phải chờ đĩa quay hay đầu đọc dịch chuyển như HDD.
- Không qua tầng filesystem và block I/O: đọc dữ liệu là dereference con trỏ trong bộ nhớ, không phải syscall đọc block từ thiết bị.
- Cấu trúc dữ liệu tối ưu cho RAM: hash table, skiplist, listpack... được thiết kế để truy cập với chi phí hằng số hoặc gần hằng số.
- Tối giản pipeline xử lý: không parse SQL, không query plan, không transaction nặng cho mỗi
GET— chỉ tra key và trả về.
Cái giá phải trả đã rõ ngay ở đây: RAM nhỏ hơn và đắt hơn disk nhiều lần, nên ta không thể (và không nên) nhét mọi thứ vào RAM. Đây là tiền đề cho tư duy ở mục tiếp theo.
Tư Duy Latency-First Và Hot Path
Latency-first là cách thiết kế hệ thống lấy độ trễ làm trục chính, thay vì chỉ nghĩ tới "lưu ở đâu". Câu hỏi đầu tiên trở thành: đường đi của một request mất bao lâu, và đoạn nào tốn nhiều nhất?
Hot path là gì?
Hot path là đoạn code được thực thi rất thường xuyên và ảnh hưởng trực tiếp tới độ trễ mà người dùng cảm nhận — ví dụ render trang chủ, kiểm tra phiên đăng nhập, đọc feed, lấy tồn kho. Ngược lại, cold path là việc chạy hiếm hoặc chạy nền (báo cáo cuối ngày, gửi email, đồng bộ định kỳ).
Ý tưởng cốt lõi: đặt dữ liệu nóng nhất, được đọc nhiều nhất vào nơi gần CPU và nhanh nhất có thể trên hot path, còn để database lo phần source of truth và các truy vấn phức tạp ở cold path. Redis chính là lớp nhanh đặt trên hot path đó.
Một cách suy nghĩ thực dụng
- Xác định các thao tác đọc lặp lại nhiều nhất và dữ liệu của chúng có thay đổi thường xuyên không.
- Nếu dữ liệu đọc nhiều, thay đổi ít, và chấp nhận được một chút "cũ" (staleness) — đó là ứng viên tốt để đưa lên Redis.
- Đo trước khi tối ưu: biết p50/p95/p99 latency của từng đoạn để biết nên đặt cache ở đâu cho đáng.
Tư duy này sẽ quay lại xuyên suốt series, đặc biệt khi bàn về caching pattern và TTL.
Tổng Quan Các Bài Toán Redis Giải Quyết
Đây chỉ là bản đồ tổng quan để bạn thấy bức tranh lớn; mỗi mục sẽ có bài riêng đi sâu ở các module sau.
| Bài toán | Redis giúp thế nào | Data structure hay dùng |
|---|---|---|
| Caching | Lưu kết quả đọc nóng để né database trên hot path | String, Hash + TTL |
| Session store | Lưu phiên đăng nhập chia sẻ giữa nhiều instance backend | Hash + TTL |
| Rate limiting | Đếm số request theo cửa sổ thời gian, chặn lạm dụng | String (INCR) + EXPIRE |
| Distributed lock | Đảm bảo chỉ một worker xử lý một tài nguyên tại một thời điểm | String (SET NX) |
| Queue / job | Hàng đợi tác vụ giữa producer và consumer | List, Stream |
| Realtime / pub-sub | Phát thông điệp tới nhiều subscriber gần tức thời | Pub/Sub, Stream |
| Leaderboard | Xếp hạng theo điểm số, lấy top-N tức thì | Sorted Set |
| Analytics gần đúng | Đếm lượt unique, đếm cờ on/off ở quy mô lớn với ít bộ nhớ | HyperLogLog, Bitmap |
Điểm chung: tất cả đều là những thao tác cần nhanh, tần suất cao, hoặc cần chia sẻ trạng thái giữa nhiều process/server. Đó là vùng sở trường của một in-memory store chạy qua mạng.
Code Minh Hoạ: SET/GET Và Đo DB vs Cache
Trước hết, dựng một Redis cục bộ bằng Docker để chạy thử:
docker run --name redis-demo -p 6379:6379 -d redis:7-alpine
# kiểm tra nhanh bằng redis-cli
docker exec -it redis-demo redis-cli PING
# trả về: PONG
Ví dụ cơ bản: SET và GET
TypeScript với ioredis (cài bằng npm i ioredis):
import Redis from "ioredis";
const redis = new Redis({ host: "127.0.0.1", port: 6379 });
async function main(): Promise<void> {
// SET key value: lưu một chuỗi vào key "user:1:name"
await redis.set("user:1:name", "Canh");
// GET key: đọc lại giá trị
const name = await redis.get("user:1:name");
console.log(name); // "Canh"
// SET kèm TTL 60 giây (EX = expire seconds)
await redis.set("session:abc", "token-xyz", "EX", 60);
await redis.quit();
}
main();
Python với redis-py (cài bằng pip install redis):
import redis
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
# SET key value
r.set("user:1:name", "Canh")
# GET key
name = r.get("user:1:name")
print(name) # "Canh"
# SET kèm TTL 60 giây
r.set("session:abc", "token-xyz", ex=60)
Pattern cache-aside mini: đo thời gian DB vs cache
Đây mới là phần thể hiện vì sao Redis đáng giá. Ý tưởng cache-aside: khi cần dữ liệu, đọc Redis trước (cache); nếu trống (cache miss) thì đọc database, rồi ghi vào Redis kèm TTL cho lần sau. Ở đây ta giả lập database chậm bằng một hàm sleep để thấy rõ chênh lệch.
TypeScript:
import Redis from "ioredis";
const redis = new Redis();
// Giả lập một truy vấn DB chậm (vài ms)
async function queryProductFromDB(id: number): Promise<string> {
await new Promise((res) => setTimeout(res, 30)); // ~30ms
return JSON.stringify({ id, name: "Bàn phím cơ", price: 1290000 });
}
async function getProduct(id: number): Promise<string> {
const cacheKey = `product:${id}`;
// 1) Thử đọc từ cache
const cached = await redis.get(cacheKey);
if (cached !== null) return cached; // cache hit
// 2) Cache miss -> đọc DB rồi ghi cache với TTL 60s
const data = await queryProductFromDB(id);
await redis.set(cacheKey, data, "EX", 60);
return data;
}
async function main(): Promise<void> {
const id = 12345;
console.time("lần 1 (cache miss, đọc DB)");
await getProduct(id);
console.timeEnd("lần 1 (cache miss, đọc DB)"); // ~30ms+
console.time("lần 2 (cache hit, đọc Redis)");
await getProduct(id);
console.timeEnd("lần 2 (cache hit, đọc Redis)"); // dưới 1ms ở localhost
await redis.quit();
}
main();
Python:
import json
import time
import redis
r = redis.Redis(decode_responses=True)
def query_product_from_db(product_id: int) -> str:
time.sleep(0.03) # giả lập DB chậm ~30ms
return json.dumps({"id": product_id, "name": "Ban phim co", "price": 1290000})
def get_product(product_id: int) -> str:
cache_key = f"product:{product_id}"
# 1) Thử đọc cache
cached = r.get(cache_key)
if cached is not None:
return cached # cache hit
# 2) Cache miss -> đọc DB rồi ghi cache với TTL 60s
data = query_product_from_db(product_id)
r.set(cache_key, data, ex=60)
return data
if __name__ == "__main__":
pid = 12345
t0 = time.perf_counter()
get_product(pid)
print(f"lan 1 (cache miss, doc DB): {(time.perf_counter() - t0) * 1000:.2f} ms")
t1 = time.perf_counter()
get_product(pid)
print(f"lan 2 (cache hit, doc Redis): {(time.perf_counter() - t1) * 1000:.2f} ms")
Chạy thử, bạn sẽ thấy lần 1 mất khoảng 30 ms (phải đi qua "DB"), còn lần 2 chỉ tốn phần round-trip tới Redis — thường dưới 1 ms ở localhost. Đây là toàn bộ tinh thần của bài: cùng một kết quả, nhưng lần đọc từ Redis nhanh hơn nhiều bậc và không làm phiền database.
Trade-off Khi Dùng Redis
Không có bữa trưa miễn phí. Đổi lấy tốc độ, bạn chấp nhận một số ràng buộc:
- RAM đắt hơn disk: cùng dung lượng, RAM tốn chi phí cao hơn disk nhiều lần. Vì vậy Redis không phải nơi để chứa mọi dữ liệu — chỉ chứa phần nóng, có chọn lọc.
- Dữ liệu mang tính volatile: RAM mất khi mất điện hoặc process restart. Redis có persistence (RDB/AOF) nhưng vẫn có khả năng mất một phần dữ liệu gần nhất tuỳ cấu hình. Đừng coi cache là nơi lưu trữ duy nhất của dữ liệu quan trọng. (Chủ đề persistence sẽ có bài riêng.)
- Cần quản lý eviction và TTL: khi RAM đầy, Redis sẽ loại bỏ key theo chính sách eviction. Bạn phải chủ động đặt TTL và chọn policy phù hợp, nếu không dữ liệu cache có thể phình to hoặc bị xoá ngoài ý muốn.
- Vấn đề nhất quán cache vs DB: khi dữ liệu trong DB thay đổi, cache có thể bị "cũ" (stale). Bạn phải có chiến lược invalidation/cập nhật — đây là một trong những vấn đề khó nhất của caching.
- Thêm một thành phần để vận hành: Redis là một service nữa cần monitor, backup, bảo mật và xử lý khi sự cố. Phức tạp vận hành tăng lên.
Quan trọng nhất cần khắc cốt: Redis không thay thế database chính. Nó là lớp tăng tốc đặt cạnh database, không phải nơi giữ source of truth cho dữ liệu nghiệp vụ quan trọng.
Pitfalls Và Anti-patterns
Những lỗi tư duy hay gặp khi mới dùng Redis:
- Coi Redis như primary database: lưu dữ liệu nghiệp vụ quan trọng chỉ trong Redis mà không có nguồn bền vững nào khác. Khi Redis restart hoặc eviction, dữ liệu có thể biến mất. Redis là lớp tăng tốc, source of truth nên nằm ở database có durability.
- Không nghĩ tới eviction/persistence: chạy Redis với cấu hình mặc định, không đặt giới hạn bộ nhớ, không chọn policy. Khi RAM đầy, hành vi loại bỏ key có thể gây ra cache miss hàng loạt hoặc lỗi ghi.
- Cache mọi thứ vô tội vạ: cache cả dữ liệu hiếm khi đọc lại, hoặc dữ liệu thay đổi liên tục. Kết quả là tỉ lệ cache hit thấp, tốn RAM, và tăng nguy cơ phục vụ dữ liệu stale. Hãy cache có chọn lọc dựa trên dữ liệu thật về tần suất đọc.
- Quên đặt TTL: key không bao giờ hết hạn sẽ tích tụ và chiếm RAM mãi mãi. Trừ khi có lý do rõ ràng, dữ liệu cache nên có TTL.
- Không xử lý cache miss và lỗi Redis: code giả định Redis luôn sống và luôn có dữ liệu. Khi Redis lỗi hoặc cache miss đồng loạt, toàn bộ tải dồn xuống database (hiện tượng này gọi là cache stampede / thundering herd) và có thể làm sập database.
- Dùng key không có quy ước: đặt key tuỳ tiện khiến khó debug, dễ trùng và khó dọn dẹp. Nên dùng namespace dạng
resource:id:field, ví dụproduct:12345.
Mỗi pitfall ở trên đều sẽ được giải quyết bằng kỹ thuật cụ thể trong các bài sau (TTL, eviction policy, invalidation, stampede protection).
Tổng Kết, Quiz Và Bài Tiếp Theo
Tổng kết
- Database thuần gặp khó với read-heavy workload, đọc lặp lại, connection limit và traffic đột biến vì mỗi query đều tốn chi phí pipeline và có thể chạm disk (tính bằng
ms). - Redis là in-memory data store dạng key-value với nhiều data structure, trả lời trong cỡ single-digit millisecond nhờ giữ dữ liệu trong RAM và tối giản pipeline xử lý.
- RAM nhanh hơn disk khoảng ba bậc độ lớn; tư duy latency-first đặt dữ liệu nóng vào hot path để né database khi có thể.
- Redis giải quyết nhiều bài toán: caching, session, rate limiting, distributed lock, queue, pub/sub, leaderboard, analytics.
- Trade-off: RAM đắt, dữ liệu volatile, cần TTL/eviction/invalidation; Redis không thay thế database chính.
Quiz 5 câu
- Vì sao một dòng dữ liệu gần như tĩnh nhưng bị đọc hàng nghìn lần lại là gánh nặng cho database, dù dữ liệu đó nhỏ?
- Theo bảng so sánh, RAM nhanh hơn disk khoảng bao nhiêu bậc độ lớn, và đơn vị latency điển hình của một query DB là gì?
- "Hot path" nghĩa là gì, và vì sao nên ưu tiên đặt Redis vào hot path thay vì cold path?
- Mô tả các bước của pattern cache-aside khi xảy ra cache miss.
- Kể ba lý do vì sao không nên coi Redis là primary database.
Đáp án gợi ý
- Vì chi phí không nằm ở kích thước dữ liệu mà ở toàn bộ pipeline lặp lại cho mỗi query: parse, query plan, kiểm tra index, lấy lock, đọc page (có thể chạm disk), serialize kết quả — nhân với hàng nghìn lần và cộng thêm cạnh tranh kết nối/CPU dưới tải.
- Khoảng ba bậc độ lớn (RAM cỡ
ns–µscòn disk cỡms, chênh ~1.000 lần); một query DB điển hình tính bằngms(khoảng 1–10ms). - Hot path là đoạn code chạy rất thường xuyên và ảnh hưởng trực tiếp tới latency người dùng cảm nhận. Đặt Redis ở đây cho lợi ích lớn nhất vì nó tăng tốc đúng phần được gọi nhiều nhất; cold path chạy hiếm nên tối ưu ít đáng giá hơn.
- Đọc cache trước; nếu cache miss thì đọc database, sau đó ghi kết quả vào Redis kèm TTL rồi trả về cho client; các lần sau sẽ là cache hit cho tới khi key hết hạn hoặc bị invalidate.
- (1) Dữ liệu volatile, RAM mất khi restart và persistence vẫn có thể mất một phần dữ liệu gần nhất; (2) eviction có thể xoá key khi RAM đầy; (3) RAM đắt nên không phù hợp làm nơi lưu trữ bền vững toàn bộ dữ liệu. (Có thể bổ sung: thiếu các đảm bảo quan hệ/transaction phức tạp như RDBMS.)
Bài tiếp theo
Ở Bài 1 ta đã hiểu Redis sinh ra để giải quyết vấn đề gì và vì sao nó nhanh ở mức bề mặt. Bài 2 sẽ mở "nắp capo" để xem Redis chạy thế nào bên trong: mô hình event loop single-threaded xử lý lệnh, vai trò của I/O threads trong Redis 7.x, và giao thức RESP mà client/server dùng để nói chuyện. Hiểu được những thứ này, bạn sẽ giải thích được vì sao một lệnh chạy nguyên tử và vì sao một lệnh chậm có thể làm nghẽn cả server.
