Danh sách bài viết

Bài 21: Hash — User Profile, Metadata & Hash Field TTL

Redis Hash là kiểu dữ liệu map field→value nằm trong 1 key — tương đương với object/dict của ngôn ngữ lập trình. Thay vì tạo nhiều String key riêng lẻ như user:123:name, user:123:email, bạn gom chúng vào 1 Hash user:123 và thao tác theo field. Bài này đi qua bộ command Hash, so sánh kỹ Hash với JSON String (khi nào dùng gì), ba use case thực tế (user profile, counter theo field, metadata/config), encoding nội bộ listpack vs hashtable và ảnh hưởng tới memory, tính năng Hash Field TTL mới trong Redis 7.4 (HEXPIRE/HTTL), HSCAN cho hash lớn, code Python redis-py hoàn chỉnh và các anti-pattern cần tránh.

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

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

  • Nắm được bộ command Hash: HSET, HGET, HMGET, HGETALL, HDEL, HINCRBY, HEXISTS, HKEYS, HVALS, HLEN, HSCAN.
  • Biết khi nào dùng Hash thay vì nhiều String key riêng lẻ, và khi nào dùng JSON String thay vì Hash.
  • Hiểu encoding nội bộ listpack vs hashtable và tác động tới memory.
  • Dùng được Hash Field TTL (Redis 7.4+) với HEXPIREHTTL.
  • Nhận ra anti-pattern HGETALL trên hash lớn và biết dùng HSCAN thay thế.
2

Hash Là Gì & Bộ Command Cơ Bản

Redis Hash là kiểu dữ liệu lưu nhiều cặp field-value dưới cùng 1 key. Field và value đều là string. Không giới hạn số field (về lý thuyết đến 232−1 field per hash), nhưng trên thực tế hash lớn cần quản lý cẩn thận.

Khác với String — mỗi key chứa 1 giá trị — Hash cho phép truy cập từng field độc lập mà không cần đọc toàn bộ giá trị.

Các lệnh cơ bản

# Tạo / cập nhật 1 hoặc nhiều field
HSET user:123 name "Anh" email "[email protected]" age 28 plan "pro"
# → (integer) 4  (số field MỚI được tạo)

# Đọc 1 field
HGET user:123 email
# → "[email protected]"

# Đọc nhiều field cùng lúc
HMGET user:123 name plan
# 1) "Anh"
# 2) "pro"

# Đọc toàn bộ hash (field + value xen kẽ)
HGETALL user:123
# 1) "name"
# 2) "Anh"
# 3) "email"
# 4) "[email protected]"
# 5) "age"
# 6) "28"
# 7) "plan"
# 8) "pro"

# Xoá field
HDEL user:123 age
# → (integer) 1

# Kiểm tra field có tồn tại không
HEXISTS user:123 email
# → (integer) 1  (1 = có, 0 = không)

# Lấy danh sách field
HKEYS user:123
# 1) "name"
# 2) "email"
# 3) "plan"

# Lấy danh sách value
HVALS user:123
# 1) "Anh"
# 2) "[email protected]"
# 3) "pro"

# Đếm số field
HLEN user:123
# → (integer) 3

# Tăng giá trị field số nguyên
HINCRBY post:456:stats views 1
# → (integer) 1

Lưu ý: HSET (Redis 4.0+) thay thế HMSET (deprecated). HSET chấp nhận 1 hoặc nhiều field-value pair trong cùng 1 lệnh.

HGET trả về nil nếu field không tồn tại. HMGET trả về mảng có nil ở vị trí của field thiếu — không báo lỗi.

3

Use Case 1 — User Profile & Object Cache

Use case kinh điển nhất của Hash: lưu một object/record từ database dưới dạng field-value, thay vì serialize toàn bộ thành JSON String.

# Lưu profile
HSET user:123 name "Anh" email "[email protected]" age 28 plan "pro"

# Đọc toàn profile
HGETALL user:123

# Chỉ cần 1 trường — không cần đọc cả object
HGET user:123 email

# Nâng cấp plan — chỉ update đúng 1 field, không đụng field khác
HSET user:123 plan "enterprise"

# Đọc 2 trường để hiển thị UI
HMGET user:123 name plan

Lợi thế so với JSON String: khi bạn chỉ cần cập nhật 1 field (ví dụ plan), với Hash chỉ cần 1 lệnh HSET user:123 plan "enterprise" — atomic, không touch các field khác. Với JSON String bạn phải GET → deserialize → sửa field → serialize → SET lại toàn bộ blob. Với object nhiều field và tần suất cập nhật cao, chi phí serialize/deserialize cộng dồn đáng kể.

HSET trên field đã có chỉ update giá trị, không tạo thêm field mới — trả về 0 (số field mới tạo = 0). Hành vi này cho phép update an toàn mà không cần kiểm tra trước.

4

Hash vs JSON String — Khi Nào Dùng Gì

Hash và JSON String đều có thể lưu object. Lựa chọn phụ thuộc vào access pattern:

Tiêu chí Hash JSON String
Update 1 field HSET key field value — rẻ, atomic GET + parse + sửa + SET lại toàn blob
Đọc toàn bộ object HGETALL — 1 round-trip GET — 1 round-trip (đơn giản hơn)
Đọc 1-2 field HGET / HMGET — trực tiếp GET toàn blob rồi parse, lãng phí
Cấu trúc lồng nhau (nested) Không — Hash chỉ flat 1 cấp Có — JSON hỗ trợ object/array lồng
Atomic increment theo field HINCRBY — có Không — phải GET + SET với WATCH/Lua
Memory (hash nhỏ) Tốt — listpack encoding compact OK — nhưng overhead key + JSON syntax
Field TTL riêng biệt Có — Redis 7.4+ với HEXPIRE Không — TTL chỉ per key

Chọn Hash khi: bạn thường xuyên update hoặc đọc từng field riêng lẻ, cần atomic increment field số, cần Hash Field TTL (Redis 7.4+), hoặc object là flat (không nested).

Chọn JSON String khi: object có cấu trúc lồng nhau (nested arrays/objects), bạn gần như luôn đọc toàn bộ object và ít khi update từng field, hoặc cần dùng RedisJSON module với query JSONPath.

Hash không hỗ trợ nested structure. HSET user:123 address.city "Hanoi" lưu field tên là chuỗi "address.city" — không phải object lồng. Nếu cần nested, dùng JSON String hoặc RedisJSON module.

5

Use Case 2 — Counter Theo Field (HINCRBY)

Khi có nhiều counter liên quan đến cùng 1 entity, Hash cho phép gom chúng vào 1 key thay vì tạo nhiều String key riêng (post:456:views, post:456:likes, post:456:comments...).

# Tăng từng counter — mỗi lệnh atomic per field
HINCRBY post:456:stats views 1
HINCRBY post:456:stats likes 1
HINCRBY post:456:stats comments 1

# Đọc tất cả stats 1 lần
HGETALL post:456:stats
# 1) "views"
# 2) "42"
# 3) "likes"
# 4) "17"
# 5) "comments"
# 6) "5"

# Đọc chỉ views và likes
HMGET post:456:stats views likes
# 1) "42"
# 2) "17"

Lợi ích: 1 key thay vì nhiều key, HGETALL lấy tất cả stats trong 1 round-trip, mỗi HINCRBY là atomic per field. HINCRBY khởi tạo field về 0 tự động nếu chưa tồn tại — hành vi giống INCR với String.

HINCRBYFLOAT tồn tại nhưng ít dùng — lý do tương tự với float counter nói chung: dùng integer đơn vị nhỏ hơn nếu được (ví dụ lưu điểm rating × 100 dạng integer).

6

Use Case 3 — Metadata & Config Store

Hash phù hợp để lưu các tập flag hoặc tham số cấu hình liên quan đến cùng 1 entity — đặc biệt khi bạn cần đọc/ghi từng mục riêng lẻ.

Feature flags per user

HSET flags:user:123 dark_mode 1 beta 0 new_dashboard 1
# Bật tính năng mới cho user này
HSET flags:user:123 new_dashboard 1
# Kiểm tra 1 flag
HGET flags:user:123 dark_mode    # → "1"
# Đọc tất cả flags của user
HGETALL flags:user:123

Service config động

HSET config:service-a timeout 30 retries 3 max_conn 100
# Điều chỉnh timeout mà không reload toàn bộ config
HSET config:service-a timeout 60
# Service đọc lại config theo field
HMGET config:service-a timeout retries

Pattern này phù hợp với config hay thay đổi giữa các entity (per-user, per-tenant, per-service). So với lưu toàn bộ config dưới dạng JSON String: khi chỉ cần đổi 1 tham số, HSET rẻ hơn nhiều so với GET → parse → sửa → SET lại.

7

Hash Field TTL (Redis 7.4+)

Trước Redis 7.4, TTL chỉ có thể đặt ở cấp key. Không có cách nào để 1 field trong hash hết hạn trong khi các field khác vẫn tồn tại — buộc phải dùng nhiều String key riêng nếu cần per-field expiry.

Redis 7.4 (phát hành tháng 7/2024) bổ sung Hash Field Expiration qua các lệnh mới:

Lệnh Ý nghĩa
HEXPIRE key seconds FIELDS n field [field ...] Đặt TTL (giây) cho n field chỉ định
HPEXPIRE key milliseconds FIELDS n field [field ...] Đặt TTL (millisecond)
HEXPIREAT key unix-time FIELDS n field [field ...] Đặt expiry theo Unix timestamp (giây)
HTTL key FIELDS n field [field ...] Đọc TTL còn lại (giây) của từng field
HPTTL key FIELDS n field [field ...] Đọc TTL còn lại (millisecond)
HPERSIST key FIELDS n field [field ...] Xoá TTL khỏi field (field tồn tại vĩnh viễn cho đến khi xoá thủ công)

Ví dụ: Session với token expire độc lập

# Tạo session hash chứa 2 field
HSET session:abc token "t-xyz" csrf "c-123" user_id 42 created_at 1716895200

# token hết hạn sau 1 giờ (3600 giây)
HEXPIRE session:abc 3600 FIELDS 1 token
# → 1) (integer) 1  (1 = thành công)

# csrf hết hạn sau 24 giờ
HEXPIRE session:abc 86400 FIELDS 1 csrf
# → 1) (integer) 1

# user_id và created_at không có TTL (tồn tại theo key)

# Kiểm tra TTL còn lại của token
HTTL session:abc FIELDS 1 token
# → 1) (integer) 3542  (còn ~59 phút)

# Khi token hết hạn, field tự xoá
# HGET session:abc token → nil
# Các field khác vẫn còn nguyên

Ví dụ: Cache field-level TTL

# Profile cache: avatar URL hết hạn sớm hơn các field khác
HSET user:123:cache name "Anh" plan "pro" avatar_url "https://cdn.x.com/a.jpg"
HEXPIRE user:123:cache 300 FIELDS 1 avatar_url   # avatar cache 5 phút
HEXPIRE user:123:cache 3600 FIELDS 1 plan        # plan cache 1 giờ
# name không có TTL (ít thay đổi)

Trước Redis 7.4 — Cách làm cũ

Trước 7.4, để có per-field expiry phải dùng nhiều String key riêng:

# Cách cũ — 4 String key riêng với TTL khác nhau
SET session:abc:token "t-xyz" EX 3600
SET session:abc:csrf "c-123" EX 86400
SET session:abc:user_id 42
SET session:abc:created_at 1716895200

# Cần xoá session: phải DEL từng key hoặc dùng SCAN + pattern

Hash Field TTL (Redis 7.4+) dọn gọn mô hình này: tất cả field nằm trong 1 key, TTL riêng từng field, xoá key là xoá toàn bộ session.

Lưu ý khi dùng Hash Field TTL

  • Yêu cầu Redis 7.4+. Dùng redis-cli INFO server | grep redis_version để kiểm tra.
  • redis-py 5.x đã hỗ trợ: r.hexpire(), r.httl(), r.hpersist(). ioredis: kiểm tra changelog từng version.
  • Khi field bị expire, field bị xoá nhưng key vẫn tồn tại (trừ khi đây là field cuối cùng).
  • HTTL trả về -1 nếu field không có TTL, -2 nếu field không tồn tại.
8

Encoding: Listpack vs Hashtable

Redis tự chọn encoding nội bộ cho Hash dựa trên kích thước:

listpack (Redis 7+, tiền thân là ziplist)

Hash nhỏ dùng listpack — một cấu trúc dữ liệu nén lưu tuần tự trong bộ nhớ liên tục, không có pointer overhead. Mỗi entry (field hoặc value) được encode trực tiếp kèm length prefix.

  • Lookup O(N) — phải scan tuần tự.
  • Tiết kiệm memory đáng kể so với hashtable vì không có pointer và bucket overhead.
  • Hiệu quả trong thực tế: N nhỏ (mặc định ≤ 128 field) nên O(N) vẫn nhanh.

hashtable

Hash lớn chuyển sang hashtable — dict thực sự với array of buckets và linked list cho collision.

  • Lookup O(1) trung bình.
  • Memory overhead cao hơn: mỗi entry có pointer, bucket array chiếm thêm không gian.

Threshold config

# Xem config hiện tại
redis-cli CONFIG GET hash-max-listpack-entries
# → "128"
redis-cli CONFIG GET hash-max-listpack-value
# → "64"
  • hash-max-listpack-entries (default 128): số field tối đa để dùng listpack.
  • hash-max-listpack-value (default 64 bytes): kích thước tối đa của 1 field hoặc value để dùng listpack.
  • Vượt ngưỡng một trong hai → Redis chuyển sang hashtable, không tự chuyển ngược lại khi xoá field.

Kiểm tra encoding thực tế

HSET user:123 name "Anh" email "[email protected]" plan "pro"
OBJECT ENCODING user:123
# → "listpack"

# Thêm nhiều field vượt threshold 128
# (sau khi thêm field thứ 129)
OBJECT ENCODING user:123
# → "hashtable"
9

So Sánh Memory: Hash vs Nhiều String Key

Đây là một trong những lý do thực tế nhất để chọn Hash với listpack encoding thay vì nhiều String key.

Scenario: 1000 user, 5 field mỗi user

Option A — 5000 String key riêng lẻ (user:1:name, user:1:email, ...):

  • Mỗi key Redis object có overhead ~50-60 bytes (struct robj + SDS header + dict entry trong keyspace).
  • 5000 key × ~55 bytes overhead = ~275 KB chỉ cho overhead key.
  • Chưa tính data thực tế và memory allocator alignment (jemalloc có thể tăng thêm).

Option B — 1000 Hash với listpack encoding (user:1 chứa 5 field):

  • 1000 key dict entry thay vì 5000.
  • Mỗi hash listpack lưu 5 field-value tuần tự, không có pointer per-entry.
  • Tiết kiệm 5-10x memory overhead so với Option A ở quy mô này.

Đo thực tế:

HSET user:123 name "Anh" email "[email protected]" age 28 plan "pro" created 1716895200
MEMORY USAGE user:123
# → khoảng 112-120 bytes (tuỳ value length)

# So sánh: 5 String key riêng
SET user:123:name "Anh"             # MEMORY USAGE → ~56 bytes
SET user:123:email "[email protected]"      # → ~64 bytes
SET user:123:age "28"               # → ~56 bytes
SET user:123:plan "pro"             # → ~56 bytes
SET user:123:created "1716895200"   # → ~64 bytes
# Tổng: ~296 bytes cho cùng dữ liệu

Hash listpack: ~120 bytes. 5 String key: ~296 bytes. Chênh lệch ~2.5x cho 1 user, nhân lên 1 triệu user → chênh ~180MB.

Lợi thế này chỉ có khi hash còn ở listpack encoding (≤ 128 field, value ≤ 64 bytes). Khi chuyển sang hashtable, overhead tăng lên và lợi thế memory giảm.

10

HSCAN Cho Hash Lớn

HGETALL là O(N) với N = số field. Trên hash nhỏ (vài chục field) không vấn đề. Trên hash lớn (hàng nghìn field trở lên), HGETALL block single-threaded event loop trong khoảng thời gian đủ để gây P99 spike cho tất cả request khác.

Khi hash có nhiều field và bạn không cần đọc hết một lúc, dùng HSCAN:

# HSCAN cursor [MATCH pattern] [COUNT count]
# Lần đầu: cursor = 0
HSCAN big:hash 0 COUNT 100
# Trả về:
# 1) "128"       ← cursor mới (khác 0 = còn tiếp)
# 2) 1) "field1"
#    2) "value1"
#    3) "field2"
#    4) "value2"
#    ...

# Tiếp tục với cursor nhận được
HSCAN big:hash 128 COUNT 100
# ...

# Khi cursor về "0" — đã scan hết
HSCAN big:hash 456 COUNT 100
# 1) "0"     ← cursor = 0, kết thúc

Lưu ý về COUNT: đây là gợi ý cho Redis, không phải số field chính xác mỗi lần. Với listpack encoding, Redis có thể trả về toàn bộ hash bất kể COUNT vì listpack đọc tuần tự không có cursor thực sự.

Khi nào dùng HGETALL vs HSCAN

  • HGETALL: hash có < vài trăm field và bạn cần toàn bộ data. Đơn giản, 1 lệnh.
  • HSCAN: hash có hàng nghìn field trở lên, hoặc bạn cần iterate qua field mà không cần toàn bộ cùng lúc.
  • Trong code: nếu không chắc hash có bao nhiêu field, dùng HLEN để kiểm tra trước, rồi quyết định dùng HGETALL hay HSCAN.
11

Code Python (redis-py)

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


# --- Use case 1: User profile ---

def save_user_profile(user_id: int, profile: dict) -> None:
    """Lưu profile vào Hash. mapping= nhận dict Python trực tiếp."""
    r.hset(f"user:{user_id}", mapping=profile)


def get_user_profile(user_id: int) -> dict:
    """Đọc toàn bộ profile. Trả về dict, rỗng nếu key không tồn tại."""
    return r.hgetall(f"user:{user_id}")


def update_plan(user_id: int, new_plan: str) -> None:
    """Cập nhật 1 field — không touch field khác."""
    r.hset(f"user:{user_id}", "plan", new_plan)


def get_display_info(user_id: int) -> list:
    """Chỉ lấy name và plan để hiển thị UI."""
    return r.hmget(f"user:{user_id}", "name", "plan")


# --- Use case 2: Counter theo field ---

def increment_post_stat(post_id: int, stat: str, delta: int = 1) -> int:
    """Tăng counter cho 1 stat (views, likes, comments...)."""
    return r.hincrby(f"post:{post_id}:stats", stat, delta)


def get_post_stats(post_id: int) -> dict:
    return r.hgetall(f"post:{post_id}:stats")


# --- Use case 3: Feature flags ---

def set_flag(user_id: int, flag: str, value: int) -> None:
    r.hset(f"flags:user:{user_id}", flag, value)


def is_flag_enabled(user_id: int, flag: str) -> bool:
    val = r.hget(f"flags:user:{user_id}", flag)
    return val == "1"


# --- Hash Field TTL (Redis 7.4+, redis-py 5.x) ---

def create_session(session_id: str, token: str, csrf: str, user_id: int) -> None:
    """Tạo session hash với TTL riêng cho token và csrf."""
    r.hset(
        f"session:{session_id}",
        mapping={
            "token": token,
            "csrf": csrf,
            "user_id": user_id,
        }
    )
    # token expire sau 1 giờ
    r.hexpire(f"session:{session_id}", 3600, "token")
    # csrf expire sau 24 giờ
    r.hexpire(f"session:{session_id}", 86400, "csrf")


def get_token_ttl(session_id: str) -> int:
    """Trả về TTL còn lại (giây) của field token. -1 nếu không có TTL."""
    result = r.httl(f"session:{session_id}", "token")
    # redis-py trả về list, lấy phần tử đầu
    return result[0] if result else -2


# --- HSCAN cho hash lớn ---

def iter_hash_fields(key: str, count: int = 100):
    """Generator iterate qua tất cả field của hash, không block."""
    cursor = 0
    while True:
        cursor, data = r.hscan(key, cursor, count=count)
        yield from data.items()
        if cursor == 0:
            break


# --- Demo ---
if __name__ == "__main__":
    save_user_profile(123, {"name": "Anh", "email": "[email protected]", "plan": "pro"})
    print(get_user_profile(123))
    # {'name': 'Anh', 'email': '[email protected]', 'plan': 'pro'}

    update_plan(123, "enterprise")
    print(get_display_info(123))
    # ['Anh', 'enterprise']

    increment_post_stat(456, "views")
    increment_post_stat(456, "likes")
    print(get_post_stats(456))
    # {'views': '1', 'likes': '1'}

    create_session("abc", "t-xyz", "c-123", 42)
    print(get_token_ttl("abc"))
    # 3598  (hoặc gần 3600)

Lưu ý: r.hgetall()r.hscan() với decode_responses=True trả về dict[str, str] — tất cả value là string. Nếu cần int/float, tự convert sau khi đọc:

profile = r.hgetall("user:123")
age = int(profile["age"])          # string → int
score = float(profile["score"])    # string → float
12

Anti-patterns

  • HGETALL trên hash triệu field:
    # Sai — block event loop nếu hash có hàng triệu field
    HGETALL big-hash-with-millions-of-fields
    
    # Đúng — iterate bằng HSCAN
    HSCAN big-hash-with-millions-of-fields 0 COUNT 500

    Redis single-threaded. HGETALL O(N) với N = số field — đủ lớn sẽ block mọi request khác trong lúc execute.

  • Hash cho dữ liệu nested:
    # Sai về mặt concept — field "address.city" là 1 string, không phải object lồng
    HSET user:123 address.city "Hanoi" address.country "VN"
    
    # Nếu cần nested structure, dùng JSON String
    SET user:123 '{"name":"Anh","address":{"city":"Hanoi","country":"VN"}}'
    # Hoặc RedisJSON module với JSONSET / JSONGET
  • Big key — 1 hash chứa quá nhiều field (> vài trăm nghìn):

    Hash với quá nhiều field là "big key" — scan chậm, memory phình lớn 1 node, xoá (DEL) tốn nhiều tài nguyên. Nếu bạn cần lưu hàng triệu entity, mỗi entity là 1 Hash (key theo entity ID), không gom tất cả vào 1 Hash.

  • Quên TTL cho session hash (trước Redis 7.4):
    # Session không bao giờ hết hạn — memory leak
    HSET session:abc token "t-xyz" user_id 42
    
    # Đúng — đặt TTL ở cấp key (trước 7.4)
    HSET session:abc token "t-xyz" user_id 42
    EXPIRE session:abc 3600
    
    # Hoặc Redis 7.4+ với field TTL riêng biệt
    HEXPIRE session:abc 3600 FIELDS 1 token
  • Vượt listpack threshold do 1 field có value dài:

    Nếu 1 field có value > 64 bytes, toàn bộ hash chuyển sang hashtable encoding, mất lợi thế memory của listpack cho tất cả field còn lại. Xem xét tách field có value lớn ra key riêng nếu memory là concern.

13

Tổng Kết & Quiz

Tổng kết

  • Hash = 1 key chứa nhiều field-value pair. Field và value đều là string (flat, không nested).
  • Update 1 field với HSET là atomic và không ảnh hưởng field khác — lợi thế chính so với JSON String khi access pattern thiên về update từng field.
  • Chọn Hash khi: flat object, update từng field thường xuyên, cần HINCRBY atomic, hoặc cần Hash Field TTL. Chọn JSON String khi: nested structure, hoặc gần như luôn đọc toàn bộ object.
  • Redis 7.4+ bổ sung Hash Field TTL (HEXPIRE/HTTL): TTL riêng từng field trong cùng 1 hash key — trước đây phải dùng nhiều String key riêng.
  • Hash nhỏ (≤ 128 field, value ≤ 64 bytes) dùng listpack encoding: compact, tiết kiệm memory 2-5x so với nhiều String key.
  • HGETALL là O(N) — chỉ dùng khi hash nhỏ. Hash lớn (nghìn field trở lên) dùng HSCAN để tránh block event loop.

Quiz 5 câu

  1. Tại sao HSET user:123 plan "enterprise" an toàn hơn GET toàn JSON rồi set lại khi chỉ muốn update trường plan?
  2. Hash có hỗ trợ lưu nested object không? Nếu cần lưu address.city dạng nested, bạn dùng gì thay thế?
  3. Hash Field TTL (Redis 7.4) giải quyết bài toán gì mà trước đó phải dùng nhiều String key?
  4. Khi nào hash dùng listpack encoding và khi nào chuyển sang hashtable? Điều chỉnh được bằng config nào?
  5. Vì sao HGETALL trên hash triệu field là anti-pattern, và thay bằng lệnh gì?

Đáp án gợi ý

  1. HSET trực tiếp cập nhật 1 field trong hash — atomic, không đọc các field khác. JSON String yêu cầu GET → deserialize → sửa → serialize → SET: 2 round-trip, không atomic (cần WATCH/Lua để tránh race condition), tốn CPU serialize/deserialize.
  2. Không — Hash chỉ flat 1 cấp. HSET user:123 address.city "Hanoi" tạo field tên là chuỗi "address.city". Nếu cần nested, dùng JSON String (SET user:123 '{"address":{"city":"Hanoi"}}') hoặc RedisJSON module.
  3. Trước 7.4, TTL chỉ đặt per key — nếu muốn field token hết hạn sau 1h và csrf hết hạn sau 24h trong cùng 1 "session", phải dùng 2 String key riêng. Redis 7.4 cho phép HEXPIRE session:abc 3600 FIELDS 1 token — gộp cả session vào 1 hash mà vẫn có per-field TTL.
  4. Hash dùng listpack khi: số field ≤ hash-max-listpack-entries (default 128) VÀ mọi field/value ≤ hash-max-listpack-value (default 64 bytes). Vượt ngưỡng một trong hai → chuyển hashtable. Điều chỉnh bằng CONFIG SET hash-max-listpack-entries NCONFIG SET hash-max-listpack-value N.
  5. HGETALL O(N) block single-threaded event loop trong suốt thời gian scan N field — đủ lớn sẽ làm P99 spike cho mọi request khác. Thay bằng HSCAN key cursor COUNT n: cursor-based, mỗi lần chỉ trả về một batch field, không block toàn bộ server.

Bài tiếp theo

Bài 22 đi vào List: timeline, capped list và queue cơ bản với LPUSH/RPUSH/LPOP/LRANGE/LTRIM.

Tham khảo