Tuần 10 — Redis Mastery

“Redis không phải ‘cache nhanh’. Redis là một in-memory data structure server. Coi nó như cache = bạn dùng 5% năng lực. Master nó = bạn unlock leaderboard, rate limiter, pub/sub, geo, stream, lock — tất cả trong 1 tool.”

Tags: database redis valkey cache data-structures operations Thời lượng: 7 ngày (4-6h/ngày) Prerequisites: Tuan-01-DB-Internals-Refresh (in-memory concepts) Liên quan: Tuan-06-Cache-Strategy (SD course) · Tuan-13-Search-Engines-ES


1. Context & Why

1.1 Redis trong 2024-2026: lịch sử + thực trạng

timeline
    title Redis evolution
    2009 : Redis 1.0 by Salvatore Sanfilippo
    2013 : Redis Cluster
    2015 : Redis 3 - sentinel HA
    2018 : Redis 5 - Streams
    2020 : Redis 6 - SSL, multi-threaded I/O
    2022 : Redis 7 - Functions, Sharded Pub/Sub
    2024 : Redis 7.4 license change to RSALv2/SSPL
    2024 : Valkey fork (BSD-3-Clause, hosted by Linux Foundation, governed by TSC)
    2025 : Valkey 8 GA - many Redis 7+ users migrate

License change (March 2024): Redis Inc moved Redis to RSALv2+SSPL — not OSS by OSI definition. Linux Foundation forked → Valkey (BSD-3). Most cloud providers (AWS, Google, Oracle) adopted Valkey.

Pattern 2024-2026: Code 99% compatible. Choose Valkey for OSS purity, Redis for Redis Stack features (RediSearch, RedisJSON).

1.2 Vai trò trong stack

graph LR
    Client[Client] --> LB[Load Balancer]
    LB --> App[App Server]
    App -->|read-through| R[(Redis<br/>cache)]
    App -->|cache miss| PG[(Postgres)]
    App -->|rate limit| R
    App -->|session| R
    App -->|queue| R
    App -->|leaderboard| R
    App -->|pub/sub| R
    R -.eviction.-> Trash[/dev/null]

Redis use cases:

  1. Cache — query result, page fragment, API response
  2. Session store — user session, JWT denylist
  3. Rate limiter — token bucket, sliding window
  4. Leaderboard — sorted set
  5. Queue — list (legacy) hoặc Stream (modern)
  6. Pub/Sub — real-time notifications
  7. Distributed lock — Redlock pattern
  8. Counter — atomic INCR
  9. Geospatial — nearby search
  10. Real-time analytics — HyperLogLog, Bitmap, TimeSeries

1.3 Mục tiêu tuần

  • Master 10 data structures, biết encoding internal
  • Persistence: RDB vs AOF — pick đúng cho từng use case
  • HA: Sentinel vs Cluster
  • Cache patterns: aside, through, behind, refresh-ahead
  • Atomic patterns: MULTI/EXEC, Lua, WATCH
  • Anti-patterns: KEYS, FLUSHALL trong production, không TTL
  • Streams (Redis 5+) cho event processing
  • Redis Cluster topology + resharding
  • Tools: redis-cli, redis-benchmark, redis-stat

1.4 Tham chiếu


2. Data Structures Deep Dive

2.1 String — most basic

SET user:42:name "Alice"
GET user:42:name
SET counter 0
INCR counter        # atomic +1
INCRBY counter 10
DECR counter
APPEND log:today "new entry\n"
GETRANGE user:42:name 0 4
SETEX session:abc 3600 "data"  # SET with TTL 1h
SETNX lock:resource "owner"     # SET if not exists
MSET k1 v1 k2 v2 k3 v3          # multi-set atomic
MGET k1 k2 k3

Internal encoding (Redis 7):

  • Short strings (<44 chars): embstr (embedded with header)
  • Long strings: raw (separate alloc)
  • Integers: int (8-byte signed)

Atomic ops: INCR, DECR, INCRBY, GETSET, INCRBYFLOAT.

2.2 Hash — Field-value map

HSET user:42 name "Alice" email "[email protected]" age 30
HGET user:42 name
HGETALL user:42
HMGET user:42 name email
HINCRBY user:42 age 1
HDEL user:42 age
HEXISTS user:42 phone
HKEYS user:42
HLEN user:42

Encoding:

  • Small (<128 fields, values <64 bytes): listpack (Redis 7+, replaces ziplist)
  • Large: hashtable

Use case: user profile, config, structured cache. Like row in DB.

Pattern: instead of SET user:42:name, SET user:42:email, etc. → HSET user:42 name ... email .... One key, less memory, atomic per-user ops.

2.3 List — Doubly linked list (or listpack)

LPUSH queue "job1"
LPUSH queue "job2"
RPUSH queue "job3"
LRANGE queue 0 -1     # all
LPOP queue            # FIFO out from RPUSH
RPOP queue
BLPOP queue 0         # blocking pop
LLEN queue
LINSERT queue BEFORE "job2" "newjob"

Encoding:

  • Small: listpack
  • Large: quicklist (linked list of listpacks)

Use cases:

  • Queue (legacy — Redis Stream is modern)
  • Recent items (LPUSH + LTRIM 0 N)
  • Activity feed
# Recent 100 events
LPUSH events:user:42 "event_data"
LTRIM events:user:42 0 99

2.4 Set — Unique unordered

SADD tags:post:1 "rust" "database" "performance"
SISMEMBER tags:post:1 "rust"      # exists?
SMEMBERS tags:post:1
SCARD tags:post:1                  # size
SREM tags:post:1 "performance"
SUNION tags:post:1 tags:post:2     # union
SINTER tags:post:1 tags:post:2     # intersect
SDIFF tags:post:1 tags:post:2      # diff
SRANDMEMBER tags:post:1 3         # random 3
SPOP tags:post:1                   # pop random

Encoding:

  • Small int-only: intset
  • Small mixed: listpack (Redis 7+)
  • Large: hashtable

Use cases:

  • Tags, categories
  • Unique visitors
  • Friend lists
  • Allowlist/denylist

2.5 Sorted Set (ZSet) — Score-ordered set

ZADD leaderboard 1500 "alice"
ZADD leaderboard 1800 "bob"
ZADD leaderboard 1200 "carol"
ZRANGE leaderboard 0 -1 WITHSCORES                    # asc
ZREVRANGE leaderboard 0 9 WITHSCORES                  # top 10
ZRANGEBYSCORE leaderboard 1000 2000                    # range
ZRANK leaderboard "alice"                              # position
ZSCORE leaderboard "alice"
ZINCRBY leaderboard 100 "alice"                        # alice +100
ZREM leaderboard "carol"
ZCOUNT leaderboard 1500 2000
ZADD leaderboard XX GT 1900 "alice"                    # only if > current

Encoding:

  • Small: listpack
  • Large: skiplist + hashtable

Use cases:

  • Leaderboard (game score, view count)
  • Time-series queue (score = timestamp)
  • Priority queue (score = priority)
  • Rate limiter (score = timestamp, window)
  • Pagination (score = sort key, member = entity)

2.6 Stream — Append-only log

XADD events * action "login" user "alice"             # * = auto ID
XADD events 1700000000000-0 action "logout" user "alice"
XLEN events
XRANGE events - +
XREAD COUNT 100 STREAMS events 0                       # read from start
 
# Consumer groups (multi-consumer at-least-once)
XGROUP CREATE events workers $ MKSTREAM
XREADGROUP GROUP workers worker1 COUNT 10 STREAMS events >
XACK events workers <id>                               # confirm processed
XPENDING events workers                                # see unacked
XCLAIM events workers worker2 60000 <id>              # claim from dead worker

Use cases:

  • Event sourcing
  • At-least-once message delivery
  • Activity feed
  • Real-time analytics ingestion

Streams replaced Lists as Redis-native queue mechanism (Redis 5+).

2.7 HyperLogLog — Cardinality estimation

PFADD visitors:2026-05-16 "ip1" "ip2" "ip3" "ip1"
PFCOUNT visitors:2026-05-16                            # ~3 (probabilistic)
PFMERGE visitors:total visitors:2026-05-16 visitors:2026-05-15
PFCOUNT visitors:total

12KB fixed memory regardless of cardinality. Error ~0.81%.

Use cases:

  • Unique visitors per day
  • Distinct events
  • Unique search terms

2.8 Bitmap — Bit manipulation

SETBIT user:42:active:2026-05 16 1                   # day 16 = active
GETBIT user:42:active:2026-05 16
BITCOUNT user:42:active:2026-05                       # active days
BITOP AND result user:42:active:2026-05 user:43:active:2026-05

Use cases:

  • User activity tracking
  • Feature flag per user
  • A/B test bucketing
  • Permission bitmap

2.9 Geospatial — Built on ZSet

GEOADD locations -122.4194 37.7749 "san_francisco"
GEOADD locations -73.9857 40.7484 "new_york"
GEOSEARCH locations FROMLONLAT -122.4 37.7 BYRADIUS 100 km ASC COUNT 10
GEODIST locations "san_francisco" "new_york" km
GEOPOS locations "san_francisco"

Use cases:

  • Nearby search
  • Driver matching (Uber-style)
  • Store locator

Note: full-blown geo workload → PostGIS more powerful. Redis Geo for simple within-radius.

2.10 Time series (Redis Stack)

RedisTimeSeries module:

TS.CREATE temperature RETENTION 86400000 LABELS sensor 1
TS.ADD temperature * 22.5
TS.RANGE temperature - + AGGREGATION avg 60000        # avg per minute
TS.MRANGE - + AGGREGATION avg 60000 FILTER sensor=1

Niche but useful for sensor data, metrics.


3. Memory Model

3.1 Encoding optimization

Redis automatically picks encoding based on data. Settings:

CONFIG SET hash-max-listpack-entries 128
CONFIG SET hash-max-listpack-value 64
CONFIG SET zset-max-listpack-entries 128
CONFIG SET set-max-listpack-entries 128

Smaller listpack thresholds → faster lookup but more memory. Defaults reasonable.

3.2 Per-key overhead

Approximate:

  • Each key adds ~50 bytes overhead (hash table entry)
  • Plus TTL: ~50 bytes
  • Plus encoding-specific

→ 1M small keys × 100 bytes overhead = 100MB just for keys.

Tip: use Hash to group related fields under one key:

  • SET user:42:name, SET user:42:email, … → 1MB overhead for 10K users with 10 fields
  • HSET user:42 name ... email ... → 50KB overhead

3.3 Memory limit & eviction

CONFIG SET maxmemory 8gb
CONFIG SET maxmemory-policy allkeys-lru

Policies:

  • noeviction (default) — return error on write when full
  • allkeys-lru — evict LRU across all keys
  • volatile-lru — evict LRU among TTL keys
  • allkeys-lfu — least frequently used (Redis 4+)
  • volatile-lfu
  • allkeys-random
  • volatile-random
  • volatile-ttl — shortest TTL first

Pattern 2024:

  • Cache use case: allkeys-lfu
  • Mixed (cache + durable data with TTL): volatile-lru
  • Database use case: noeviction (you’d rather error than lose data)

3.4 Monitor memory

INFO memory
# used_memory_human, used_memory_peak_human, mem_fragmentation_ratio

Fragmentation ratio:

  • 1.0-1.5 OK
  • 1.5 → defragment via CONFIG SET activedefrag yes

  • <1.0 → swap! Bad. Add RAM or reduce maxmemory.

3.5 redis-cli —bigkeys, —memkeys

redis-cli --bigkeys              # find biggest keys per data type
redis-cli --memkeys --memkeys-samples 1000   # memory by sampling
redis-cli memory usage key:name  # bytes used by specific key

4. Persistence

4.1 RDB — Point-in-time snapshot

save 3600 1     # every 1h if >=1 change
save 300 100    # every 5min if >=100 changes
save 60 10000   # every 1min if >=10000 changes

Mechanism:

  1. Postgres-style fork() → child process
  2. Child writes snapshot to dump.rdb
  3. Parent continues serving (copy-on-write)

Pros:

  • Compact file
  • Fast recovery (single file load)
  • Periodic backup

Cons:

  • Lose data since last snapshot (minutes)
  • fork() heavy on large datasets

4.2 AOF — Append-Only File

Every write → append to log. Replay on restart.

appendonly yes
appendfsync always       # safest, slowest
appendfsync everysec     # default, good balance
appendfsync no           # OS decides (~30s window)

Pros:

  • Durable (<1s data loss with everysec)
  • Easier to recover partial
  • Human-readable (mostly)

Cons:

  • Larger file
  • Slower restart (replay log)
  • Rewrite (compaction) needed periodically

4.3 Combination

Best practice 2024: both enabled.

  • RDB for backup, fast recovery baseline
  • AOF for durability

Redis 7+ multi-part AOF: combines RDB base + AOF tail → fast recovery + low data loss.

4.4 Replication != Backup

Replication propagates corruption. If app deletes wrong data, replica deletes too. → Take RDB/AOF snapshot, store offsite, test restore.


5. High Availability

5.1 Sentinel — automatic failover

graph TB
    Sentinel1[Sentinel 1]
    Sentinel2[Sentinel 2]
    Sentinel3[Sentinel 3]
    Master[(Redis Master)]
    Replica1[(Replica 1)]
    Replica2[(Replica 2)]

    Sentinel1 -.monitor.-> Master
    Sentinel2 -.monitor.-> Master
    Sentinel3 -.monitor.-> Master
    Sentinel1 -.monitor.-> Replica1
    Sentinel1 -.monitor.-> Replica2

    Master --> Replica1
    Master --> Replica2

    Client[App Client] -.discover via Sentinel.-> Sentinel1
    Client --> Master

Setup:

  • 1 master + N replicas (replicaof master_host 6379)
  • 3+ Sentinel nodes (odd, for quorum)
  • Client uses Sentinel SDK to discover master

Sentinel detects master down → promotes a replica → updates clients. RTO ~30s typical.

5.2 Cluster — sharded HA

graph TB
    subgraph "Shard 1: hash slots 0-5461"
        M1[Master 1]
        R1a[Replica 1a]
        R1b[Replica 1b]
        M1 --> R1a
        M1 --> R1b
    end
    subgraph "Shard 2: hash slots 5462-10922"
        M2[Master 2]
        R2a[Replica 2a]
        M2 --> R2a
    end
    subgraph "Shard 3: hash slots 10923-16383"
        M3[Master 3]
        R3a[Replica 3a]
        M3 --> R3a
    end

    Client --> M1
    Client --> M2
    Client --> M3

16384 hash slots. Each master owns range. Key → CRC16 % 16384 → slot → master.

Setup:

redis-cli --cluster create node1:6379 node2:6379 node3:6379 \
    node4:6379 node5:6379 node6:6379 --cluster-replicas 1

5.3 Cluster operations

# Check cluster status
redis-cli -c -h node1 CLUSTER NODES
redis-cli -c -h node1 CLUSTER INFO
 
# Reshard (move slots)
redis-cli --cluster reshard node1:6379
 
# Add node
redis-cli --cluster add-node new:6379 existing:6379
 
# Remove node
redis-cli --cluster del-node existing:6379 <node_id>

5.4 Multi-key operations + hashtags

Cluster forbids multi-key ops across shards. Solution: hashtags.

# Without hashtag: keys may go different shards
SET user:42 ... ; SET user:43 ...
 
# With hashtag {tag}: same hash slot
SET {user}:42 ... ; SET {user}:43 ...
MGET {user}:42 {user}:43           # works - same slot

Use sparingly — hashtag = manual sharding, can lead to hot shard.

5.5 Cluster vs Sentinel decision

SentinelCluster
HA
Sharding
Setup complexityLowerHigher
Client supportUniversalMost modern clients
Multi-key opsAllOnly same hashtag
Throughput limitSingle masterScales horizontally
Use caseSmall-medium, single-shardLarge, write-heavy

6. Caching Patterns

6.1 Cache-aside (Lazy loading)

def get_user(id):
    cached = redis.get(f"user:{id}")
    if cached:
        return json.loads(cached)
    user = db.query("SELECT * FROM users WHERE id = %s", id)
    redis.setex(f"user:{id}", 300, json.dumps(user))
    return user
  • Pros: only cache what’s needed
  • Cons: cache miss → 2 roundtrips; stale data possible

6.2 Write-through

def update_user(id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", data, id)
    redis.setex(f"user:{id}", 300, json.dumps(data))
  • Pros: cache always fresh
  • Cons: write latency higher; cache stores unused data

6.3 Write-behind (Write-back)

def update_user(id, data):
    redis.setex(f"user:{id}", 300, json.dumps(data))
    queue.publish("user_updated", {"id": id, "data": data})
    # Worker processes queue, writes to DB async
  • Pros: fast writes
  • Cons: data loss risk if Redis crashes; complexity

6.4 Cache invalidation

3 strategies:

  • TTL — set expiry, accept staleness
  • Explicit DELETE on update — risk dual-write inconsistency
  • Listen to CDC — Postgres changes → invalidate cache via Debezium

6.5 Cache stampede prevention

Many users request same expired key → 1000s hit DB simultaneously.

Locking pattern:

def get_user(id):
    cached = redis.get(f"user:{id}")
    if cached:
        return json.loads(cached)
 
    # Try acquire lock
    lock_acquired = redis.set(f"lock:user:{id}", "1", ex=10, nx=True)
    if lock_acquired:
        try:
            user = db.query(...)
            redis.setex(f"user:{id}", 300, json.dumps(user))
            return user
        finally:
            redis.delete(f"lock:user:{id}")
    else:
        # Wait for other process to populate
        time.sleep(0.05)
        return get_user(id)  # retry

Alternative: stale-while-revalidate, probabilistic early expiration.

6.6 Hot key problem

Single key receiving massive traffic (e.g., trending product viewed millions/sec) → bottleneck on shard hosting that key.

Solutions:

  • Replicate to multiple keys: product:42:shard:0, product:42:shard:1, … randomize on read
  • Local cache (multi-tier): app-level cache → Redis → DB
  • Pre-aggregate / read replicas

7. Atomic Patterns

7.1 INCR for counters

INCR page:views
INCR page:views:2026-05-16
HINCRBY user:42 visits 1

Atomic, lock-free. Single-shard fast.

7.2 SETNX for distributed lock

SET lock:resource owner123 NX PX 30000
# NX = only set if not exists
# PX = expire in 30s
 
# Release - safe (only own lock)
EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 lock:resource owner123

Redlock: multi-node lock algorithm. Acquire from majority of N independent Redis nodes. Aphyr’s Jepsen has criticized — not 100% safe under all assumptions. For most apps OK.

7.3 MULTI/EXEC transactions

MULTI
SET key1 val1
INCR counter
LPUSH queue item
EXEC

All queued, then executed atomically. Compare and swap with WATCH:

while True:
    redis.watch("balance:42")
    balance = int(redis.get("balance:42"))
    if balance < amount:
        redis.unwatch()
        raise InsufficientFunds
 
    pipe = redis.pipeline()
    pipe.multi()
    pipe.decrby("balance:42", amount)
    pipe.lpush("transactions:42", str(amount))
    try:
        pipe.execute()
        break  # success
    except WatchError:
        continue  # someone modified, retry

7.4 Lua scripting

EVAL "
    local current = tonumber(redis.call('GET', KEYS[1]) or '0')
    if current < tonumber(ARGV[1]) then
        return -1
    end
    redis.call('DECRBY', KEYS[1], ARGV[1])
    return current - tonumber(ARGV[1])
" 1 balance:42 100

Lua script atomic (no other commands interleave). Useful for:

  • Complex atomic operations
  • Reducing roundtrips
  • Conditional logic

Redis 7+ added Functions (server-stored Lua) for organized scripts.

7.5 Pipelining

Not atomic, just batched (reduces roundtrips):

pipe = redis.pipeline()
for i in range(1000):
    pipe.set(f"key:{i}", i)
pipe.execute()
# 1 roundtrip vs 1000

Vs MULTI/EXEC:

  • Pipeline: just batch commands, no atomicity
  • MULTI/EXEC: atomic transaction (no interleave)

8. Patterns by Use Case

8.1 Rate Limiter — token bucket via Lua

EVAL "
    local key = KEYS[1]
    local now = tonumber(ARGV[1])
    local refill_rate = tonumber(ARGV[2])
    local max_tokens = tonumber(ARGV[3])
    local cost = tonumber(ARGV[4])
 
    local data = redis.call('HMGET', key, 'tokens', 'last_refill')
    local tokens = tonumber(data[1] or max_tokens)
    local last_refill = tonumber(data[2] or now)
 
    local elapsed = now - last_refill
    tokens = math.min(max_tokens, tokens + elapsed * refill_rate)
 
    if tokens < cost then
        redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
        redis.call('EXPIRE', key, 3600)
        return 0
    end
 
    tokens = tokens - cost
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
    redis.call('EXPIRE', key, 3600)
    return 1
" 1 ratelimit:user:42 1700000000 10 100 1
-- Refill 10/sec, max 100, cost 1

8.2 Sliding window rate limiter

ZADD requests:user:42 <timestamp> <unique_id>
ZREMRANGEBYSCORE requests:user:42 0 <timestamp_minus_window>
ZCARD requests:user:42
# Compare with limit, allow/deny

8.3 Leaderboard

ZADD leaderboard:global 1500 alice
ZADD leaderboard:weekly:2026-w20 100 alice
 
# User rank
ZREVRANK leaderboard:global alice
 
# Top 10
ZREVRANGE leaderboard:global 0 9 WITHSCORES
 
# Around user (10 above + 10 below)
ZREVRANGEBYSCORE leaderboard:global +inf -inf LIMIT (rank-10) 21

8.4 Queue with priorities

ZADD jobs <priority_score> <job_id>
ZPOPMIN jobs                       # pop lowest score (highest priority if score = priority)

Or use Streams for at-least-once delivery.

8.5 Session store

HMSET session:abc123 user_id 42 created 1700000000
EXPIRE session:abc123 3600
HGET session:abc123 user_id        # validate

8.6 Caching aggregate (Top N items)

# Top 10 viewed products
ZINCRBY top_products 1 "product:42"
ZREVRANGE top_products 0 9 WITHSCORES

8.7 Distributed lock fencing token

INCR lock:resource:counter         # fencing token
# Use token to verify ordering at downstream

8.8 Job deduplication

SADD jobs:processed_today <job_id>
SISMEMBER jobs:processed_today <job_id>
EXPIRE jobs:processed_today 86400

9. Streams Deep Dive

9.1 Why Streams over List

Lists:

  • LPUSH/RPOP — simple FIFO
  • BLPOP blocking
  • ❌ Once popped, gone
  • ❌ No consumer groups
  • ❌ No replay

Streams:

  • Persistent log (like Kafka)
  • Consumer groups (workers split load)
  • At-least-once delivery
  • Replay any time
  • Better for event-driven architecture

9.2 Consumer group

XGROUP CREATE events workers $ MKSTREAM
 
# Worker loop
while True:
    XREADGROUP GROUP workers worker1 COUNT 10 STREAMS events >
    # Process each message
    XACK events workers <id>
 
# Dead worker recovery - claim pending
XPENDING events workers IDLE 60000  # idle >60s
XCLAIM events workers worker2 60000 <id>

9.3 Trim stream

XTRIM events MAXLEN 1000           # keep latest 1000
XTRIM events MINID 1700000000000   # delete older than timestamp
XADD events MAXLEN 1000 * data hello  # auto-trim on add

9.4 Use cases

  • Activity feed with rewind
  • Event sourcing light
  • Worker job distribution with at-least-once
  • Real-time analytics ingest

For full Kafka semantics → use Kafka, not Streams. Streams good for single-cluster, lower throughput.


10. Operations & Tuning

10.1 Persistence config trade-offs

WorkloadPersistence config
Pure cache (data recoverable from DB)No AOF, optional RDB hourly
Session storeAOF everysec, RDB daily
Counter / atomic stateAOF everysec, RDB hourly
Queue (durable)AOF always or everysec
Database roleAOF + RDB combined

10.2 Slow log

CONFIG SET slowlog-log-slower-than 10000   # 10ms
SLOWLOG GET 10
SLOWLOG RESET

Find slow commands. KEYS scan, large MGET, etc.

10.3 Latency monitor

CONFIG SET latency-monitor-threshold 100   # ms
LATENCY LATEST
LATENCY HISTORY event

Reasons latency spike: fork (RDB save), AOF rewrite, expire bursts, network.

10.4 Common config tune

maxmemory 16gb
maxmemory-policy allkeys-lfu
 
tcp-keepalive 60
timeout 0                          # disable idle timeout
 
# Persistence
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite yes      # don't fsync during AOF rewrite (latency safety)
 
# Snapshotting
save 3600 1
save 300 100
 
# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
 
# Latency monitor
latency-monitor-threshold 100
 
# Active defrag (Redis 4+)
activedefrag yes
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100

10.5 Backup

# Force RDB save
redis-cli BGSAVE
 
# Copy out
cp /var/lib/redis/dump.rdb /backup/
 
# Or stream from running
redis-cli --rdb /backup/dump.rdb

For Cluster: per-node, ensure all shards captured.

10.6 Monitor with INFO

redis-cli INFO server      # version, uptime
redis-cli INFO clients     # connections
redis-cli INFO memory      # memory stats
redis-cli INFO stats       # commands processed, hit/miss
redis-cli INFO replication # master/replica status
redis-cli INFO persistence # last RDB, AOF status
redis-cli INFO commandstats # per-command stats

10.7 Prometheus exporter

oliver006/redis_exporter:

docker run -d -p 9121:9121 \
    -e REDIS_ADDR=redis:6379 \
    oliver006/redis_exporter

Scrape to Prometheus. Grafana community dashboards available.


11. Anti-patterns

PatternWhy badFix
KEYS * in productionO(N), blocks RedisSCAN (incremental)
FLUSHALL accidentallyWipe all dataRename command in config, or remove access
No TTL on cache keysMemory grows foreverAlways SETEX or expire policy
Large value (1MB+)Slow op, network burstSplit / store in DB
LRANGE 0 -1 on huge listSlow + memoryLRANGE with limit
Many small keys vs HashWasted overheadHSET grouping
Pub/Sub for critical messagesNo durability, no replayUse Streams
Synchronous client in hot pathBlocks app threadPipelining or async client
Run on default port no authPublic internet attackrequirepass, bind localhost, network isolation
Cache without invalidation strategyStale dataTTL + explicit invalidation
Cache stampedeDB hit by 1000s simultaneouslyLock-based fetch
Hot keySingle shard bottleneckSharded keys, multi-tier cache
Use Redis as primary DBMemory cost, no joinsUse proper DB

12. Redis Stack + Modules

12.1 RediSearch

Full-text search inside Redis.

FT.CREATE idx:products SCHEMA name TEXT price NUMERIC tags TAG SORTABLE
FT.SEARCH idx:products "iPhone" LIMIT 0 10
FT.SEARCH idx:products "@price:[100 500] @tags:{rust}"

Faster than Elasticsearch for in-memory data. Use for products catalog, autocomplete.

12.2 RedisJSON

Native JSON with path operations.

JSON.SET user:42 $ '{"name":"Alice","age":30}'
JSON.GET user:42 $.name
JSON.SET user:42 $.age 31
JSON.NUMINCRBY user:42 $.age 1

12.3 RedisBloom

Bloom filter + Cuckoo filter + Top-K + Count-Min Sketch.

BF.RESERVE seen 0.01 1000000       # 1% error, 1M items
BF.ADD seen "user42"
BF.EXISTS seen "user42"             # probably yes

12.4 RedisGraph (deprecated 2024)

Cypher-style graph. Discontinued by Redis Inc.

12.5 RedisTimeSeries

Already covered section 2.10.


13. Valkey — fork status 2024-2026

Code-compatible with Redis 7.2. Maintained by Linux Foundation, AWS, Google, Oracle, Snap, etc.

Use cases:

  • AWS ElastiCache → Valkey option
  • Google Memorystore → Valkey
  • Self-host: 99% compat, drop-in
  • Redis Stack modules NOT available on Valkey

Decision 2024-2026:

  • Need Redis modules (Search, JSON, etc) → stick Redis or buy Redis Cloud
  • Pure OSS preference → Valkey
  • Existing Redis < 7.4 → no urgent migrate, but plan

14. Lab

14.1 Day 1: Data structures

docker run -d -p 6379:6379 redis:7
 
redis-cli
# Practice each data structure: String, Hash, List, Set, ZSet, Stream, HLL, Bitmap, Geo
# Make notes on common commands

14.2 Day 2: Memory

# Generate 1M keys with String
for i in {1..1000000}; do redis-cli SET "key:$i" "value$i" > /dev/null; done
redis-cli INFO memory | grep used_memory_human
 
# Same data in Hash
redis-cli FLUSHALL
for i in {1..1000000}; do redis-cli HSET "users" "user:$i" "value$i" > /dev/null; done
# Compare memory

14.3 Day 3: Persistence experiment

Configure AOF + RDB. Generate writes. Force kill Redis. Restart. Compare data loss.

14.4 Day 4: Cluster setup

Use docker-compose with 6 nodes (3 masters + 3 replicas). redis-cli --cluster create. Test resharding.

14.5 Day 5: Patterns

Implement 3 patterns:

  1. Rate limiter (Lua script)
  2. Leaderboard with daily reset
  3. Distributed lock with fencing

14.6 Day 6: Cache patterns

Build a cache-aside + stampede protection layer in Python/Node. Load test with 1000 concurrent users.

14.7 Day 7: Streams worker

Multi-consumer worker pool processing stream messages. Implement worker failure recovery (XCLAIM).


15. Self-check

  1. 10 data structures Redis + 1 use case mỗi cái?
  2. RDB vs AOF — pick khi nào? Combine?
  3. Sentinel vs Cluster — khác nhau, pick khi nào?
  4. Hashtag là gì? Trade-off?
  5. Cache stampede — pattern phòng ngừa?
  6. Hot key problem — 3 solutions?
  7. Streams vs List vs Pub/Sub — pick khi nào?
  8. INCR atomic — vì sao đặc biệt với Redis?
  9. Lua script vs MULTI/EXEC vs Pipeline?
  10. Valkey vs Redis 7.4 — chọn khi nào?

16. Tiếp theo

Bài tiếp: Tuan-11-DynamoDB-Cassandra — wide-column NoSQL với access pattern thinking.


Tuần 10 hoàn thành. Redis is a data structure server, not a cache. Cập nhật: 2026-05-16