Giải Thích Source Code Redis: Khám Phá Repository GitHub

Giới Thiệu
Bạn đã dùng Redis. Bạn biết các lệnh — SET, GET, LPUSH, ZADD. Nhưng bạn đã bao giờ tự hỏi điều gì thực sự xảy ra khi Redis thực thi một trong những lệnh đó? Code C trông như thế nào?
Source code Redis nằm tại github.com/redis/redis và đây là một trong những codebase C dễ đọc, có comment tốt nhất trong open source. Đọc nó không chỉ dạy bạn về Redis — nó dạy bạn các pattern lập trình hệ thống được dùng trong database, game engine và hệ điều hành.
Bài viết này đi qua năm file source code quan trọng và rút ra những ý tưởng giải thích tại sao Redis lại nhanh đến vậy.
Bạn Sẽ Học Được:
✅ Cách Redis biểu diễn mọi giá trị bằng một struct chung (robj)
✅ Cách SET và GET thực sự hoạt động trong C
✅ Tại sao counter Redis dùng gần như không có bộ nhớ (integer encoding)
✅ Cách Redis list tự động chuyển đổi giữa hai cấu trúc dữ liệu
✅ Thủ thuật incremental rehashing giúp Redis có độ trễ ổn định
✅ Cách event loop đơn luồng xử lý 100.000+ kết nối
Điều Kiện Tiên Quyết:
- Kiến thức Redis cơ bản (khuyến nghị đọc Learning Redis Fundamentals)
- Thoải mái khi đọc code ở bất kỳ ngôn ngữ nào (cú pháp C sẽ được giải thích)
- Tò mò về cách database hoạt động nội tại
Cấu Trúc Repository
Clone repository và bạn sẽ thấy cấu trúc này:
git clone https://github.com/redis/redis.git
cd redis/src
ls *.c | head -20Thư mục src/ chứa ~100 file C. Đây là các file chúng ta sẽ nghiên cứu:
| File | Công Dụng |
|---|---|
server.h | Định nghĩa các cấu trúc dữ liệu cốt lõi |
t_string.c | Tất cả lệnh string (GET, SET, INCR…) |
t_list.c | Tất cả lệnh list (LPUSH, RPOP, BLPOP…) |
dict.c | Hash table dùng ở khắp nơi |
ae.c | Event loop — engine của Redis |
server.h — Bản Thiết Kế
Mọi file Redis đều include server.h. Đây là header trung tâm định nghĩa các struct mà mọi thứ khác phụ thuộc vào.
robj — Một Kiểu Để Cai Quản Tất Cả
Trong Redis, mọi giá trị bạn lưu trữ — dù là string, list, sorted set hay hash — đều được bọc trong một struct duy nhất gọi là robj (Redis Object):
// Đơn giản hóa từ server.h
typedef struct redisObject {
unsigned type:4; // OBJ_STRING, OBJ_LIST, OBJ_SET...
unsigned encoding:4; // Cách lưu trữ nội tại
unsigned lru:LRU_BITS; // Thời gian truy cập cuối cho eviction
int refcount; // Đếm tham chiếu để quản lý bộ nhớ
void *ptr; // Con trỏ đến dữ liệu thực
} robj;Trường type chứa một trong các hằng số này:
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3 // Sorted Set
#define OBJ_HASH 4
#define OBJ_STREAM 6Thiết kế này rất thanh lịch. Các lệnh như TYPE key chỉ cần đọc robj->type. EXPIRE hoạt động với mọi kiểu vì nó được đặt trên key, không phải trên giá trị. Hàm checkType() kiểm tra rằng bạn đang dùng đúng lệnh với đúng kiểu dữ liệu.
Struct Database
Redis có thể giữ tới 16 logical database. Mỗi cái là một redisDb:
typedef struct redisDb {
kvstore *keys; // Các cặp key-value thực tế
kvstore *expires; // Theo dõi TTL cho từng key
dict *blocking_keys; // Key mà client đang BLPOP
int id; // Số database (SELECT 0, SELECT 1...)
} redisDb;Khi bạn chạy EXPIRE user:1 3600, Redis thêm một entry vào expires ánh xạ user:1 → Unix timestamp. Một timer nền kiểm tra dict này và xóa key hết hạn một cách lazy (khi truy cập) hoặc chủ động (mỗi 100ms).
Struct Server
Tất cả trạng thái toàn cục nằm trong một struct redisServer. Một vài trường đáng chú ý:
struct redisServer {
pid_t pid; // Process ID
redisDb *db; // Mảng 16 database
dict *commands; // Tên lệnh → hàm xử lý
list *clients; // Tất cả client đang kết nối
aeEventLoop *el; // Event loop
size_t stat_numcommands; // Tổng lệnh đã xử lý
};Dict commands là cách Redis định tuyến "SET" đến hàm setCommand(). Redis phân tích tên lệnh từ input của client, tra cứu trong dict này và gọi handler.
t_string.c — Cách GET và SET Hoạt Động
File này cài đặt mọi lệnh string. Hãy đọc GET trước — nó đơn giản đến mức tuyệt vời.
Đọc Lệnh GET
int getGenericCommand(client *c) {
kvobj *o;
// 1. Tra cứu key (c->argv[1] là tên key)
if ((o = lookupKeyReadOrReply(c, c->argv[1],
shared.null[c->resp])) == NULL)
return C_OK; // Không tìm thấy key — đã trả lời null
// 2. Đảm bảo đây là string, không phải list hay hash
if (checkType(c, o, OBJ_STRING)) return C_ERR;
// 3. Gửi giá trị về cho client
addReplyBulk(c, o);
return C_OK;
}
void getCommand(client *c) {
getGenericCommand(c);
}Ba bước: tra cứu, kiểm tra kiểu, trả lời. Hàm lookupKeyReadOrReply xử lý kiểm tra dict expires nội tại — nếu key đã hết hạn, nó xóa key ngay tại đây và trả về NULL.
Lệnh SET Phức Tạp Hơn
SET có nhiều tùy chọn: EX, PX, NX, XX, GET, KEEPTTL. Nhưng một chi tiết cài đặt đặc biệt thú vị: thời gian hết hạn tương đối được chuyển đổi thành timestamp tuyệt đối trước khi replication.
// Khi bạn chạy: SET key value EX 60
// Redis nội tại chuyển thành: SET key value PXAT <unix_ms + 60000>
// trước khi gửi đến replicaTại sao? Vì các replica server có thể nhận lệnh muộn hơn một chút. Nếu Redis replication EX 60 theo nghĩa đen, thời gian hết hạn của replica sẽ lệch đúng bằng độ trễ replication. Chuyển sang timestamp tuyệt đối (EXAT/PXAT) đảm bảo tất cả replica hết hạn key vào đúng cùng một thời điểm.
Integer Encoding — Không Cần Cấp Phát Bộ Nhớ
Đây là một trong những micro-optimization thông minh nhất trong Redis. Khi bạn lưu một counter:
SET visits 0
INCR visits # Bây giờ là 1
INCR visits # Bây giờ là 2Redis không lưu "1" hay "2" dưới dạng string trong bộ nhớ. Thay vào đó, nó đóng gói số nguyên trực tiếp vào trường con trỏ của robj:
// Thay vì cấp phát string "1001" trên heap...
// Redis lưu số trực tiếp trong slot con trỏ:
o->ptr = (void*)((long)value);
o->encoding = OBJ_ENCODING_INT;Con trỏ C là 8 byte trên hệ thống 64-bit. Một long cũng là 8 byte. Vậy Redis tái sử dụng slot con trỏ để lưu chính số nguyên. Không có heap allocation. Không có string parsing. Chỉ là một bitcast.
Đây là lý do tại sao INCR trên counter nhanh hơn so với set key string — và tại sao Redis counter dùng một lượng bộ nhớ nhỏ và không đổi bất kể giá trị là bao nhiêu.
Giới hạn: nếu số nguyên không vừa trong C long (xấp xỉ ±9,2 × 10¹⁸), Redis dùng string encoding.
t_list.c — List Thông Minh Hơn Bạn Nghĩ
Cài đặt list tiết lộ một trong những triết lý thiết kế cốt lõi của Redis: tự động sử dụng cấu trúc tiết kiệm bộ nhớ nhất dựa trên kích thước dữ liệu thực tế.
Hai Cấu Trúc Nền
Redis list không phải lúc nào cũng được cài đặt như linked list. Nội tại, chúng chọn giữa hai biểu diễn:
Listpack là định dạng bộ nhớ liền kề, compact. Hãy tưởng tượng đóng gói các phần tử chặt chẽ trong một byte array với tiền tố độ dài — không có con trỏ, không có overhead, thân thiện với cache CPU.
Quicklist là doubly-linked list trong đó mỗi node chứa một listpack. Nó được dùng cho các list lớn hơn khi cần O(1) push/pop ở cả hai đầu.
Cài Đặt Push Cho Thấy Cả Hai
void listTypePush(robj *subject, robj *value, int where) {
if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
int pos = (where == LIST_HEAD) ?
QUICKLIST_HEAD : QUICKLIST_TAIL;
quicklistPush(subject->ptr, value->ptr,
sdslen(value->ptr), pos);
} else if (subject->encoding == OBJ_ENCODING_LISTPACK) {
subject->ptr = (where == LIST_HEAD) ?
lpPrepend(subject->ptr, ...) :
lpAppend(subject->ptr, ...);
}
}Cùng một lệnh LPUSH dispatch khác nhau dựa trên encoding hiện tại. Caller không bao giờ thấy điều này.
Tự Động Thăng Cấp với Hysteresis
Khi list listpack phát triển vượt list-max-listpack-size (mặc định: 128 phần tử), Redis gọi listTypeConvert để thăng cấp lên quicklist. Điều này tự động và vô hình với người dùng.
Còn co lại thì sao? Redis không giảm cấp ngay ở 128 — nó dùng ngưỡng thấp hơn. Hysteresis này ngăn cấu trúc bị thrashing giữa các định dạng khi phần tử dao động gần ranh giới:
Thăng cấp lên quicklist: khi phần tử > 128
Giảm cấp về listpack: khi phần tử < 64 (xấp xỉ)Pattern này — thăng cấp ở N, giảm cấp ở N/2 — xuất hiện trong nhiều hệ thống nhạy cảm với hiệu suất (CPU branch predictor, thuật toán thay đổi kích thước buffer) vì cùng lý do: tránh chi phí chuyển đổi liên tục ở ranh giới.
dict.c — Hash Table Cung Cấp Năng Lượng Cho Mọi Thứ
dict của Redis là cấu trúc dữ liệu quan trọng nhất trong codebase. Nó được tái sử dụng ở khắp nơi: lưu toàn bộ key của mỗi database (redisDb->keys), lưu command registry (ánh xạ tên lệnh → hàm xử lý), cài đặt kiểu dữ liệu Set, lưu trữ các field của Hash, và nhiều hơn nữa.
Cấu Trúc: Chained Hash Table
struct dictEntry {
struct dictEntry *next; // Entry tiếp theo trong collision chain
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d; // Không cần allocation cho số
} v;
};Collision được giải quyết bằng chaining — mỗi bucket là một linked list các entry. Union trong trường giá trị là cùng trick tiết kiệm không gian chúng ta đã thấy trong robj: nếu giá trị của bạn là số, không cần heap allocation.
Chỉ số bucket dùng bitwise AND thay vì modulo:
// Thay vì: index = hash % table_size (phép chia chậm)
// Redis dùng: index = hash & (table_size - 1) (AND nhanh)Điều này chỉ hoạt động khi table_size là lũy thừa của hai — và Redis luôn cấp phát table theo lũy thừa của hai chính vì lý do này.
Incremental Rehashing — Viên Ngọc Quý
Khi hash table quá đầy (load factor > 1), nó cần resize. Cách tiếp cận ngây thơ: cấp phát bảng mới lớn hơn, sao chép tất cả entry, xóa bảng cũ. Với database có hàng triệu key, điều này sẽ gây ra spike độ trễ nhìn thấy được — có thể hàng trăm millisecond bị chặn.
Giải pháp của Redis là incremental rehashing: duy trì hai bảng đồng thời và di chuyển entry dần dần qua nhiều thao tác.
// dict giữ hai bảng trong khi rehashing
struct dict {
dictEntry **ht_table[2]; // [0] = cũ, [1] = mới
long ht_used[2]; // Số entry trong mỗi bảng
long rehashidx; // Tiến trình di chuyển hiện tại (chỉ số bucket)
};Hàm di chuyển tiến hành vài bucket mỗi lần:
int dictRehash(dict *d, int n) {
while (n-- && d->ht_used[0] != 0) {
// Di chuyển entry từ bucket table[0] cũ sang table[1] mới
rehashEntriesInBucketAtIndex(d, d->rehashidx);
d->rehashidx++; // Tiến cursor
}
return !dictCheckRehashingCompleted(d);
}Trong khi rehashing:
- Tra cứu kiểm tra table[0] trước, rồi table[1]
- Ghi đi thẳng vào table[1]
- Timer nền (
serverCron, chạy mỗi 100ms) gọidictRehashđể tăng tốc di chuyển khi rảnh
Kết quả: không có thao tác đơn lẻ nào phải trả đủ chi phí di chuyển. Công việc được phân tán qua hàng nghìn request thông thường, mỗi cái chỉ trả một chi phí tăng thêm nhỏ.
ae.c — Event Loop Làm Cho Mọi Thứ Hoạt Động
Redis nổi tiếng là đơn luồng để thực thi lệnh. Một thread. Không có lock. 100.000+ kết nối. Làm thế nào?
Câu trả lời là ae.c — event loop 500 dòng cài đặt reactor pattern.
Hai Loại Event
typedef struct aeFileEvent {
int mask; // AE_READABLE | AE_WRITABLE
aeFileProc *rfileProc; // Gọi khi socket có thể đọc
aeFileProc *wfileProc; // Gọi khi socket có thể ghi
void *clientData;
} aeFileEvent;
typedef struct aeTimeEvent {
long long id;
monotime when; // Khi nào kích hoạt (microseconds)
aeTimeProc *timeProc; // Hàm callback
struct aeTimeEvent *next;
} aeTimeEvent;File event kích hoạt khi socket mạng có dữ liệu sẵn sàng để đọc (client gửi lệnh) hoặc sẵn sàng để ghi (buffer phản hồi có thể được flush). Time event kích hoạt vào thời điểm tương lai đã lên lịch — dùng cho serverCron, kiểm tra hết hạn và thu thập stats.
Vòng Lặp Chính
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop,
AE_ALL_EVENTS |
AE_CALL_BEFORE_SLEEP |
AE_CALL_AFTER_SLEEP);
}
}aeProcessEvents là nơi phép màu xảy ra:
- Tính toán thời gian còn lại đến time event tiếp theo kích hoạt
- Gọi OS multiplexer (
epoll_waittrên Linux,keventtrên macOS) với timeout đó — chặn cho đến khi socket sẵn sàng HOẶC timeout hết hạn - Với mỗi socket sẵn sàng, gọi handler của nó (đọc lệnh / ghi phản hồi)
- Kiểm tra time event và kích hoạt bất kỳ cái nào đã hết hạn
Trừu Tượng Hóa Nền Tảng
Redis hỗ trợ bốn backend multiplexing sau một interface chung:
// ae.c chọn backend tốt nhất có sẵn khi compile:
#ifdef HAVE_EVPORT
#include "ae_evport.c" // Solaris
#elif defined(HAVE_EPOLL)
#include "ae_epoll.c" // Linux (mặc định)
#elif defined(HAVE_KQUEUE)
#include "ae_kqueue.c" // macOS/BSD
#else
#include "ae_select.c" // Fallback (bất kỳ hệ thống POSIX)
#endifPhần còn lại của ae.c không bao giờ gọi epoll trực tiếp — nó gọi aeApiPoll, mà mỗi backend cài đặt. Đây là strategy pattern trong C.
Tại Sao Đơn Luồng Hoạt Động Tốt
Mô hình event loop tránh mọi đau đầu của multithreading:
- Không mutex — chỉ một thread truy cập cấu trúc dữ liệu
- Không deadlock — không có lock để acquire
- Không overhead context switch — OS không phải juggling các thread
Bottleneck cho hầu hết workload Redis không phải CPU — mà là network I/O. OS kernel xử lý việc theo dõi hàng nghìn file descriptor đồng thời. Redis chỉ phản ứng với những gì sẵn sàng. Một handler tại một thời điểm, không chặn, không locking.
Lưu ý: Redis 6.0+ đã thêm I/O thread để đọc/ghi network data song song, nhưng thực thi lệnh vẫn là đơn luồng. Điều này bảo tồn sự đơn giản trong khi cải thiện throughput thô trên workload băng thông rất cao.
Kết Hợp Tất Cả Lại
Hãy theo dõi điều gì xảy ra khi bạn chạy SET counter 0 rồi INCR counter:
Mọi layer đóng góp vào tốc độ:
ae.cthức dậy chỉ vì socket sẵn sàng (không polling)dict.ctìm thấy key trong O(1) với bitwise ANDt_string.ctăng số mà không cấp phát bộ nhớ- Phản hồi là string nhỏ 4 byte
Bài Học Chính Từ Source Code
Đọc source code Redis tiết lộ một tập hợp nguyên tắc thiết kế nhất quán:
1. Lưu số là số, không phải string
Integer encoding đóng gói giá trị vào slot con trỏ, loại bỏ heap allocation cho use case counter phổ biến nhất.
2. Dùng cấu trúc phù hợp cho kích thước
Listpack cho list nhỏ, quicklist cho list lớn. Encoding bị ẩn với người dùng nhưng tiết kiệm đáng kể bộ nhớ cho workload điển hình mà hầu hết list nhỏ.
3. Phân tán công việc tốn kém theo thời gian
Incremental rehashing đảm bảo không có thao tác đơn lẻ nào phải trả đủ chi phí resize hash table với hàng triệu entry.
4. Một thread, OS-level multiplexing
Event loop scale đến hàng nghìn kết nối mà không có thread hay lock, vì việc chờ network I/O được xử lý bởi kernel, không phải Redis thread.
5. Mọi thứ đều là robj
Một wrapper đa hình duy nhất làm cho việc thêm kiểu dữ liệu mới, eviction policy và lệnh mới trở nên đơn giản mà không cần viết lại infrastructure.
Cách Khám Phá Code Tự Mình
# Clone repository
git clone https://github.com/redis/redis.git
cd redis
# Bắt đầu với entry point
vim src/server.c # Main function, server startup, command table
# Tìm bất kỳ command handler nào
grep -n "void setCommand" src/t_string.c
grep -n "void lrangeCommand" src/t_list.c
grep -n "void zaddCommand" src/t_zset.c
# Tìm kiếm nơi một khái niệm được cài đặt
grep -rn "OBJ_ENCODING_INT" src/
grep -rn "dictRehash" src/Thứ tự đọc gợi ý:
server.h— Hiểu các kiểu cốt lõi (robj, client, redisServer)t_string.c— Cài đặt lệnh đơn giản nhấtdict.c— Hash table dùng ở khắp nơiae.c— Event loopt_list.c,t_zset.c— Kiểu dữ liệu phức tạp hơn
Codebase Redis dùng style nhất quán, tên hàm rõ ràng và comment xuất sắc. Đừng bị đe dọa bởi C — hầu hết logic thú vị đọc như pseudocode khi bạn qua được các khai báo kiểu.
Tổng Kết
Redis nhanh không phải bằng phép màu, mà bằng những tối ưu hóa có chủ đích, nhiều lớp ở mọi tầng:
✅ robj cung cấp wrapper thống nhất làm cho tất cả kiểu dữ liệu có thể kết hợp
✅ Integer encoding loại bỏ heap allocation cho giá trị counter
✅ List tự động chọn lưu trữ compact hay linked dựa trên kích thước
✅ Incremental rehashing phân tán chi phí resize qua hàng nghìn request
✅ Event loop xử lý concurrency lớn với không thread và không lock
Lần tới khi bạn chạy INCR counter hay LPUSH queue item, bạn sẽ biết chính xác điều gì đang xảy ra trong code C bên dưới.
Tài Nguyên Bổ Sung
Redis Source Code:
- github.com/redis/redis — Repository chính thức
- redis/src/server.h — Kiểu cốt lõi
- redis/src/dict.c — Cài đặt hash table
Bài Viết Liên Quan:
- Learning Redis: Hướng Dẫn Hoàn Chỉnh Cho Người Mới Bắt Đầu — Lệnh và khái niệm Redis
- Spring Boot Caching với Redis — Tích hợp Redis thực tế
- Load Balancing Giải Thích — Bối cảnh distributed systems
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.