Danh sách bài viết

Bài 30: Checklist & Anti-patterns Data Structure — Bài Học Từ Incident HGETALL

Bài cuối Module 2 tổng kết Data Structures Thực Chiến qua hai phần chính. Phần đầu là incident thật: một Hash tích lũy 5 triệu field qua một năm, đến khi dashboard gọi HGETALL, Redis block event loop vài giây, P99 latency từ 3ms vọt lên 6 giây, API cascade timeout. Phần sau là big key problem, danh sách O(N) command nguy hiểm kèm thay thế an toàn, top 10 anti-pattern data structure, SCAN family pattern, và checklist production-ready đầy đủ trước Module 3.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

  • Hiểu diễn biến cụ thể của incident HGETALL trên Hash 5 triệu field: tại sao O(N) trên single-threaded event loop gây cascade timeout.
  • Nhận diện big key: định nghĩa, tác hại, cách detect và fix.
  • Biết toàn bộ O(N) command nguy hiểm trong Redis và thay thế an toàn tương ứng.
  • Dùng SCAN family (HSCAN, SSCAN, ZSCAN) đúng cách thay cho HGETALL/SMEMBERS/ZRANGE toàn bộ.
  • Nhận diện 10 anti-pattern data structure phổ biến nhất và hướng khắc phục.
  • Sử dụng checklist production-ready để review thiết kế data structure trước khi deploy.
2

Incident: Hash 5 Triệu Field & HGETALL Block Event Loop

Bối cảnh

Một e-commerce platform lưu toàn bộ mapping product_id → metadata (tên, giá, category, thumbnail URL) trong một Hash duy nhất tên all_products. Khi mới ra mắt, catalog có vài nghìn sản phẩm — Hash nhỏ gọn, HGETALL trả về trong vài millisecond.

Sau một năm, catalog tăng lên 5 triệu sản phẩm. Hash vẫn là một key duy nhất. Admin dashboard có một dropdown "Chọn sản phẩm" load toàn bộ danh sách khi trang mở — gọi thẳng HGETALL all_products.

Diễn biến

HGETALL là lệnh O(N) — nó đọc và serialize toàn bộ N field của Hash trước khi trả về bất kỳ byte nào. Redis single-threaded event loop: trong khi HGETALL đang serialize 5 triệu field, mọi command khác phải đợi.

  • T+0: Admin mở dashboard. Browser gửi request. Backend gọi HGETALL all_products.
  • T+0 → T+3s: Redis chiếm event loop để serialize 5M field. Trong khoảng thời gian này, mọi GET/SET/ZADD từ toàn bộ application đều queue lại.
  • T+1s: Các API endpoint có timeout 1 giây bắt đầu trả 504 Gateway Timeout vì Redis không phản hồi.
  • T+2s: P99 latency Redis từ 3ms vọt lên 6 giây. Application log tràn timeout error.
  • T+3s: HGETALL trả xong ~80MB data cho backend. Event loop Redis giải phóng. Nhưng hàng trăm command đang chờ trong queue bắt đầu xử lý tuần tự — P99 vẫn cao thêm vài giây.
  • T+5s: Backend nhận 80MB JSON, phải deserialize để render dropdown. Memory spike trên application server.
  • T+10s: Hệ thống dần hồi phục. Nhưng admin dropdown tiếp tục refresh mỗi 30 giây — incident sẽ lặp lại.

Timeline tóm tắt

T+0    HGETALL all_products (5M field)
       → event loop bị chiếm, mọi command queue lại

T+0–3s P99 latency: 3ms → 6s
       API timeout hàng loạt (504)

T+3s   HGETALL trả xong ~80MB, event loop giải phóng
       queue dồn → P99 vẫn cao

T+10s  hệ thống hồi phục một phần
       nhưng dropdown tiếp tục refresh → incident lặp lại mỗi 30s

Incident này khác với KEYS ở Module 1: KEYS là lỗi của developer chạy tay một lần. HGETALL ở đây được gọi tự động từ code production, lặp lại định kỳ — nghĩa là nó sẽ xảy ra mãi cho đến khi được fix.

3

Root Cause & Fix

Root cause

  • Big key không kiểm soát: Hash all_products không có giới hạn size. Không ai đặt câu hỏi "nếu catalog tăng 1000x thì sao?"
  • O(N) command trên hot path: HGETALL gọi từ code production, chạy định kỳ, không phân trang.
  • Encoding đã chuyển sang hashtable: khi Hash vượt ngưỡng hash-max-listpack-entries (mặc định 128 entry), Redis tự động chuyển encoding từ listpack sang hashtable. Với 5M field, HGETALL phải traverse hashtable khổng lồ.
  • Không phân biệt "load tất cả" vs "load cần thiết": dropdown chỉ cần tên và ID, nhưng HGETALL trả cả metadata đầy đủ của 5M sản phẩm.

Fix ngắn hạn — giảm thiểu ngay

import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Thay HGETALL bằng HSCAN với cursor
# Chỉ duyệt khi thực sự cần — không dùng trong hot path
def iter_all_products(r, count=200):
    cursor = 0
    while True:
        cursor, data = r.hscan("all_products", cursor, count=count)
        yield data  # dict {field: value}
        if cursor == 0:
            break

Fix dài hạn — shard Hash

Chia all_products thành N shard nhỏ dựa trên product_id % N:

NUM_SHARDS = 64  # có thể tăng khi cần

def get_shard_key(product_id: int) -> str:
    shard = product_id % NUM_SHARDS
    return f"products:shard:{shard}"

def set_product(r, product_id: int, metadata: dict):
    key = get_shard_key(product_id)
    r.hset(key, str(product_id), json.dumps(metadata))

def get_product(r, product_id: int) -> dict | None:
    key = get_shard_key(product_id)
    value = r.hget(key, str(product_id))
    return json.loads(value) if value else None

# Mỗi shard có tối đa ~78K product (5M / 64)
# HGETALL trên shard: O(78K) thay vì O(5M)

Fix dashboard

Dropdown không nên load 5 triệu sản phẩm cùng lúc. Thay bằng:

  • Paginate: load 50-100 record mỗi lần scroll hoặc search.
  • Full-text search: dùng Elasticsearch hoặc PostgreSQL full-text cho tìm kiếm sản phẩm — Redis không phải công cụ phù hợp cho use case này.
  • Nếu vẫn cần dropdown cố định (danh mục nhỏ): cache kết quả query riêng, không load từ Hash nguyên bản.

Bài học từ incident

  • O(N) command trên collection lớn = tắc nghẽn toàn bộ Redis. Không phải chỉ chậm riêng request đó.
  • Big key (1 key chứa quá nhiều element) là "silent killer" — không gây lỗi ngay, tích lũy theo thời gian.
  • Single-threaded: 1 lệnh chậm 3 giây = tất cả client khác chờ 3 giây.
  • Detect sớm: redis-cli --bigkeys nên chạy định kỳ, không chờ đến khi có incident.
4

Big Key Problem

Định nghĩa

Big key là key có kích thước bất thường lớn. Hai loại phổ biến:

  • Collection lớn: Hash/Set/List/ZSet với quá nhiều element. Ngưỡng thực tế: > 10.000 element cần cân nhắc; > 100.000 là nguy hiểm.
  • Value lớn: String key chứa value > 10MB (ví dụ: toàn bộ JSON response, file nhị phân).

Không có ngưỡng tuyệt đối — phụ thuộc vào tần suất truy cập và command dùng. Hash 10K field ít được đọc ít nguy hiểm hơn Hash 1K field bị HGETALL gọi mỗi giây.

Tác hại

  • O(N) command block event loop: HGETALL, SMEMBERS, LRANGE 0 -1 trên big key gây latency spike toàn Redis, không chỉ request đó.
  • Memory không đều trong cluster: Big key chiếm nguyên một slot. Khi cluster rebalance, node chứa slot đó gánh memory lớn hơn. Cluster có thể từ chối write khi node đầy dù tổng cluster còn trống.
  • DEL chậm: DEL bigkey là O(N) — nó giải phóng memory đồng bộ, block event loop. Hash 5M field: DEL có thể mất vài trăm ms đến vài giây.
  • Migration / resharding chậm: Di chuyển slot chứa big key mất thời gian dài, làm chậm toàn bộ quá trình resharding cluster.
  • Bandwidth spike: Serialize/deserialize big key qua mạng tốn băng thông lớn, ảnh hưởng các lệnh khác trên cùng kết nối.

Detect big key

# Scan toàn bộ DB tìm key lớn nhất theo từng type
redis-cli --bigkeys

# Ví dụ output:
# Biggest string found 'session:abc' has 512 bytes
# Biggest hash   found 'all_products' has 5000000 fields
# Biggest list   found 'events:log' has 2300000 items

# Sort key theo memory usage (Redis 4+, chậm hơn --bigkeys)
redis-cli --memkeys

# Đo memory của một key cụ thể
redis-cli MEMORY USAGE all_products
# Trả về bytes (bao gồm Redis overhead)

# Xem số field/element của key
redis-cli HLEN all_products     # Hash
redis-cli SCARD members         # Set
redis-cli LLEN events:log       # List
redis-cli ZCARD leaderboard     # ZSet

Lưu ý khi dùng --bigkeys: Lệnh này chạy SCAN nên không block, nhưng trên DB rất lớn nó chạy một lúc. Nên chạy ngoài giờ cao điểm hoặc trên replica. Không dùng trên primary đang high traffic.

Fix big key

  • Shard collection: chia thành nhiều key nhỏ bằng key:shard:{N} (xem ví dụ mục 3). Số shard nên là lũy thừa 2 để dễ tính toán.
  • UNLINK thay DEL: UNLINK key (Redis 4.0+) xóa key đồng bộ khỏi keyspace nhưng giải phóng memory bất đồng bộ ở background thread — không block event loop.
  • Đặt giới hạn khi thiết kế: collection nên có cơ chế cap (LTRIM cho List, cleanup job cho ZSet) ngay từ đầu, không chờ đến khi lớn.
# UNLINK thay DEL cho big key
UNLINK all_products   # async, không block event loop

# So sánh:
DEL all_products      # synchronous O(N) — block
5

O(N) Command Nguy Hiểm — Danh Sách Đầy Đủ

Redis là single-threaded event loop. Mọi command chạy tuần tự. Lệnh O(N) trên collection lớn không chỉ chậm riêng request đó — nó block tất cả client còn lại trong thời gian chạy.

Command Độ phức tạp Nguy hiểm khi Thay thế an toàn
KEYS pattern O(N) toàn DB DB > 100K key SCAN cursor-based
HGETALL key O(N) hash fields Hash > 10K field HSCAN hoặc HMGET selective
SMEMBERS key O(N) set members Set > 10K member SSCAN
LRANGE key 0 -1 O(N) list length List > 10K element LRANGE với range nhỏ
ZRANGE key 0 -1 O(N+log N) ZSet > 10K member ZRANGE với LIMIT hoặc ZSCAN
ZRANGEBYSCORE -inf +inf O(log N + M) M (kết quả) lớn Thêm LIMIT offset count
DEL bigkey O(N) giải phóng Collection lớn UNLINK async (Redis 4.0+)
FLUSHALL / FLUSHDB O(N) toàn DB Luôn nguy hiểm trên production Variant ASYNC hoặc disable hoàn toàn
SORT key O(N+M log M) List/Set lớn, sort phức tạp Dùng ZSet đã sort sẵn
SUNION / SINTER nhiều Set lớn O(N*M) Set lớn, nhiều operand Giới hạn kích thước Set trước

Nguyên tắc: bất kỳ command nào trả về tập lớn hoặc traverse toàn bộ collection đều cần phân trang. Câu hỏi cần hỏi khi code review: "Nếu collection này có 1 triệu element, lệnh này mất bao lâu?"

Phát hiện lệnh chậm

# Xem các lệnh có execution time > 10ms (slowlog-log-slower-than = 10000 microseconds)
redis-cli SLOWLOG GET 20

# Ví dụ output:
# 1) 1) (integer) 14           -- ID
#    2) (integer) 1716820800   -- timestamp
#    3) (integer) 3200000      -- microseconds (3.2 giây!)
#    4) 1) "HGETALL"
#       2) "all_products"

# Cấu hình trong redis.conf
slowlog-log-slower-than 10000   # microseconds
slowlog-max-len 256             # giữ 256 entry gần nhất
6

SCAN Family — Pattern An Toàn

SCAN family — SCAN, HSCAN, SSCAN, ZSCAN — dùng cursor-based iteration: mỗi lần chỉ xử lý một phần nhỏ, không block event loop toàn bộ. Trade-off: có thể trả duplicate, không snapshot (collection thay đổi trong lúc iterate), COUNT chỉ là hint.

HSCAN thay HGETALL

import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Thay HGETALL hash lớn — cursor-based, không block
def hscan_all(r, key: str, count: int = 100):
    """Iterate toàn bộ Hash an toàn. Yield từng batch dict."""
    cursor = 0
    while True:
        cursor, data = r.hscan(key, cursor, count=count)
        if data:
            yield data      # dict {field: value}
        if cursor == 0:
            break

# Sử dụng:
for batch in hscan_all(r, "products:shard:0"):
    for product_id, metadata_json in batch.items():
        process(product_id, metadata_json)

SSCAN thay SMEMBERS

def sscan_all(r, key: str, count: int = 100):
    """Iterate toàn bộ Set an toàn."""
    cursor = 0
    while True:
        cursor, members = r.sscan(key, cursor, count=count)
        if members:
            yield members   # list of strings
        if cursor == 0:
            break

ZSCAN thay ZRANGE 0 -1

def zscan_all(r, key: str, count: int = 100):
    """Iterate toàn bộ ZSet an toàn. Yield list of (member, score)."""
    cursor = 0
    while True:
        cursor, pairs = r.zscan(key, cursor, count=count)
        if pairs:
            yield pairs     # list of (member, score) tuples
        if cursor == 0:
            break

TypeScript — HSCAN với ioredis

import Redis from "ioredis";

const redis = new Redis({ host: "127.0.0.1", port: 6379 });

async function hscanAll(
  key: string,
  count = 100
): Promise<Map<string, string>> {
  const result = new Map<string, string>();
  let cursor = "0";

  do {
    // [cursor_mới, [field1, val1, field2, val2, ...]]
    const [nextCursor, flatList] = await redis.hscan(key, cursor, "COUNT", count);
    cursor = nextCursor;

    // ioredis trả flat array — pair up
    for (let i = 0; i < flatList.length; i += 2) {
      result.set(flatList[i], flatList[i + 1]);
    }
  } while (cursor !== "0");

  return result;
}

Lưu ý quan trọng khi dùng SCAN family

  • COUNT là hint, không guarantee: Redis có thể trả ít hơn hoặc nhiều hơn số COUNT yêu cầu. Tăng COUNT giảm số round-trip nhưng mỗi call tốn thêm thời gian.
  • Có thể trả duplicate: Nếu dictionary resize trong lúc iterate, một số entry bị trả lại. Code xử lý kết quả cần idempotent hoặc dùng Set để deduplicate.
  • Không snapshot: Key thêm/xóa trong lúc iterate có thể bị bỏ qua hoặc gặp. SCAN không đảm bảo consistency với collection đang thay đổi.
  • Cursor = 0 là điều kiện dừng: Không dừng khi cursor nhỏ hay âm. Chỉ dừng khi cursor trả về chính xác "0".
7

Top 10 Data Structure Anti-patterns

  1. Big key — HGETALL/SMEMBERS triệu element
    Đây là incident ở mục 2. Collection không giới hạn size, tích lũy theo thời gian, đến khi O(N) command chạy trên nó thì block toàn bộ Redis.
    Fix: shard collection thành nhiều key nhỏ; dùng SCAN family; đặt giới hạn size khi thiết kế.
  2. Sai structure cho bài toán
    Dùng List để implement reliable queue (không có acknowledgment), hoặc dùng Set khi cần ordered data, hoặc dùng String JSON khi cần update field lẻ. Bài 29 đã phân tích chi tiết decision matrix — cần chọn đúng trước khi viết code.
    Fix: review lại bài 29, đặc biệt ma trận "access pattern → structure".
  3. Collection không giới hạn size
    List/ZSet tích lũy không có cơ chế cleanup: log event ghi vào List, leaderboard ZSet không bao giờ xóa entry cũ. Memory tăng đều đặn, không có cảnh báo rõ ràng cho đến khi OOM.
    Fix: LTRIM sau mỗi LPUSH để giữ N item gần nhất; cleanup job định kỳ cho ZSet; TTL cho toàn bộ key nếu data tạm thời.
  4. Set cho unique count khổng lồ
    Dùng Set để đếm unique visitor, unique device ID, unique IP — mỗi member chiếm bộ nhớ thực sự. 1 triệu unique string = nhiều MB RAM chỉ để lấy 1 số đếm.
    Fix: HyperLogLog (bài 26) cho use case "đếm unique cardinality" — sai số ~0.81%, dùng 12KB cố định bất kể số lượng, đủ cho analytics dashboard.
  5. Bitmap với sparse integer ID
    Bitmap allocate string đủ để cover offset lớn nhất. SETBIT user:active 99999999 1 allocates ~12.5MB dù chỉ set 1 bit — vì Redis cấp phát toàn bộ string đến offset đó.
    Fix: Bitmap phù hợp khi ID là dense integer bắt đầu từ 0. Nếu ID sparse (UUID-based, large integer gap), dùng Set thay thế.
  6. ZSet không có TTL hay cleanup
    Leaderboard game, trending topic — tất cả đều grow theo thời gian nếu không có giới hạn. Entry cũ không tự xóa, không có TTL trên ZSet member.
    Fix: ZREMRANGEBYSCORE để xóa entry theo score (timestamp); ZREMRANGEBYRANK để giữ top N; hoặc TTL trên toàn bộ key nếu đây là window tạm thời.
  7. Vô tình làm encoding chuyển sang dạng nặng hơn
    Thêm 1 field vào Hash vượt hash-max-listpack-entries (mặc định 128), hoặc thêm 1 string dài vượt hash-max-listpack-value (mặc định 64 bytes) → toàn bộ Hash chuyển từ listpack sang hashtable, dùng nhiều RAM hơn gấp 3-5 lần. Xem bài 28 về encoding.
    Fix: theo dõi OBJECT ENCODING key; nếu memory critical thì giữ collection dưới threshold; hoặc tăng threshold nếu đó là giá trị hợp lý.
  8. O(N) command trên hot path
    HGETALL/SMEMBERS chạy trong API endpoint xử lý hàng trăm RPS — mỗi request gọi một lần, mỗi lần block vài ms, cộng dồn block event loop liên tục.
    Fix: cache kết quả ở tầng khác (application cache); phân trang; chuyển sang SCAN nền nếu cần iterate toàn bộ; dùng HGET/HMGET chọn lọc thay HGETALL.
  9. JSON String cho object update field lẻ
    Lưu user profile là String JSON. Mỗi khi update 1 field (vd: last_login), phải GET cả object, deserialize, update, serialize, SET lại — race condition và bandwidth lãng phí. Với traffic cao: nhiều request GET/SET race nhau.
    Fix: dùng Hash — HSET user:{id} last_login 1716820800 update atomic 1 field. Bài 21 đã phân tích so sánh String JSON vs Hash.
  10. Quên member uniqueness của các structure
    Dùng List khi cần unique member (List không enforce unique — duplicate được thêm bình thường). Hoặc dùng Set khi cần thứ tự insertion. Nhầm lẫn semantic giữa các structure.
    Fix: List = ordered, duplicates OK; Set = unordered, unique; ZSet = ordered by score, unique member; hiểu semantic trước khi dùng.
8

Checklist Production-Ready

Dùng checklist này khi review thiết kế data structure trước khi deploy lên production.

Sizing — giới hạn kích thước

  • Mọi collection có cơ chế giới hạn size (LTRIM, ZREMRANGEBYRANK, cleanup job, hoặc TTL).
  • Đặt câu hỏi: "Nếu hệ thống chạy 1 năm, key này lớn đến đâu?" — có câu trả lời rõ ràng.
  • Không có key > 10MB value hoặc > 100K element mà không có kế hoạch kiểm soát.
  • Big key detection routine: chạy redis-cli --bigkeys hoặc --memkeys định kỳ (ví dụ: weekly cron trên replica).

Command safety — an toàn khi dùng lệnh

  • Không có HGETALL, SMEMBERS, LRANGE 0 -1, ZRANGE 0 -1 trên collection không giới hạn size trong hot path.
  • Mọi iteration trên collection lớn dùng SCAN family (HSCAN, SSCAN, ZSCAN).
  • UNLINK thay DEL cho key lớn khi cần xóa.
  • SLOWLOG theo dõi thường xuyên, alert khi có command > 100ms.
  • Cân nhắc rename-command KEYS ""rename-command FLUSHALL "" trên production (Module 10).

Structure choice — chọn đúng structure

  • Structure phù hợp với access pattern thực tế (bài 29): không chỉ "structure này store được data cần lưu".
  • Unique count cardinality lớn → HyperLogLog, không phải Set.
  • Bitmap chỉ dùng cho dense integer ID (sequential, bắt đầu từ 0 hoặc số nhỏ).
  • Cần update field lẻ trong object → Hash, không phải String JSON.
  • Cần thứ tự + unique → ZSet. Cần unique không cần thứ tự → Set. Cần thứ tự + duplicates → List.

Encoding — compact memory

  • Kiểm tra OBJECT ENCODING key cho các key quan trọng sau khi populate data thực.
  • Nếu memory là ưu tiên: giữ collection dưới listpack threshold (128 entry, 64-byte value mặc định).
  • Tránh vô tình push encoding lên hashtable/skiplist bằng 1 value dài — kiểm tra max value length trong data.

TTL — vòng đời dữ liệu

  • Collection growing không có TTL hoặc cleanup routine là rủi ro.
  • ZSet leaderboard, List event log: có cleanup theo score/rank hoặc TTL trên key.
  • Dữ liệu tạm thời (session, token, cache): luôn có TTL.
9

Module 2 — Bản Đồ Khái Niệm

Module 2 đã đi qua 10 bài về data structures:

Bài Chủ đề Khái niệm cốt lõi
20 String INCR/DECR atomic, SETNX, Bitfield, counter, token
21 Hash User profile, object storage, HSET/HGET/HMGET, so sánh với String JSON
22 List Timeline capped, LPUSH/RPOP, LTRIM, LMPOP, queue vs stack
23 Set Tags, permissions, SADD/SISMEMBER, SUNION/SINTER/SDIFF
24 Sorted Set Leaderboard, ranking, ZADD/ZRANK/ZRANGE, score-based query
25 Bitmap & Bitfield Daily active user, feature flag, SETBIT/GETBIT/BITCOUNT
26 HyperLogLog Unique count với sai số ~0.81%, PFADD/PFCOUNT, 12KB cố định
27 GEO Geospatial index, GEOADD/GEODIST/GEOSEARCH, Haversine, store nearby
28 Encoding listpack vs hashtable vs skiplist vs intset, threshold, OBJECT ENCODING
29 Chọn structure Decision matrix, access pattern, 8 bài toán thực tế, trade-off
10

Self-Assessment Trước Module 3

Trước khi qua Module 3 Rate Limiting, tự kiểm tra các điểm sau:

  • Biết chọn đúng structure cho 1 bài toán bất kỳ: cho trước access pattern (read/write, range/exact, unique/duplicate, ordered/unordered), xác định được structure phù hợp.
  • Hiểu encoding ảnh hưởng memory: biết khi nào listpack chuyển sang hashtable/skiplist và tác động là gì.
  • Biết O(N) command nào nguy hiểm và thay thế tương ứng: HGETALL → HSCAN, SMEMBERS → SSCAN, DEL bigkey → UNLINK.
  • Biết detect và fix big key: redis-cli --bigkeys, MEMORY USAGE, shard strategy, UNLINK.
  • Biết khi nào dùng HyperLogLog thay Set (unique count lớn), khi nào Bitmap phù hợp (dense integer ID), khi nào GEO (proximity search).

Nếu còn chưa chắc về bất kỳ điểm nào, các bài bài 20-29 đều có quiz ở cuối để ôn lại.

11

Tổng Kết & Quiz

Tổng kết

  • HGETALL trên Hash 5 triệu field block toàn bộ Redis event loop vài giây — tất cả client đợi, P99 spike, cascade timeout. Không phải lỗi crash, nhưng nguy hiểm hơn vì lặp lại định kỳ.
  • Big key là silent killer: không gây lỗi ngay, tích lũy theo thời gian. Detect bằng --bigkeys định kỳ, fix bằng shard + SCAN + UNLINK.
  • O(N) command (HGETALL, SMEMBERS, LRANGE 0 -1, ZRANGE 0 -1, KEYS, DEL bigkey) đều nguy hiểm trên collection lớn. Đều có thay thế an toàn hơn.
  • SCAN family (HSCAN, SSCAN, ZSCAN) là cách iterate an toàn: cursor-based, non-blocking, nhưng có thể duplicate và không snapshot.
  • 10 anti-pattern data structure có chung gốc rễ: không giới hạn size, sai structure cho access pattern, hoặc không hiểu semantic của structure.

Quiz 5 câu

  1. Tại sao HGETALL trên Hash 5 triệu field không chỉ ảnh hưởng đến request đó mà còn ảnh hưởng đến tất cả client khác đang dùng Redis cùng lúc?
  2. Giải thích tại sao DEL bigkey có thể block event loop Redis. Lệnh nào thay thế và cơ chế hoạt động của nó khác gì?
  3. Một ứng dụng dùng Set để đếm số unique user xem trang trong ngày — hiện có 10 triệu unique user/ngày. Anti-pattern nào đang xảy ra? Giải pháp là gì và trade-off là gì?
  4. HSCAN có thể trả duplicate trong trường hợp nào? Code xử lý kết quả HSCAN cần đặc điểm gì để an toàn?
  5. Một Hash dùng để lưu session token — mỗi user có 1 field, format {user_id}: {token}. Sau 2 năm, Hash có 800K field. Chạy OBJECT ENCODING thấy "hashtable". Phân tích vấn đề và đề xuất ít nhất 2 hướng fix khác nhau.

Đáp án gợi ý

  1. Redis là single-threaded event loop: tại một thời điểm chỉ xử lý một command. HGETALL là O(N) — nó phải traverse và serialize toàn bộ N field trước khi trả kết quả. Trong suốt thời gian đó, event loop bị chiếm. Tất cả GET/SET/ZADD từ client khác phải đợi trong queue. Với 5M field, HGETALL có thể mất 2-5 giây — mọi client đợi 2-5 giây.
  2. DEL giải phóng memory đồng bộ: nó traverse cấu trúc dữ liệu (hashtable, skiplist...) để free từng phần tử, O(N) với N là số element. Với Hash 5M field, DEL block event loop tương đương HGETALL. UNLINK (Redis 4.0+) khác: nó unlink key khỏi keyspace đồng bộ (O(1)), nhưng chuyển việc giải phóng memory sang background thread — không block event loop. Kết quả từ client: key biến mất ngay sau UNLINK, nhưng RAM được trả về sau một chút.
  3. Anti-pattern: dùng Set để lưu 10M unique user mỗi ngày. Set lưu từng string thực sự — 10M user ID (giả sử 8 byte mỗi ID) = ~100MB RAM chỉ để lấy 1 con số. Giải pháp: HyperLogLog. PFADD user:pageview:{date} {user_id} rồi PFCOUNT để lấy estimate. Trade-off: sai số ~0.81% (không chính xác tuyệt đối), nhưng dùng 12KB cố định bất kể có 10K hay 10M unique user — phù hợp cho analytics dashboard không cần exact count.
  4. HSCAN có thể trả duplicate khi Redis rehash dictionary trong lúc iteration: nếu Hash tăng/giảm kích thước (rehash), cursor encoding thay đổi, một số bucket có thể được trả lại lần hai. Code xử lý kết quả HSCAN cần idempotent với duplicate: nếu dùng kết quả để upsert — không có vấn đề. Nếu dùng để đếm hoặc accumulate — cần deduplication (dùng Python set hoặc dict để lọc duplicate theo field name).
  5. Vấn đề: (1) Hash 800K field đã vượt xa threshold listpack — encoding là hashtable, dùng nhiều RAM hơn gấp nhiều lần so với listpack. (2) Session token trong Hash duy nhất: nếu cần HGETALL để debug hoặc cleanup, sẽ là big key O(800K). (3) Không có TTL — token hết hạn nhưng field vẫn còn trong Hash. Hướng fix 1 (shard): chia thành session:shard:{user_id % 64} — mỗi shard ~12.5K field, giữ được listpack nếu dưới threshold. Hướng fix 2 (tách key): mỗi session là key riêng session:{user_id} là String, đặt TTL thực sự — token hết hạn thì key tự xóa, không cần cleanup job, không có big key.

Bài tiếp theo

Module 3 bắt đầu với bài 31 — Vì Sao Cần Rate Limiting: abuse, cost control, fair usage — nền tảng trước khi xem các implementation dùng String INCR, Sorted Set sliding window, và Lua atomic script.