Cache: Redis Internals
1. Redis là gì — và tại sao nó nhanh
Redis (Remote Dictionary Server) là in-memory data structure store. Điểm khác biệt so với Memcached: không chỉ là key-value string, mà còn hỗ trợ nhiều data structure phức tạp.
Tại sao Redis nhanh?
1. In-memory: không có disk I/O trên hot path
2. Single-threaded event loop: không có lock contention, no context switching overhead
3. Non-blocking I/O: epoll/kqueue multiplexing
4. Simple data structures: không có query planning overhead
5. Pre-allocated memory pools: ít malloc/free overhead
Benchmark thực tế:
GET/SET: ~100,000–1,000,000 ops/sec trên single instance
Latency: < 1ms ở p99 với network gần (same datacenter)
Memory: ~1 byte overhead per byte of data (rất hiệu quả)
Single-threaded model
flowchart TB N["Network requests"] --> E["epoll_wait() → readable sockets"] E --> R["Read command from socket"] R --> X["Execute command (single-threaded)\nGET key → dict lookup O(1)"] X --> W["Write response to socket"]
Network requests
│
▼
┌─────────────────────────────────────────┐
│ Event Loop (I/O thread) │
│ │
│ epoll_wait() ──► readable sockets │
│ │ │
│ ▼ │
│ Read command from socket │
│ │ │
│ ▼ │
│ Execute command (single-threaded) │
│ [GET key] → dict lookup → O(1) │
│ │ │
│ ▼ │
│ Write response to socket │
└─────────────────────────────────────────┘
Không cần lock vì chỉ có 1 thread xử lý commands.
Redis 6.0+ thêm I/O threads (đọc/ghi socket song song)
nhưng command execution vẫn single-threaded.
💡 Interview: "Redis single-threaded thì làm sao handle được 1M ops/sec?" — Vì bottleneck không phải CPU mà là network I/O. Single thread loại bỏ lock contention và context switching. CPU time để xử lý 1 GET command là ~microsecond — 1M ops/sec chỉ cần 1 CPU core.
2. Data Structures — Chọn đúng type tiết kiệm RAM gấp 10x
2.1 String
Type cơ bản nhất. Nhưng đừng nhầm: String trong Redis là binary-safe byte array, không phải chỉ text.
Use cases:
- Cache arbitrary data (JSON, protobuf, images)
- Counters (INCR, INCRBY — atomic)
- Distributed locks (SET key value NX PX timeout)
- Rate limiting counter
Limits: tối đa 512MB per key
// Counter với atomic increment
func IncrementPageView(ctx context.Context, rdb *redis.Client, pageID string) (int64, error) {
key := fmt.Sprintf("pageviews:%s", pageID)
count, err := rdb.Incr(ctx, key).Result()
if err != nil {
return 0, err
}
// Set expiry nếu là key mới tạo
if count == 1 {
rdb.Expire(ctx, key, 24*time.Hour)
}
return count, nil
}
// Distributed lock
func AcquireLock(ctx context.Context, rdb *redis.Client, resource string, ttl time.Duration) (bool, string, error) {
token := uuid.New().String()
key := fmt.Sprintf("lock:%s", resource)
// SET key token NX PX milliseconds
ok, err := rdb.SetNX(ctx, key, token, ttl).Result()
return ok, token, err
}
func ReleaseLock(ctx context.Context, rdb *redis.Client, resource, token string) error {
key := fmt.Sprintf("lock:%s", resource)
// Lua script để atomic check-and-delete
script := redis.NewScript(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`)
return script.Run(ctx, rdb, []string{key}, token).Err()
}
2.2 Hash
Field-value pairs trong một key. Tương tự struct hoặc map.
Use cases:
- User profile (id, name, email, age — thay vì JSON string)
- Product attributes
- Config objects
Memory advantage:
100 users, mỗi user 5 fields:
String (JSON): 100 keys × ~200 bytes = ~20KB overhead
Hash: 100 small hashes → Redis dùng ziplist (compact)
Tiết kiệm 60-80% memory so với JSON string!
Lý do: Redis dùng "ziplist" encoding cho Hash nhỏ (< 128 fields, < 64 bytes/value)
— lưu như contiguous memory thay vì hash table → cực compact
// Lưu user dùng Hash
func SetUser(ctx context.Context, rdb *redis.Client, user User) error {
key := fmt.Sprintf("user:%d", user.ID)
return rdb.HSet(ctx, key,
"name", user.Name,
"email", user.Email,
"age", user.Age,
"created_at", user.CreatedAt.Unix(),
).Err()
}
func GetUser(ctx context.Context, rdb *redis.Client, userID int64) (*User, error) {
key := fmt.Sprintf("user:%d", userID)
vals, err := rdb.HGetAll(ctx, key).Result()
if err != nil || len(vals) == 0 {
return nil, err
}
age, _ := strconv.Atoi(vals["age"])
ts, _ := strconv.ParseInt(vals["created_at"], 10, 64)
return &User{
ID: userID,
Name: vals["name"],
Email: vals["email"],
Age: age,
CreatedAt: time.Unix(ts, 0),
}, nil
}
// Update một field mà không cần đọc toàn bộ struct
func UpdateUserName(ctx context.Context, rdb *redis.Client, userID int64, name string) error {
key := fmt.Sprintf("user:%d", userID)
return rdb.HSet(ctx, key, "name", name).Err()
}
2.3 List
Doubly-linked list. Hỗ trợ push/pop từ cả 2 đầu — O(1).
Use cases:
- Message queues (LPUSH + BRPOP)
- Activity feed (LPUSH + LRANGE 0 99 → lấy 100 item mới nhất)
- Job queue đơn giản
- Recent history (last N items)
Operations:
LPUSH key val → push vào head (O(1))
RPUSH key val → push vào tail (O(1))
LPOP key → pop từ head (O(1))
RPOP key → pop từ tail (O(1))
LRANGE key 0 9 → lấy items (O(N))
BRPOP key 30 → blocking pop, timeout 30s
// Simple job queue
func EnqueueJob(ctx context.Context, rdb *redis.Client, job Job) error {
data, err := json.Marshal(job)
if err != nil {
return err
}
return rdb.LPush(ctx, "jobs:pending", data).Err()
}
func DequeueJob(ctx context.Context, rdb *redis.Client) (*Job, error) {
// Blocking pop với timeout 30s — không busy-wait
result, err := rdb.BRPop(ctx, 30*time.Second, "jobs:pending").Result()
if err == redis.Nil {
return nil, nil // timeout, no job
}
if err != nil {
return nil, err
}
var job Job
err = json.Unmarshal([]byte(result[1]), &job)
return &job, err
}
// Activity feed: chỉ giữ 100 items mới nhất
func AddActivity(ctx context.Context, rdb *redis.Client, userID int64, activity string) error {
key := fmt.Sprintf("feed:%d", userID)
pipe := rdb.Pipeline()
pipe.LPush(ctx, key, activity)
pipe.LTrim(ctx, key, 0, 99) // chỉ giữ 100 items đầu
_, err := pipe.Exec(ctx)
return err
}
2.4 Set
Unordered collection of unique strings. O(1) add/remove/check membership.
Use cases:
- Tags, labels (user interests, product categories)
- Unique visitors set
- Friend list / follower list (với set intersection)
- Bloom filter alternative cho small sets
Set operations:
SADD, SREM, SISMEMBER — O(1)
SMEMBERS — O(N) ← cẩn thận với set lớn
SUNION / SINTER / SDIFF — O(N+M) — common friends!
// Common friends / Mutual connections
func GetMutualFriends(ctx context.Context, rdb *redis.Client, userA, userB int64) ([]string, error) {
keyA := fmt.Sprintf("friends:%d", userA)
keyB := fmt.Sprintf("friends:%d", userB)
return rdb.SInter(ctx, keyA, keyB).Result()
}
// Check if user has seen a notification (deduplication)
func MarkNotificationSeen(ctx context.Context, rdb *redis.Client, userID, notifID int64) error {
key := fmt.Sprintf("seen_notifs:%d", userID)
pipe := rdb.Pipeline()
pipe.SAdd(ctx, key, notifID)
pipe.Expire(ctx, key, 7*24*time.Hour) // keep 7 days
_, err := pipe.Exec(ctx)
return err
}
2.5 Sorted Set (ZSet)
Set với mỗi member có một score (float64). Tự động sắp xếp theo score. Đây là Redis data structure "đắt tiền" nhất và cũng hữu ích nhất.
Use cases:
- Leaderboard (score = điểm, lấy top N)
- Rate limiting (score = timestamp)
- Priority queue (score = priority)
- Expiring members (score = expiry timestamp)
- Search autocomplete (score = relevance)
Complexity:
ZADD O(log N)
ZRANK O(log N)
ZRANGE O(log N + M) ← M là số items trả về
ZRANGEBYSCORE O(log N + M)
// Leaderboard
func UpdateScore(ctx context.Context, rdb *redis.Client, gameID string, userID int64, score float64) error {
key := fmt.Sprintf("leaderboard:%s", gameID)
return rdb.ZAdd(ctx, key, redis.Z{
Score: score,
Member: userID,
}).Err()
}
func GetTopPlayers(ctx context.Context, rdb *redis.Client, gameID string, n int) ([]redis.Z, error) {
key := fmt.Sprintf("leaderboard:%s", gameID)
// ZREVRANGE: lấy từ cao xuống thấp
return rdb.ZRevRangeWithScores(ctx, key, 0, int64(n-1)).Result()
}
func GetPlayerRank(ctx context.Context, rdb *redis.Client, gameID string, userID int64) (int64, error) {
key := fmt.Sprintf("leaderboard:%s", gameID)
rank, err := rdb.ZRevRank(ctx, key, userID).Result()
return rank + 1, err // ZRevRank là 0-indexed
}
// Sliding window rate limiter dùng ZSet
func IsRateLimited(ctx context.Context, rdb *redis.Client, userID string, limit int, window time.Duration) (bool, error) {
key := fmt.Sprintf("ratelimit:%s", userID)
now := time.Now()
windowStart := now.Add(-window).UnixNano()
pipe := rdb.Pipeline()
// Xóa entries ngoài window
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
// Đếm entries trong window
countCmd := pipe.ZCard(ctx, key)
// Thêm request hiện tại
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now.UnixNano()), Member: now.UnixNano()})
pipe.Expire(ctx, key, window)
_, err := pipe.Exec(ctx)
if err != nil {
return false, err
}
return countCmd.Val() >= int64(limit), nil
}
2.6 Stream
Redis 5.0+. Append-only log, tương tự Kafka partition nhưng trong memory.
Use cases:
- Event streaming giữa microservices
- Audit log
- Real-time notifications
- IoT sensor data
So sánh với List:
List: đơn giản, nhưng LPOP mất data sau khi pop
Stream: nhiều consumers có thể đọc cùng message, message persist,
consumer groups, acknowledgment
So sánh với Kafka:
Redis Stream: in-memory, fast, single node hoặc cluster
Kafka: disk-based, durable, horizontal scaling, replay lâu dài
// Producer
func PublishEvent(ctx context.Context, rdb *redis.Client, event Event) error {
data, _ := json.Marshal(event)
return rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "events:orders",
Values: map[string]interface{}{
"type": event.Type,
"payload": data,
},
}).Err()
}
// Consumer với Consumer Group
func ConsumeEvents(ctx context.Context, rdb *redis.Client) error {
// Tạo consumer group (idempotent)
rdb.XGroupCreateMkStream(ctx, "events:orders", "processor", "$")
for {
msgs, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "processor",
Consumer: "worker-1",
Streams: []string{"events:orders", ">"},
Count: 10,
Block: 5 * time.Second,
}).Result()
if err == redis.Nil {
continue // timeout, không có message
}
for _, msg := range msgs[0].Messages {
if err := processMessage(msg); err == nil {
rdb.XAck(ctx, "events:orders", "processor", msg.ID)
}
}
}
}
3. Persistence — RDB vs AOF
Redis là in-memory → crash mất hết. Persistence là cơ chế lưu data ra disk để recovery.
RDB (Redis Database Backup): AOF (Append-Only File):
───────────────────────────────── ─────────────────────────────────
Point-in-time snapshot Log mọi write command
Binaryformat (compact) Text format (human readable)
Fork child process để snapshot Append mỗi write vào file
Ít tốn I/O Tốn I/O hơn
Restart fast Restart chậm hơn (replay commands)
Có thể mất data đến lần backup gần Mất tối đa 1 giây data (fsync)
nhất (e.g., mất 5 phút nếu config (với fsync everysec)
BGSAVE mỗi 5 phút)
flowchart TB
subgraph RDB["RDB flow"]
P["Redis parent process"] --> F["fork() (copy-on-write)"]
F --> C["Child process writes dump.rdb"]
P --> S["Parent vẫn serve requests"]
end
subgraph AOF["AOF flow"]
W["Mỗi write command"] --> A["Append vào AOF file"]
A --> R["Restart: replay AOF để rebuild state"]
A --> RW["Periodic AOF rewrite (compact)"]
endRDB flow:
┌─────────────────────────────────┐
│ Redis (parent process) │
│ đang serve requests │
│ │
│ fork() ─────────────────────► child process
│ (copy-on-write memory) │ writes dump.rdb
│ tiếp tục serve requests │ (atomic rename when done)
└─────────────────────────────────┘
Ưu điểm: fork() + copy-on-write → parent không bị block
Nhược điểm: fork() tốn thời gian nếu Redis dùng nhiều RAM
(e.g., 10GB Redis → fork có thể mất vài giây)
AOF flow:
Mỗi write command → append vào AOF file
┌──────────────────────────────┐
│ AOF file (append-only) │
│ SET user:1 {"name":"Alice"} │
│ INCR counter:page:home │
│ DEL session:abc123 │
│ ... │
└──────────────────────────────┘
Restart: Redis replay toàn bộ file để rebuild state
AOF rewrite: định kỳ compact AOF (e.g., INCR 1000 lần → SET counter 1000)
Fsync policies (AOF)
appendfsync always: fsync sau mỗi write → durability tốt nhất, chậm nhất
appendfsync everysec: fsync mỗi giây → mất tối đa 1 giây data (recommended)
appendfsync no: để OS quyết định → nhanh nhất, có thể mất nhiều data
Nên dùng gì?
Chỉ cache, mất data ok: Tắt persistence (save "" trong config)
Muốn fast recovery: RDB only (BGSAVE mỗi 5-15 phút)
Muốn minimal data loss: AOF everysec
Muốn tốt nhất cả hai: RDB + AOF (Redis dùng AOF để recover, RDB để backup)
💡 Interview: "Redis là cache hay database?" — Có thể là cả hai, tuỳ configuration. Khi persistence tắt: pure cache. Khi AOF + RDB bật: data store với durability guarantees. Nhưng không nên dùng Redis thay thế PostgreSQL cho primary data vì thiếu ACID, query flexibility.
4. Memory Eviction Policies
Khi Redis hết memory (maxmemory), nó cần quyết định drop key nào.
Policy Behavior
──────────────────────────────────────────────────────────────────
noeviction Reject writes khi full (trả error) — default
allkeys-lru Evict least recently used trong toàn bộ keys
volatile-lru Evict LRU chỉ từ keys có TTL set
allkeys-lfu Evict least frequently used (Redis 4.0+)
volatile-lfu Evict LFU chỉ từ keys có TTL
allkeys-random Evict ngẫu nhiên
volatile-random Evict ngẫu nhiên từ keys có TTL
volatile-ttl Evict key có TTL ngắn nhất (sắp expire)
LRU vs LFU:
LRU (Least Recently Used):
Evict key lâu nhất không được access
Vấn đề: key được access 1 triệu lần nhưng không access trong 1 phút
→ có thể bị evict, trong khi key mới tạo (ít dùng) lại giữ
LFU (Least Frequently Used):
Evict key ít được access nhất theo thời gian
Redis LFU dùng Morris counter (probabilistic, xấp xỉ frequency)
Tốt hơn LRU cho workload có clear hot/cold data pattern
Redis LRU implementation:
Không dùng true LRU (quá tốn memory cho linked list)
Thay vào đó: sample N random keys (default 5), evict LRU nhất trong sample
→ Approximation LRU với O(1) memory overhead
// Config Redis maxmemory và eviction policy
// redis.conf:
// maxmemory 2gb
// maxmemory-policy allkeys-lru
// Kiểm tra memory usage
func GetMemoryInfo(ctx context.Context, rdb *redis.Client) (map[string]string, error) {
return rdb.Info(ctx, "memory").Result() // "memory" section
}
5. Redis Cluster vs Sentinel
flowchart TB
subgraph S["Redis Sentinel architecture"]
S1["Sentinel 1"] --> M["Master (read+write)"]
S2["Sentinel 2"] --> M
S3["Sentinel 3"] --> M
M --> R1["Replica 1 (read)"]
M --> R2["Replica 2 (read)"]
end
subgraph C["Redis Cluster architecture (16384 slots)"]
M1["Master 1\nslots 0-5460"] --> R1a["Replica 1a"]
M2["Master 2\nslots 5461-10922"] --> R2a["Replica 2a"]
M3["Master 3\nslots 10923-16383"] --> R3a["Replica 3a"]
endRedis Sentinel: Redis Cluster:
───────────────────────────────── ─────────────────────────────────
High Availability (HA) HA + Horizontal Scaling
1 master + N replicas Multiple master nodes
Sentinel monitors master Data sharded across masters
Auto failover nếu master die Auto failover per shard
Client vẫn connect đến master Client connect đến any node
Single master → giới hạn throughput N masters → N× throughput
Max data = RAM của 1 node Max data = Tổng RAM của N nodes
Dễ setup hơn Phức tạp hơn
Sentinel architecture:
┌──────────────────────────────────────────────────┐
│ Sentinel 1 Sentinel 2 Sentinel 3 │
│ (quorum: 2/3 agree = failover) │
└────────┬──────────────┬──────────────────────────┘
│ │
▼ ▼
┌───────────────┐ ┌──────────────────────────────┐
│ Master │ │ Replica 1 Replica 2 │
│ (read+write) │ │ (read only) (read only) │
└───────────────┘ └──────────────────────────────┘
Cluster architecture:
┌──────────────────────────────────────────────────────┐
│ 16384 hash slots chia đều cho N master nodes │
│ │
│ Master 1 Master 2 Master 3 │
│ slots: 0-5460 slots: 5461- slots: 10923- │
│ 10922 16383 │
│ Replica 1a Replica 2a Replica 3a │
└──────────────────────────────────────────────────────┘
key → CRC16(key) % 16384 → tìm node chứa slot đó
Khi nào dùng gì?
Dùng Sentinel khi:
- Data fit vào 1 node RAM (< ~100GB)
- Cần HA nhưng không cần scale write throughput
- Đơn giản hơn để operate
Dùng Cluster khi:
- Data > single node RAM (e.g., 1TB leaderboard)
- Cần > 1M ops/sec write throughput
- Muốn horizontal scaling cho cả reads và writes
💡 Interview: "Làm sao Redis Cluster handle cross-slot operations?" — Không handle! Multi-key operations (MGET, transaction) chỉ work nếu tất cả keys thuộc cùng slot. Giải pháp: dùng hash tags
{user:123}:profilevà{user:123}:settings→ cùng slot vì cùng{user:123}.
6. Pub/Sub vs Streams
Pub/Sub: Streams:
───────────────────────────────── ─────────────────────────────────
Fire-and-forget Persistent log
Message lost nếu subscriber offline Message persist (configurable)
Không có message ID Mỗi message có unique ID
Không replay được Replay từ bất kỳ offset
Không có consumer group Consumer groups + ACK
Fanout tự nhiên (1 pub → N subs) Cần config consumer groups
Real-time only Both real-time + catch-up
Use Pub/Sub khi:
- Real-time notifications (chat, live dashboard)
- Không cần delivery guarantee
- Subscriber luôn online
Use Streams khi:
- Event sourcing
- Audit log
- Cần at-least-once delivery
- Cần replay (debug, reprocessing)
// Pub/Sub: subscribe và publish
func Subscribe(ctx context.Context, rdb *redis.Client, channel string) {
pubsub := rdb.Subscribe(ctx, channel)
defer pubsub.Close()
ch := pubsub.Channel()
for msg := range ch {
fmt.Printf("Received: %s\n", msg.Payload)
}
}
func Publish(ctx context.Context, rdb *redis.Client, channel, message string) error {
return rdb.Publish(ctx, channel, message).Err()
}
7. Pipeline và Transaction
// Pipeline: gom nhiều commands, send trong 1 network round trip
func BatchSetUsers(ctx context.Context, rdb *redis.Client, users []User) error {
pipe := rdb.Pipeline()
for _, u := range users {
data, _ := json.Marshal(u)
pipe.Set(ctx, fmt.Sprintf("user:%d", u.ID), data, 5*time.Minute)
}
_, err := pipe.Exec(ctx)
return err
// 1 round trip thay vì len(users) round trips
// Với 1000 users: giảm từ ~100ms xuống ~1ms (same datacenter)
}
// MULTI/EXEC Transaction: atomic, nhưng không rollback như DB
func TransferPoints(ctx context.Context, rdb *redis.Client, fromID, toID int64, points int) error {
fromKey := fmt.Sprintf("points:%d", fromID)
toKey := fmt.Sprintf("points:%d", toID)
// WATCH + MULTI/EXEC = optimistic locking
return rdb.Watch(ctx, func(tx *redis.Tx) error {
fromPoints, err := tx.Get(ctx, fromKey).Int()
if err != nil {
return err
}
if fromPoints < points {
return errors.New("insufficient points")
}
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.DecrBy(ctx, fromKey, int64(points))
pipe.IncrBy(ctx, toKey, int64(points))
return nil
})
return err // nếu WATCH key bị thay đổi bởi concurrent tx → retry
}, fromKey)
}