Danh sách bài viết

Bài 107: Big Keys — Detect & Split

Big key là một trong những nguyên nhân phổ biến gây latency spike đột ngột trong production Redis. Bài này định nghĩa ngưỡng big key theo từng data type, phân tích cụ thể các tác động (blocking single thread, memory imbalance Cluster, network saturation, AOF rewrite chậm), trình bày bốn phương pháp phát hiện, và đi qua bốn chiến lược tách key kèm code Python mẫu. Cuối bài là danh sách anti-pattern và checklist best practice để ngăn big key xuất hiện từ đầu.

01/06/2026
16 phút đọc
0 lượt xem
1

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

  • Xác định ngưỡng big key theo từng data type và hiểu tại sao ngưỡng này mang tính chủ quan theo workload.
  • Phân tích bốn tác động chính: blocking event loop, memory imbalance Cluster, network saturation, AOF rewrite chậm.
  • Sử dụng được ba phương pháp phát hiện: --bigkeys, MEMORY USAGE + SCAN, DEBUG OBJECT.
  • Áp dụng chiến lược tách key phù hợp: hash sharding, ZSet bucketing, string chunking, externalize sang object storage.
  • Dùng UNLINK thay DEL và cấu hình lazyfree để tránh latency spike khi xóa.
  • Thiết lập cap size (MAXLEN, ZREMRANGEBYRANK) để ngăn key lớn dần không kiểm soát.
2

Big Key Là Gì

Không có một ngưỡng cố định cho mọi hệ thống. Các con số dưới đây là điểm khởi đầu thực tế — trên đó bắt đầu phát sinh vấn đề rõ ràng:

Data Type Ngưỡng cảnh báo thực tế Ghi chú
String > 10KB giá trị GET/SET copy toàn bộ value qua network
Hash > 5.000 fields HGETALL iterate toàn bộ; encoding upgrade lên hashtable
Set > 5.000 members SMEMBERS trả toàn bộ; set ops O(N)
ZSet > 5.000 members ZRANGE 0 -1 iterate toàn bộ; skiplist tốn memory
List > 10.000 elements LRANGE 0 -1 trả toàn bộ; lindex O(N) với list lớn
Stream > 100.000 entries XREAD full iterate chậm; memory tăng tuyến tính

Ngưỡng này mang tính subjective: instance có 16GB RAM và throughput thấp chịu được big key tốt hơn instance 2GB với 100k req/s. Điều quan trọng là theo dõi định kỳ thay vì chỉ đặt một con số cứng.

3

Vấn Đề Big Key Gây Ra

Latency spike — blocking single thread

Redis xử lý command trên single event loop. Command trên big key chạy đồng bộ từ đầu đến cuối — mọi command khác phải xếp hàng chờ. Một HGETALL trên hash 1M field mất ~500ms: toàn bộ traffic Redis của ứng dụng đứng lại trong khoảng thời gian đó.

Memory imbalance trong Redis Cluster

Redis Cluster phân phối keys theo hash slot. Nếu một số keys lớn bất thường tập trung trên cùng một node, node đó chiếm dụng RAM nhiều hơn hẳn các node còn lại. Khi memory của node đó chạm maxmemory, eviction xảy ra cục bộ trong khi các node khác còn rất nhiều dư. Rebalancing Cluster không giải quyết được vấn đề nếu big key không được tách.

Network bandwidth saturation

GET một string 10MB: Redis copy 10MB qua network socket cho mỗi lần gọi. Với 1000 req/s, đó là 10GB/s chỉ cho một key. Ngay cả 1MB/request ở tần suất cao cũng đủ bão hòa NIC 1Gbps.

Migration chậm — Cluster reshard

Khi Cluster reshard di chuyển key sang slot khác, big key phải serialize toàn bộ giá trị, truyền qua network, rồi xóa bên gửi. Với key lớn, thao tác này có thể vượt timeout mặc định (5 giây), gây lỗi migration và để slot ở trạng thái MIGRATING không xác định.

AOF rewrite

Khi BGREWRITEAOF chạy, Redis fork process và serialize toàn bộ keyspace. Big key làm thời gian serialize kéo dài và tăng peak memory trong thời gian fork (copy-on-write amplification). Chi tiết cơ chế AOF và THP interaction đã trình bày ở bài 105.

4

Latency: Ước Lượng Cụ Thể

Các con số dưới đây là ước lượng dựa trên hardware điển hình (instance ~4 core, mạng 10Gbps, Redis 7.x). Giá trị thực tế phụ thuộc vào phần cứng và tải hệ thống:

Command Kích thước Latency ước tính Tác động
GET 100MB string ~50ms + copy 100MB Network saturation; mọi command khác queue
HGETALL Hash 1M fields ~500ms Event loop blocked hoàn toàn
ZRANGE 0 -1 ZSet 1M members ~200ms Serialize + network transfer toàn bộ
SMEMBERS Set 500k members ~100ms Tương tự HGETALL

Trong khoảng thời gian block đó, toàn bộ command khác gửi đến instance phải chờ — bao gồm các GET/SET đơn giản vốn chỉ tốn <1ms. Điều này khiến p99 latency của toàn hệ thống tăng đột biến, ngay cả khi chỉ có một client thỉnh thoảng gọi command trên big key.

5

Detection: redis-cli --bigkeys

redis-cli --bigkeys là cách nhanh nhất để có cái nhìn tổng quan:

redis-cli --bigkeys

Output mẫu:

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest string found so far '"user:profile:1234"' with 512 bytes
[13.42%] Biggest hash   found so far '"orders:2024"' with 85234 fields
[27.10%] Biggest zset   found so far '"leaderboard:global"' with 1234567 members

-------- summary -------
Sampled 892341 keys in the keyspace!
Total key length in bytes is 18234123 (avg len 20.43)

Biggest   string found '"session:abc123"' has 8388608 bytes
Biggest     hash found '"orders:2024"' has 85234 fields
Biggest     zset found '"leaderboard:global"' has 1234567 members

Một số điểm cần lưu ý khi dùng --bigkeys:

  • Sampling, không exhaustive: tool dùng SCAN để duyệt keyspace nhưng chỉ report biggest per type. Có thể bỏ sót nhiều key lớn ở mức trung bình.
  • Chạy off-peak: SCAN là thao tác nặng trên keyspace lớn. Thêm flag -i 0.1 để thêm delay 100ms mỗi 100 SCAN call, giảm tải cho production:
redis-cli --bigkeys -i 0.1
  • Metric cho string là byte size, cho collection là element count — không phải byte size tổng. Hash 1M fields với mỗi field nhỏ vẫn được report theo element count.
  • Kết nối đến replica thay vì primary nếu lo ngại tải:
redis-cli -h replica-host -p 6379 --bigkeys -i 0.1
6

Detection: MEMORY USAGE + SCAN

MEMORY USAGE key trả về byte size thực tế Redis cấp phát cho key (bao gồm overhead encoding, metadata). Kết hợp với SCAN để tìm toàn bộ key vượt ngưỡng:

import redis

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

def find_big_keys(threshold=10 * 1024 * 1024):  # ngưỡng 10MB
    """
    Scan toàn keyspace, trả danh sách (key, size_bytes)
    cho mọi key có MEMORY USAGE > threshold.
    """
    cursor = 0
    big = []
    while True:
        cursor, keys = r.scan(cursor, count=100)
        if not keys:
            if cursor == 0:
                break
            continue
        pipe = r.pipeline()
        for k in keys:
            pipe.memory_usage(k)
        sizes = pipe.execute()
        for k, size in zip(keys, sizes):
            if size and size > threshold:
                big.append((k, size))
        if cursor == 0:
            break
    return sorted(big, key=lambda x: x[1], reverse=True)

big_keys = find_big_keys()
for key, size in big_keys:
    print(f"{key}: {size / 1024 / 1024:.2f} MB")

Lưu ý khi sử dụng:

  • SCAN count=100 là gợi ý, không phải đảm bảo — Redis trả về số key gần với 100 nhưng có thể nhiều hoặc ít hơn.
  • Pipeline batch MEMORY USAGE giảm round-trip đáng kể so với gọi từng cái một.
  • Trên keyspace hàng triệu key, scan toàn bộ mất nhiều phút — chạy từ replica hoặc off-peak.
  • Thêm count=1000 để tăng tốc nếu keyspace lớn và chấp nhận tải cao hơn.
7

Detection: Element Count & DEBUG OBJECT

Đếm element theo type

Với collection, byte size không phải lúc nào cũng nói lên vấn đề — 1M field nhỏ vẫn gây blocking khi HGETALL. Dùng lệnh count tương ứng:

# Từ redis-cli
HLEN key         # Hash: số fields
LLEN key         # List: số elements
SCARD key        # Set: số members
ZCARD key        # ZSet: số members
XLEN key         # Stream: số entries

Script Python để tìm collection có element count vượt ngưỡng:

COUNT_CMDS = {
    'hash': 'hlen',
    'list': 'llen',
    'set':  'scard',
    'zset': 'zcard',
    'stream': 'xlen',
}
THRESHOLDS = {
    'hash': 5_000,
    'list': 10_000,
    'set':  5_000,
    'zset': 5_000,
    'stream': 100_000,
}

def find_big_collections(r):
    cursor = 0
    results = []
    while True:
        cursor, keys = r.scan(cursor, count=200)
        for k in keys:
            ktype = r.type(k)
            if ktype in COUNT_CMDS:
                count = getattr(r, COUNT_CMDS[ktype])(k)
                threshold = THRESHOLDS.get(ktype, 0)
                if count > threshold:
                    results.append((k, ktype, count))
        if cursor == 0:
            break
    return results

DEBUG OBJECT

DEBUG OBJECT key cho thấy serialized size và encoding hiện tại:

DEBUG OBJECT user:profile:1234
# Value at:0x... refcount:1 encoding:listpack serializedlength:4821 lru:... lru_seconds_idle:3

serializedlength là kích thước sau khi serialize (gần với kích thước RDB/AOF trên disk). Giá trị này nhỏ hơn MEMORY USAGE vì không tính overhead allocator và metadata trong RAM. Dùng để ước lượng impact lên persistence, không thay thế MEMORY USAGE cho việc đo RAM.

8

Split Strategy 1: Hash Sharding

Thay vì 1 Hash 1M fields, chia thành N Hash nhỏ hơn. Shard được chọn bằng hash của field name:

SHARDS = 100  # điều chỉnh theo kích thước dataset

def hset_sharded(r, base_key, field, value):
    shard = hash(field) % SHARDS
    return r.hset(f"{base_key}:shard:{shard}", field, value)

def hget_sharded(r, base_key, field):
    shard = hash(field) % SHARDS
    return r.hget(f"{base_key}:shard:{shard}", field)

def hdel_sharded(r, base_key, field):
    shard = hash(field) % SHARDS
    return r.hdel(f"{base_key}:shard:{shard}", field)

def hgetall_sharded(r, base_key):
    """Đọc toàn bộ fields từ tất cả shards — dùng pipeline."""
    pipe = r.pipeline()
    for i in range(SHARDS):
        pipe.hgetall(f"{base_key}:shard:{i}")
    result = {}
    for partial in pipe.execute():
        result.update(partial)
    return result

Trade-off của hash sharding:

  • Ưu điểm: mỗi shard độc lập, không blocking nhau; phân tán đều qua nhiều slot trong Cluster.
  • Nhược điểm: hgetall_sharded cần N pipeline calls — tốn hơn 1 HGETALL duy nhất khi collection nhỏ.
  • Lưu ý Cluster: các shard key rải vào hash slot khác nhau, không thể dùng multi-key command (MGET, pipeline across slots) trực tiếp — cần route theo từng shard.

Với key như user:profile:{user_id}, pattern phổ biến là sharding theo user_id:

def key_for_user(user_id):
    shard = user_id % SHARDS
    return f"user:profile:{user_id}:shard:{shard}"
9

Split Strategy 2: ZSet Bucketing

Leaderboard toàn cầu 10M user không thể lưu trong một ZSet. Chia theo bucket, mỗi bucket chứa một tập con:

BUCKET_SIZE = 1_000_000  # 1M user mỗi bucket

def zadd_bucketed(r, base_key, user_id, score):
    bucket = user_id // BUCKET_SIZE
    return r.zadd(f"{base_key}:bucket:{bucket}", {str(user_id): score})

def zscore_bucketed(r, base_key, user_id):
    bucket = user_id // BUCKET_SIZE
    return r.zscore(f"{base_key}:bucket:{bucket}", str(user_id))

def top_n_bucketed(r, base_key, n, num_buckets):
    """Lấy top N trên toàn bộ buckets — merge + sort."""
    candidates = []
    pipe = r.pipeline()
    for b in range(num_buckets):
        pipe.zrevrange(f"{base_key}:bucket:{b}", 0, n - 1, withscores=True)
    for partial in pipe.execute():
        candidates.extend(partial)
    # Sort theo score giảm dần, lấy N đầu
    candidates.sort(key=lambda x: x[1], reverse=True)
    return candidates[:n]

Chiến lược bucketing phù hợp khi bucket key có thể xác định từ member ID. Nếu không thể (member phân bố ngẫu nhiên), cần dùng hash sharding tương tự phần 8, nhưng top_n query phải scan toàn bộ shard — chi phí tăng tuyến tính.

10

Split Strategy 3: String Chunking

Khi cần cache một value lớn (JSON 10MB, binary blob), chia thành nhiều chunk key:

CHUNK_SIZE = 1 * 1024 * 1024  # 1MB mỗi chunk

def write_chunked(r, base_key, data: bytes, ttl=3600):
    """
    Ghi data thành nhiều chunk + meta key.
    Dùng pipeline để atomic hơn (không đảm bảo atomicity hoàn toàn
    — dùng Lua script nếu cần atomicity nghiêm ngặt).
    """
    chunks = [data[i:i + CHUNK_SIZE] for i in range(0, len(data), CHUNK_SIZE)]
    num_chunks = len(chunks)
    pipe = r.pipeline()
    # Ghi từng chunk
    for idx, chunk in enumerate(chunks):
        pipe.set(f"{base_key}:chunk:{idx}", chunk, ex=ttl)
    # Ghi meta (số chunk, size)
    pipe.set(f"{base_key}:meta", num_chunks, ex=ttl)
    pipe.execute()

def read_chunked(r, base_key) -> bytes | None:
    """Đọc meta rồi đọc toàn bộ chunk."""
    num_chunks = r.get(f"{base_key}:meta")
    if num_chunks is None:
        return None
    num_chunks = int(num_chunks)
    pipe = r.pipeline()
    for idx in range(num_chunks):
        pipe.get(f"{base_key}:chunk:{idx}")
    parts = pipe.execute()
    if any(p is None for p in parts):
        return None  # một chunk đã expire hoặc bị xóa
    return b"".join(parts)

def delete_chunked(r, base_key):
    num_chunks = r.get(f"{base_key}:meta")
    if num_chunks is None:
        return
    num_chunks = int(num_chunks)
    pipe = r.pipeline()
    pipe.unlink(f"{base_key}:meta")
    for idx in range(num_chunks):
        pipe.unlink(f"{base_key}:chunk:{idx}")
    pipe.execute()

Nhược điểm của chunking: đọc cần N+1 round-trip (1 meta + N chunk). Nếu một chunk expire trước các chunk còn lại (race condition TTL), toàn bộ data không đọc được. Đặt TTL giống nhau và viết meta key sau cùng để giảm window này.

11

Split Strategy 4: Externalize

Khi value thực sự lớn (hàng chục MB, binary asset, ML model checkpoint), Redis không phải nơi lưu trữ phù hợp. Pattern externalize:

  • Lưu value vào object storage (S3, GCS, MinIO) hoặc database.
  • Redis chỉ giữ pointer (URL, object key, ID) — thường vài chục byte.
  • Application đọc pointer từ Redis, tải value từ storage.
import boto3
import json

s3 = boto3.client('s3')
BUCKET = 'my-app-cache'

def store_large(r, key, data: bytes, ttl=3600):
    s3_key = f"redis-ext/{key}"
    s3.put_object(Bucket=BUCKET, Key=s3_key, Body=data)
    pointer = json.dumps({'backend': 's3', 'key': s3_key})
    r.set(f"ext:{key}", pointer, ex=ttl)

def load_large(r, key) -> bytes | None:
    raw = r.get(f"ext:{key}")
    if raw is None:
        return None
    meta = json.loads(raw)
    if meta['backend'] == 's3':
        resp = s3.get_object(Bucket=BUCKET, Key=meta['key'])
        return resp['Body'].read()
    return None

Khi nào dùng externalize thay vì chunking:

  • Value > vài chục MB — chunking vẫn tốn RAM Redis.
  • Value ít thay đổi — object storage phù hợp cho read-heavy, write-rare.
  • Dữ liệu cần audit trail hoặc versioning (S3 versioning).
12

Xóa Big Key: UNLINK vs DEL

DEL key xóa key đồng bộ: Redis giải phóng toàn bộ bộ nhớ ngay lập tức trên event loop. Với big key, thao tác này block event loop trong thời gian đáng kể.

UNLINK key (Redis 4.0+) tách thành hai bước:

  1. Ngay lập tức: xóa key khỏi keyspace (key không còn visible — thao tác O(1)).
  2. Background: giải phóng bộ nhớ trong background thread — không block event loop.
# Không dùng cho big key
DEL user:data:bigblob

# Luôn dùng UNLINK cho big key
UNLINK user:data:bigblob

Trong Python với redis-py:

# DEL — blocking
r.delete("user:data:bigblob")

# UNLINK — async reclaim, Redis 4.0+
r.unlink("user:data:bigblob")

Lưu ý: UNLINK không đảm bảo memory được trả lại ngay. Nếu cần biết chắc memory đã được giải phóng (ví dụ trước khi scale down), kiểm tra INFO memory sau một khoảng thời gian ngắn.

13

Lazyfree & AOF Rewrite Impact

Lazyfree configuration

Ngoài UNLINK explicit, Redis có thể giải phóng memory bất đồng bộ trong các trường hợp khác thông qua lazyfree-* config:

# redis.conf (Redis 4.0+, default Redis 7: lazy-expire = yes)
lazyfree-lazy-eviction   yes   # eviction async
lazyfree-lazy-expire     yes   # key expire async
lazyfree-lazy-server-del yes   # DEL internal (flush, rename) async
replica-lazy-flush       yes   # FLUSHALL replica async

lazyfree-lazy-expire yes quan trọng nhất cho big key: khi key hết TTL, Redis không block event loop để giải phóng memory — đẩy xuống background thread. Mặc định đã là yes từ Redis 7.x.

AOF rewrite

Khi BGREWRITEAOF chạy:

  1. Redis fork child process.
  2. Child serialize toàn bộ keyspace sang file AOF mới.
  3. Big key có serialized size lớn → child viết lâu hơn.
  4. Trong thời gian fork, copy-on-write: mọi write từ parent đến big key page → copy cả page → peak memory tăng.

Cách giảm impact: tách big key trước khi để AOF rewrite chạy tự động; điều chỉnh auto-aof-rewrite-percentage để tránh rewrite vào giờ cao điểm.

14

Cluster: Migration Big Key

Khi Redis Cluster reshard di chuyển slot chứa big key:

redis-cli --cluster reshard <host>:<port> \
  --cluster-from <node-id> \
  --cluster-to <node-id> \
  --cluster-slots <num-slots> \
  --cluster-yes

Quá trình migration cho từng key: MIGRATE command serialize key, gửi qua network, xóa bên gửi. Big key có thể vượt timeout mặc định (5 giây) — lệnh thất bại, slot kẹt ở trạng thái MIGRATING.

Xử lý:

  • Tách big key thành các key nhỏ trước khi reshard.
  • Tạm thời tắt replica auto-migration trong lúc xử lý: CONFIG SET cluster-allow-replica-migration no.
  • Nếu migration đã kẹt, dùng CLUSTER SETSLOT <slot> STABLE để reset, sau đó retry với key đã được tách.
15

Cap Size: Truncate Strategies

Nhiều big key xuất hiện vì không có giới hạn kích thước khi append. Giải pháp tốt nhất là cap ngay từ đầu:

List: LTRIM sau mỗi LPUSH/RPUSH

-- Lua: push và giữ tối đa MAX_LEN phần tử (atomic)
local key = KEYS[1]
local value = ARGV[1]
local max_len = tonumber(ARGV[2])
redis.call('LPUSH', key, value)
redis.call('LTRIM', key, 0, max_len - 1)
return redis.call('LLEN', key)

Stream: MAXLEN

# ~ không đảm bảo exact, nhưng hiệu quả hơn MAXLEN exact
XADD mystream MAXLEN ~ 10000 * field1 value1

ZSet: giữ top N

# Sau khi ZADD, xóa bớt member rank thấp
ZADD leaderboard 1500 "user:1001"
ZREMRANGEBYRANK leaderboard 0 -10001  # giữ top 10000

Pattern append-only key

Log/audit trail thường dùng List và push liên tục. Nếu không cap:

  • Key lớn dần không giới hạn — phát hiện muộn khi đã đến hàng triệu entries.
  • Fix: dùng key per-period (ví dụ audit:2026-06, audit:2026-07) với TTL, hoặc cap MAXLEN và archive phần cũ sang persistent storage.
16

Anti-patterns & Best Practices

Anti-patterns

  • HGETALL trên hash không giới hạn: khi hash chỉ có vài chục field thì ổn; khi đã có 100k+ fields mà vẫn dùng HGETALL là blocking event loop. Không có cơ chế nào tự động cảnh báo điều này nếu bạn không monitor.
  • Persist collection vô giới hạn: counter, log, event stream dùng List/ZSet mà không đặt MAXLEN → grow forever. Phổ biến trong analytics pipeline.
  • DEL thay vì UNLINK: DEL big key block hàng trăm ms — đủ gây latency spike đột ngột trong production.
  • Migrate big key trong Cluster không chuẩn bị: migration timeout → slot kẹt MIGRATING → một phần keyspace không accessible.
  • Không monitor key size: big key thường phát triển dần dần trong nhiều tuần — không có alert thì phát hiện khi đã có vấn đề production.
  • Cache toàn bộ large object: nhét nguyên response 50MB vào cache vì "cho nhanh" — network saturation khi nhiều client đọc đồng thời.

Best practices

  • Chạy redis-cli --bigkeys -i 0.1 định kỳ (tuần/tháng) từ replica, so sánh kết quả theo thời gian.
  • Đặt alert khi MEMORY USAGE key vượt ngưỡng — tích hợp vào monitoring pipeline.
  • Mọi collection phải có size cap ngay từ khi thiết kế (MAXLEN, ZREMRANGEBYRANK).
  • Dùng UNLINK thay DEL cho mọi key có khả năng lớn.
  • Cấu hình lazyfree-lazy-expire yeslazyfree-lazy-eviction yes.
  • Tách hash > 5k field thành shards trước khi đưa vào Cluster.
  • Externalize value > vài chục MB sang object storage; Redis chỉ giữ pointer.
  • Code review: mọi command HGETALL, SMEMBERS, ZRANGE 0 -1, LRANGE 0 -1 phải kèm comment giải thích tại sao collection có size cap.
17

Tổng Kết & Quiz

Tổng kết

  • Big key: String > 10KB, Hash/Set/ZSet > 5k elements, List > 10k elements — ngưỡng mang tính subjective theo workload.
  • Bốn tác động chính: blocking event loop (latency spike), memory imbalance Cluster, network saturation, migration timeout.
  • Ba phương pháp phát hiện: --bigkeys (overview nhanh), MEMORY USAGE + SCAN (byte-level exhaustive), đếm element count per type.
  • Chiến lược tách: hash sharding, ZSet bucketing, string chunking, externalize sang object storage — chọn theo data type và access pattern.
  • Xóa: dùng UNLINK, không dùng DEL. Cấu hình lazyfree-lazy-expire yes.
  • Phòng ngừa: cap size ngay từ đầu (MAXLEN, ZREMRANGEBYRANK), monitor định kỳ, alert khi vượt ngưỡng.

Quiz 5 câu

  1. Tại sao HGETALL trên hash 1M fields gây latency spike cho toàn bộ traffic Redis, kể cả client không dùng key đó?
  2. So sánh DELUNLINK: cơ chế khác nhau như thế nào và tại sao UNLINK tốt hơn cho big key?
  3. Bạn có một ZSet leaderboard 10M members. Chiến lược nào phù hợp để tách? Viết đại cương hàm top_n sau khi tách.
  4. --bigkeys report biggest ZSet có 50k members. Bạn còn cần kiểm tra gì thêm để đảm bảo không có big key nào bị bỏ sót?
  5. Một List dùng để lưu audit log đang có 8M entries và vẫn đang tăng. Nêu hai cách fix và trade-off của mỗi cách.

Đáp án gợi ý

  1. Redis xử lý command trên single event loop. HGETALL 1M fields chạy đồng bộ từ đầu đến cuối (~500ms) mà không yield. Trong khoảng thời gian đó, toàn bộ command khác — kể cả GET/SET đơn giản — phải xếp hàng chờ. Đây là tác động cơ bản của single-threaded event loop.
  2. DEL giải phóng memory đồng bộ trên event loop — blocking. UNLINK chỉ tách key khỏi keyspace (O(1), không blocking), rồi đẩy việc giải phóng memory thực tế sang background thread. Với big key, UNLINK không để event loop chờ quá trình free memory có thể mất hàng trăm ms.
  3. ZSet bucketing: chia user theo bucket = user_id // BUCKET_SIZE, lưu vào leaderboard:bucket:{bucket}. Hàm top_n: dùng pipeline đọc top-N từ mỗi bucket, gom candidates, sort theo score giảm dần, trả N đầu. Chi phí O(B * N log N) với B là số bucket — chấp nhận được nếu B nhỏ (10–20 bucket).
  4. Cần chạy thêm MEMORY USAGE + SCAN với ngưỡng byte size để tìm big key không phải collection lớn nhất (vì --bigkeys chỉ report 1 key lớn nhất per type). Ngoài ra chạy element count scan để tìm collection trung bình nhưng nhiều về số lượng (nhiều key 10k members không bị bắt nếu có key 50k lớn hơn).
  5. (1) Cap MAXLEN + LTRIM: thêm Lua script push-and-trim, giữ N entries gần nhất. Trade-off: entries cũ bị mất — phù hợp nếu audit log chỉ cần window gần đây. (2) Key per-period (audit:2026-06) với TTL: mỗi tháng một key, key cũ hết TTL tự xóa. Trade-off: query span nhiều tháng cần đọc nhiều key; key tháng hiện tại vẫn có thể lớn nếu volume cao — cần kết hợp với cap.

Bài tiếp theo

Bài 108 chuyển sang hot key: phát hiện key bị đọc với tần suất cao bất thường, tác động khác với big key, và các kỹ thuật giảm tải (local cache, key replication, read replica routing).

Tham khảo