Danh sách bài viết

Bài 28: Encoding Internals — listpack, intset, skiplist, embstr vs raw

Mỗi data type Redis không chỉ có một cách lưu trữ duy nhất. Redis chọn internal encoding dựa trên kích thước thực tế của key: khi collection nhỏ, Redis dùng encoding compact (listpack, intset, embstr) tiết kiệm memory đáng kể; khi vượt threshold cấu hình, Redis tự chuyển sang encoding lớn hơn (hashtable, skiplist, raw) để đảm bảo performance. Bài này đi sâu vào từng encoding theo type, cơ chế chuyển đổi một chiều, cách đo và kiểm tra, chiến lược tuning threshold và shard collection — áp dụng được ngay khi cần tối ưu memory trong production.

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

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

  • Giải thích được vì sao Redis dùng nhiều encoding cho cùng một logical type.
  • Biết encoding nào áp dụng cho String (int/embstr/raw), List (listpack/quicklist), Hash (listpack/hashtable), Set (intset/listpack/hashtable), Sorted Set (listpack/skiplist).
  • Hiểu cấu trúc listpack và lý do nó tiết kiệm memory hơn hashtable.
  • Nắm rõ encoding chuyển một chiều — và hệ quả với design.
  • Kiểm tra encoding bằng OBJECT ENCODING và đo memory bằng MEMORY USAGE.
  • Biết khi nào và cách tune threshold, và khi nào nên shard collection thay vì tune.
2

Tại Sao Cần Quan Tâm Encoding

Một Hash trong Redis không luôn được lưu bằng cùng một cách. Nếu hash nhỏ, Redis lưu bằng listpack — một block memory liên tục rất gọn. Nếu hash vượt ngưỡng cấu hình, Redis tự chuyển sang hashtable — cấu trúc có pointer overhead và bucket allocation.

Mức độ chênh lệch này đáng kể. Một hash với 100 field/value nhỏ:

  • Encoding listpack: khoảng 2-4 KB.
  • Encoding hashtable: khoảng 10-20 KB, tùy kích thước value.

Nhân với hàng trăm nghìn key trong production, chênh lệch này có thể tính bằng GB. Điều quan trọng là Redis tự chọn encoding — nhưng bạn kiểm soát được threshold quyết định khi nào chuyển. Hiểu encoding giúp bạn:

  • Thiết kế key/collection ở kích thước phù hợp để giữ encoding compact.
  • Tránh một value lớn vô tình làm cả collection chuyển sang encoding tốn memory hơn.
  • Tune threshold có cơ sở thay vì đoán.
  • Debug khi memory usage cao bất ngờ.
3

Kiểm Tra Encoding & Memory

Hai lệnh cần nhớ:

OBJECT ENCODING mykey   -- cho biết encoding hiện tại của key
MEMORY USAGE mykey      -- cho biết số byte key tiêu thụ (bao gồm overhead)

Ví dụ thực tế với hash:

127.0.0.1:6379> HSET user:1 name "Alice" age 30 city "HCM"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING user:1
"listpack"
127.0.0.1:6379> MEMORY USAGE user:1
(integer) 120

127.0.0.1:6379> HSET user:1 bio "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING user:1
"hashtable"     -- chuyển vì value bio > 64 bytes (hash-max-listpack-value)
127.0.0.1:6379> MEMORY USAGE user:1
(integer) 792   -- tăng ~6x

MEMORY USAGE trả về byte thực tế, tính cả key name, value, overhead của encoding và allocator. Không phải con số application-level data thuần túy. Thêm option SAMPLES 0 để tính toàn bộ nested structure (quan trọng với collection lớn).

4

String Encoding: int, embstr, raw

String là type cơ bản nhất nhưng Redis vẫn có 3 encoding nội bộ:

int

Khi value là integer nằm trong range 64-bit signed (-2^63 đến 2^63-1), Redis lưu trực tiếp dưới dạng integer trong object header, không allocate string buffer riêng. INCR/DECR là atomic và không cần parse string.

SET counter 42
OBJECT ENCODING counter   -- "int"
INCR counter              -- atomic, O(1), không parse string

Redis cũng shared pool các integer từ 0 đến 9999 (compile-time constant REDIS_SHARED_INTEGERS). Hai key có value "100" trỏ về cùng object — tiết kiệm thêm memory.

embstr

String có độ dài ≤ 44 bytes được lưu dạng embstr: object header Redis (robj) và string buffer nằm trong cùng một lần malloc. Chỉ một allocation, bộ nhớ liên tục — cache-friendly.

SET greeting "Hello"
OBJECT ENCODING greeting   -- "embstr"

Giới hạn 44 bytes là hard-coded (OBJ_ENCODING_EMBSTR_SIZE_LIMIT trong object.c). Con số này đã tăng qua các version: Redis 3.x là 39 bytes, Redis 4+ là 44 bytes.

Một điểm quan trọng: embstr là read-only về mặt nội bộ. Bất kỳ thao tác modify nào (APPEND, SETRANGE, INCR trên value không phải integer) đều khiến Redis chuyển ngay sang raw.

SET s "hello"
OBJECT ENCODING s     -- "embstr"
APPEND s " world"
OBJECT ENCODING s     -- "raw"  (dù tổng chỉ 11 bytes)

raw

String > 44 bytes, hoặc bất kỳ string nào đã bị modify. Object header và string buffer là hai allocation riêng biệt (SDS — Simple Dynamic String). Cho phép in-place modification.

SET long_str "This string is definitely longer than forty-four bytes total"
OBJECT ENCODING long_str   -- "raw"

Tóm tắt String encoding

EncodingĐiều kiệnĐặc điểm
intInteger 64-bitKhông cần string buffer, INCR atomic
embstr≤ 44 bytes1 allocation, cache-friendly, read-only internally
raw> 44 bytes hoặc đã modify2 allocation, mutable, overhead cao hơn
5

List Encoding: listpack, quicklist

Từ Redis 7.0, List dùng hai encoding:

  • listpack: List nhỏ. Toàn bộ data trong một block memory liên tục. LPUSH/RPUSH/LRANGE đều O(N) nhưng N nhỏ và cache-friendly.
  • quicklist: List lớn. Là linked list của các node, mỗi node là một listpack. Cân bằng giữa memory và performance — LPUSH/RPUSH O(1), random access vẫn O(N).

Config kiểm soát khi nào chuyển từ listpack sang quicklist:

# redis.conf
list-max-listpack-size 128   # default: 128 elements per listpack node

Khi số element vượt list-max-listpack-size, list chuyển sang quicklist. Giá trị âm có ý nghĩa khác: -1 là 4KB per node, -2 là 8KB (default quicklist), -3 là 16KB, -4 là 32KB, -5 là 64KB.

Trước Redis 7.0: encoding nhỏ gọi là ziplist, encoding lớn là quicklist (quicklist của ziplist node). Redis 7.0 chuyển sang listpack để fix một số edge case của ziplist liên quan đến cascade update.

RPUSH mylist a b c
OBJECT ENCODING mylist   -- "listpack" (3 elements)
6

Hash Encoding: listpack, hashtable

Hash có hai encoding:

  • listpack: Hash nhỏ. Field và value lưu xen kẽ trong array liên tục (field₁, value₁, field₂, value₂, …). Lookup là O(N) nhưng N nhỏ — thực tế nhanh nhờ cache line.
  • hashtable: Hash lớn. O(1) lookup. Mỗi entry có pointer overhead; bucket array + load factor rehashing tốn thêm memory.

Hai threshold kiểm soát chuyển đổi:

hash-max-listpack-entries 128   # số field/value pair tối đa (default 128)
hash-max-listpack-value   64    # kích thước tối đa mỗi field hoặc value (byte, default 64)

Vượt một trong hai điều kiện → chuyển hashtable ngay lập tức:

HSET profile name "Alice" age "30"
OBJECT ENCODING profile   -- "listpack"

-- Thêm field có value > 64 bytes
HSET profile bio "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor."
OBJECT ENCODING profile   -- "hashtable"  (bio value > 64 bytes)
MEMORY USAGE profile      -- tăng đáng kể so với trước

Đây là gotcha phổ biến: chỉ một field có value lớn cũng đủ để chuyển cả hash sang hashtable, kể cả khi 99 field còn lại đều nhỏ.

7

Set Encoding: intset, listpack, hashtable

Set có ba encoding:

intset

Khi tất cả member đều là integer, Redis dùng intset — sorted array của integer, không có pointer overhead. Lookup dùng binary search O(log N). Cực kỳ tiết kiệm: 1000 integer 64-bit trong intset chỉ tốn 8 KB; trong hashtable tương đương tốn ~50-80 KB.

SADD ids 1 2 3 100 200
OBJECT ENCODING ids   -- "intset"

SADD ids "not-an-int"
OBJECT ENCODING ids   -- "hashtable"  (member không phải integer → intset không dùng được)

listpack (Redis 7.2+)

Set nhỏ không phải toàn integer. Thêm từ Redis 7.2 như một bước giữa intset và hashtable.

SADD tags "redis" "cache" "nosql"
OBJECT ENCODING tags   -- "listpack"  (Redis 7.2+, set nhỏ non-integer)

hashtable

Set lớn. Đảm bảo O(1) SISMEMBER, SADD, SREM.

Các config threshold:

set-max-intset-entries   512   # số member tối đa để giữ intset (default 512)
set-max-listpack-entries 128   # số member tối đa để giữ listpack non-integer (default 128)
set-max-listpack-value    64   # kích thước tối đa mỗi member (byte, default 64)

Logic chọn encoding theo thứ tự ưu tiên: nếu tất cả member là integer và số member ≤ set-max-intset-entries → intset. Nếu số member ≤ set-max-listpack-entries và mỗi member ≤ set-max-listpack-value → listpack. Còn lại → hashtable.

8

Sorted Set Encoding: listpack, skiplist

Sorted Set (ZSet) có hai encoding:

  • listpack: ZSet nhỏ. Member và score lưu xen kẽ (member₁, score₁, member₂, score₂, …), sắp xếp theo score. O(N) nhưng tiết kiệm memory.
  • skiplist: ZSet lớn. Kết hợp skiplist (cho range queries O(log N)) và hashtable (cho ZSCORE O(1)). Bài 24 đã phân tích chi tiết cấu trúc này.

Threshold config:

zset-max-listpack-entries 128   # số member tối đa (default 128)
zset-max-listpack-value    64   # kích thước tối đa mỗi member (byte, default 64)

Bài 24 đã đề cập encoding này trong phần ZSet; bài 28 tổng hợp vào bức tranh chung với các type khác.

ZADD scores 100 "alice" 200 "bob"
OBJECT ENCODING scores   -- "listpack"

-- Thêm đủ member để vượt threshold
-- (hoặc 1 member > 64 bytes)
OBJECT ENCODING scores   -- "skiplist"
9

listpack Là Gì — Cấu Trúc Bên Trong

listpack (Redis source: src/listpack.c) là một contiguous memory block với layout đơn giản:

[ total_bytes (4B) ][ num_elements (2B) ][ entry₁ ][ entry₂ ][ ... ][ 0xFF end marker ]

Mỗi entry có cấu trúc:

[ encoding+data ][ backlen ]

Trong đó encoding xác định cách interpret data (integer các kích thước, string ngắn, string dài). backlen là độ dài của entry hiện tại để cho phép traverse ngược (từ cuối về đầu).

Không có pointer giữa các entry. Để tìm entry thứ N, phải duyệt từ đầu — O(N). Nhưng khi N nhỏ (128 entries trở xuống), toàn bộ listpack thường nằm trong một vài cache line CPU, nên thực tế nhanh hơn traversal linked list có pointer miss.

listpack là sự thay thế cho ziplist (cấu trúc tương tự nhưng cũ hơn). ziplist có vấn đề cascade update: khi sửa một entry ở giữa, prevlen field của entry kế tiếp có thể cần thay đổi, dẫn đến chain update O(N) trong worst case. listpack loại bỏ prevlen, dùng backlen encode khác để fix điểm này. Redis 7.0 chuyển List, Hash, ZSet, Set sang listpack. Redis config key cũng đổi từ *-max-ziplist-* sang *-max-listpack-*.

10

Tại Sao listpack Tiết Kiệm Memory

So sánh overhead của listpack và hashtable cho cùng 100 field/value pair nhỏ (mỗi field ~10 bytes, value ~20 bytes):

hashtable

  • Mỗi entry: dictEntry struct = 3 pointer (key, value, next) = 24 bytes trên 64-bit.
  • Bucket array: thường là power-of-2, 100 entry → 128 bucket × 8 bytes pointer = 1024 bytes.
  • Load factor = 1 → Redis rehash khi vượt ngưỡng, tạm thời giữ 2 hashtable song song.
  • Key và value là SDS object riêng biệt: mỗi cái có header + buffer + null terminator.
  • Tổng cho 100 entry: khoảng 10-15 KB tùy fragmentation.

listpack

  • Không có pointer. Không có bucket array. Không có dictEntry struct.
  • Mỗi entry: encoding byte + data bytes + backlen (1-5 bytes).
  • 100 field/value pair ~30 bytes mỗi pair: listpack ~3-4 KB + listpack header < 10 bytes.
  • Tổng: khoảng 3-5 KB.

Tỷ lệ tiết kiệm: 3-5x với data nhỏ. Khi value còn nhỏ hơn (ví dụ integer field), tỷ lệ có thể đạt 10x. Đây là lý do tại sao giữ collection dưới threshold là việc đáng đầu tư khi memory là bottleneck.

Ví dụ đo thực tế:

import redis

r = redis.Redis()

# Hash nhỏ - sẽ dùng listpack
for i in range(100):
    r.hset("hash:small", f"field{i}", f"val{i}")

print(r.object_encoding("hash:small"))   # listpack
print(r.memory_usage("hash:small"))      # ~3000-4000 bytes

# Hash lớn - vượt threshold 128 entries → hashtable
for i in range(200):
    r.hset("hash:large", f"field{i}", f"val{i}")

print(r.object_encoding("hash:large"))   # hashtable
print(r.memory_usage("hash:large"))      # ~15000-20000 bytes
11

Encoding Conversion Một Chiều

Đây là điểm quan trọng nhất trong bài: encoding chuyển một chiều, không tự chuyển ngược lại.

Khi hash vượt threshold và chuyển sang hashtable, xóa bớt field sau đó cũng không làm nó quay về listpack:

-- Tạo hash nhỏ
HSET user:1 name "Alice" age "30"
OBJECT ENCODING user:1   -- "listpack"

-- Thêm 200 field → vượt threshold 128
-- (script giả lập: thêm field0..field199)
OBJECT ENCODING user:1   -- "hashtable"

-- Xóa bớt về còn 10 field
HDEL user:1 field10 field11 ... field199
OBJECT ENCODING user:1   -- vẫn "hashtable"  ← không tự chuyển ngược
MEMORY USAGE user:1      -- vẫn cao hơn listpack

Để "reset" về listpack, cách duy nhất là tạo lại key:

def reset_hash_to_listpack(r, key):
    """
    Tạo lại key để ép Redis chọn encoding tối ưu.
    Chỉ nên làm khi biết chắc data nhỏ hơn threshold.
    """
    data = r.hgetall(key)
    ttl = r.ttl(key)

    pipe = r.pipeline()
    pipe.delete(key)
    pipe.hset(key, mapping=data)
    if ttl > 0:
        pipe.expire(key, ttl)
    pipe.execute()

Hệ quả thực tế: Nếu một hash đôi khi có spike lên 200 field (vd job batch viết metadata tạm), nó sẽ bị stuck ở hashtable mãi mãi sau spike đó, kể cả khi bình thường chỉ có 10 field. Cần thiết kế để tránh spike vượt threshold, hoặc chấp nhận encoding hashtable và không kỳ vọng tiết kiệm memory từ listpack.

12

Tuning Threshold

Threshold có thể thay đổi runtime không cần restart:

CONFIG SET hash-max-listpack-entries 256
CONFIG SET hash-max-listpack-value 128
CONFIG SET set-max-intset-entries 1024
CONFIG SET zset-max-listpack-entries 256

Hoặc cố định trong redis.conf để persist qua restart.

Tăng threshold: khi nào hợp lý

Tăng hash-max-listpack-entries từ 128 lên 256 có nghĩa hash với 256 field vẫn dùng listpack thay vì hashtable. Tiết kiệm memory — nhưng có trade-off:

  • HGET/HSET trên hash 256 field với listpack là O(256) thay vì O(1) với hashtable.
  • Với field nhỏ, O(256) listpack scan vẫn nhanh (~microsecond) vì memory liên tục. Nhưng với field có value lớn hơn, hoặc khi hash gần threshold, latency tăng đo được.
  • Nếu tăng threshold quá cao (vd 10000), HGET trở thành O(10000) — đây là anti-pattern rõ ràng.

Trade-off cần đo

Trước khi tăng threshold, đo trong môi trường staging với dataset sát production:

-- Đo latency HGET với encoding hiện tại
DEBUG SLEEP 0   -- đảm bảo Redis không bị block
OBJECT ENCODING target:key
-- Dùng redis-benchmark hoặc application-level timing
import time
import redis

r = redis.Redis()

# Đo HGET latency ở các threshold khác nhau
def benchmark_hget(key, field, iterations=10000):
    start = time.perf_counter()
    for _ in range(iterations):
        r.hget(key, field)
    elapsed = time.perf_counter() - start
    avg_us = (elapsed / iterations) * 1e6
    enc = r.object_encoding(key)
    mem = r.memory_usage(key)
    print(f"encoding={enc}, memory={mem}B, avg_hget={avg_us:.2f}µs")

Default (128 entries, 64 bytes value) là điểm cân bằng đã được Redis team validate trên nhiều workload. Chỉ tune khi đo được benefit rõ ràng — đừng đoán.

13

Shard Collection Để Giữ Encoding Compact

Thay vì tăng threshold, một cách hiệu quả hơn là shard collection lớn thành nhiều key nhỏ.

Ví dụ: hash với 10000 field dùng hashtable. Thay bằng 100 hash, mỗi cái 100 field — tất cả dùng listpack:

SHARD_COUNT = 100

def shard_key(base: str, field: str) -> str:
    """Tính key shard dựa trên hash của field."""
    slot = hash(field) % SHARD_COUNT
    return f"{base}:{slot}"

def hset_sharded(r, base: str, field: str, value: str) -> None:
    key = shard_key(base, field)
    r.hset(key, field, value)

def hget_sharded(r, base: str, field: str) -> str | None:
    key = shard_key(base, field)
    return r.hget(key, field)

def hgetall_sharded(r, base: str) -> dict:
    """Lấy tất cả field từ tất cả shard."""
    result = {}
    for slot in range(SHARD_COUNT):
        key = f"{base}:{slot}"
        result.update(r.hgetall(key))
    return result

Trade-off của sharding:

  • Lợi: Memory giảm đáng kể (listpack thay hashtable), mỗi key nhỏ hơn → HGETALL nhanh hơn.
  • Hại: HGETALL toàn bộ collection cần N round-trip (hoặc pipeline). Logic phức tạp hơn. TTL cần set cho từng shard key. DEL cần loop qua tất cả shard.
  • Phù hợp khi: Workload chủ yếu là HGET/HSET trên field cụ thể (không cần HGETALL). Memory là bottleneck thực sự.
  • Không phù hợp khi: Cần atomic operation trên toàn bộ collection, hoặc cần HSCAN/HGETALL thường xuyên.

Pattern này được dùng nhiều trong các hệ thống cần lưu user attribute với số attribute lớn: thay vì một hash per user với 10000 field, dùng 100 shard hash, mỗi shard listpack.

14

Anti-patterns & Best Practices

Anti-patterns

  • Tăng threshold quá cao mà không đo: hash-max-listpack-entries 10000 nghĩa là HGET có thể là O(10000) linear scan. Tiết kiệm memory nhưng latency tăng mạnh trên hash gần threshold. Threshold default là điểm cân bằng đã validated — chỉ thay đổi khi có số đo thực tế.
  • Không biết encoding, bị tốn RAM bất ngờ: Hash lớn bị stuck ở hashtable nhưng code luôn assume listpack. Dùng OBJECT ENCODINGMEMORY USAGE định kỳ trên key quan trọng để detect sớm.
  • Một value lớn làm cả collection chuyển encoding: Thêm một field có value 100-byte vào hash — cả hash chuyển sang hashtable dù 99 field còn lại nhỏ. Validate kích thước value trước khi write, hoặc tách field lớn sang key riêng.
  • Assume encoding chuyển ngược khi xóa field: listpack → hashtable là một chiều. Sau khi xóa field, key không tự về listpack. Cần tạo lại key nếu muốn reset encoding.
  • Spike tạm thời vượt threshold để lại hậu quả lâu dài: Job batch ghi 200 field tạm vào hash → hash stuck hashtable mãi dù sau đó chỉ dùng 10 field. Thiết kế để tránh spike, hoặc dùng key riêng cho dữ liệu tạm thời.

Best practices

  • Chạy OBJECT ENCODINGMEMORY USAGE cho key quan trọng ở staging với dataset production-like trước khi rollout.
  • Giữ value trong hash/zset/set dưới *-max-listpack-value (64 bytes default). Field có value lớn hơn → tách sang key string riêng.
  • Giữ collection dưới *-max-listpack-entries (128 default). Nếu collection tự nhiên lớn hơn → cân nhắc shard.
  • Dùng intset bằng cách giữ Set member toàn integer khi có thể — tiết kiệm memory nhất cho tập ID.
  • Tune threshold chỉ sau khi đo được trade-off memory vs latency trên workload thực tế.
  • Tài liệu hóa kích thước kỳ vọng của collection trong code comment/design doc — dễ dàng audit khi memory tăng bất thường.
15

Bảng Tổng Hợp

Type Encoding compact Encoding lớn Config threshold Ghi chú
String int / embstr raw 44 bytes (hard-coded) embstr → raw khi modify
List listpack quicklist list-max-listpack-size 128 Thay ziplist từ Redis 7.0
Hash listpack hashtable hash-max-listpack-entries 128, hash-max-listpack-value 64 Vượt 1 trong 2 → hashtable
Set intset / listpack hashtable set-max-intset-entries 512, set-max-listpack-entries 128 intset chỉ khi toàn integer; listpack từ Redis 7.2
Sorted Set listpack skiplist zset-max-listpack-entries 128, zset-max-listpack-value 64 skiplist kết hợp hashtable cho ZSCORE O(1)
16

Tổng Kết & Quiz

Tổng kết

  • Redis chọn encoding nội bộ dựa trên kích thước thực tế: encoding compact (listpack, intset, embstr) khi nhỏ, encoding lớn (hashtable, skiplist, quicklist, raw) khi vượt threshold.
  • String: int (integer 64-bit), embstr (≤ 44 bytes, 1 allocation, read-only), raw (> 44 bytes hoặc đã modify).
  • listpack là contiguous memory block không có pointer overhead — thay thế ziplist từ Redis 7.0, fix cascade update issue.
  • Hash, ZSet, Set (non-integer), List đều dùng listpack khi nhỏ. Set toàn integer dùng intset — tiết kiệm memory nhất.
  • Encoding chuyển một chiều: listpack → hashtable khi vượt threshold, không tự quay về. Reset cần tạo lại key.
  • Một value lớn hơn threshold (default 64 bytes) đủ để chuyển cả collection sang encoding lớn.
  • Tuning threshold là trade-off memory vs CPU. Default 128 entries/64 bytes thường là tốt; chỉ thay đổi sau khi đo.
  • Shard collection lớn thành nhiều key nhỏ là cách giảm memory hiệu quả hơn tăng threshold — nhưng tăng độ phức tạp logic.
  • Công cụ: OBJECT ENCODING key, MEMORY USAGE key.

Quiz 5 câu

  1. Hash user:100 có 50 field, mỗi field/value đều nhỏ hơn 64 bytes. Đột nhiên một field avatar_data được ghi với value 200 bytes. Điều gì xảy ra với encoding của hash? Khi xóa avatar_data đi, encoding có trở về cũ không?
  2. String SET s "hello"OBJECT ENCODING s cho kết quả gì? Sau APPEND s " world" (tổng 11 bytes), encoding là gì? Giải thích tại sao.
  3. Bạn có một Set lưu user ID dạng integer (1, 2, 3, …, 500). OBJECT ENCODING cho kết quả gì? Nếu thêm một member là string "guest", encoding chuyển thành gì?
  4. Vì sao intset tiết kiệm memory hơn hashtable khi lưu 1000 integer? Tra cứu trong intset diễn ra bằng thuật toán nào?
  5. Bạn cần lưu 10000 user attribute dạng hash. Nếu giữ nguyên 1 hash lớn, encoding là gì? Nếu shard thành 100 hash mỗi cái 100 field, encoding có thể là gì? Nêu trade-off của shard.

Đáp án gợi ý

  1. Hash chuyển sang hashtable ngay khi avatar_data có value > 64 bytes (vượt hash-max-listpack-value). Sau khi xóa avatar_data, encoding không tự trở về listpack — conversion là một chiều. Muốn reset phải tạo lại key với dữ liệu còn lại.
  2. OBJECT ENCODING s"embstr" (5 bytes ≤ 44 bytes). Sau APPEND, tuy tổng chỉ 11 bytes, encoding chuyển sang "raw" vì embstr là read-only internally — bất kỳ modify nào đều trigger chuyển sang raw.
  3. 500 integer ≤ set-max-intset-entries (default 512) → "intset". Khi thêm "guest" (không phải integer), intset không thể dùng nữa → chuyển sang "listpack" (Redis 7.2+) nếu tổng member ≤ 128, hoặc "hashtable" nếu nhiều hơn.
  4. intset là sorted array of integer, không có pointer hay bucket. 1000 integer 64-bit = 8000 bytes + tiny header. hashtable cùng 1000 entry: 1000 × dictEntry (24 bytes) + bucket array (1024 × 8 bytes) ≈ 32 KB. Ratio ~4x. intset dùng binary search O(log N) cho SISMEMBER thay vì O(1) của hashtable — nhưng binary search trên array liên tục cache-friendly, thực tế nhanh với N nhỏ.
  5. 10000 field → hashtable. Shard 100 × 100 field → mỗi shard listpack (100 < 128 threshold). Memory giảm 3-5x. Trade-off lợi: memory thấp hơn, HGET nhanh trên shard nhỏ. Trade-off hại: HGETALL toàn bộ cần 100 round-trip (nên pipeline), TTL/DEL cần loop qua tất cả shard, logic phức tạp hơn, không có atomic operation trên toàn collection.

Bài tiếp theo

Bài 29 tổng hợp Module 2: cho bài toán thực tế, chọn data structure nào, và vì sao — mapping từ problem đến structure.

Tham khảo