Mục lục
- Mục Tiêu Bài Học
- Big Key Là Gì
- Vấn Đề Big Key Gây Ra
- Latency: Ước Lượng Cụ Thể
- Detection: redis-cli --bigkeys
- Detection: MEMORY USAGE + SCAN
- Detection: Element Count & DEBUG OBJECT
- Split Strategy 1: Hash Sharding
- Split Strategy 2: ZSet Bucketing
- Split Strategy 3: String Chunking
- Split Strategy 4: Externalize
- Xóa Big Key: UNLINK vs DEL
- Lazyfree & AOF Rewrite Impact
- Cluster: Migration Big Key
- Cap Size: Truncate Strategies
- Anti-patterns & Best Practices
- Tổng Kết & Quiz
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
UNLINKthayDELvà cấu hìnhlazyfreeđể 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.
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.
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.
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.
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:
SCANlà 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
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=100là 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 USAGEgiả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.
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.
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_shardedcầ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}"
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.
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.
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).
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:
- Ngay lập tức: xóa key khỏi keyspace (key không còn visible — thao tác O(1)).
- 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.
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:
- Redis fork child process.
- Child serialize toàn bộ keyspace sang file AOF mới.
- Big key có serialized size lớn → child viết lâu hơn.
- 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.
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.
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.
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 USAGEkey 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
UNLINKthayDELcho mọi key có khả năng lớn. - Cấu hình
lazyfree-lazy-expire yesvàlazyfree-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 -1phải kèm comment giải thích tại sao collection có size cap.
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ùngDEL. Cấu hìnhlazyfree-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
- Tại sao
HGETALLtrên hash 1M fields gây latency spike cho toàn bộ traffic Redis, kể cả client không dùng key đó? - So sánh
DELvàUNLINK: cơ chế khác nhau như thế nào và tại saoUNLINKtốt hơn cho big key? - 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_nsau khi tách. --bigkeysreport 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?- 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 ý
- Redis xử lý command trên single event loop.
HGETALL1M 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. DELgiải phóng memory đồng bộ trên event loop — blocking.UNLINKchỉ 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.- ZSet bucketing: chia user theo
bucket = user_id // BUCKET_SIZE, lưu vàoleaderboard:bucket:{bucket}. Hàmtop_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). - Cần chạy thêm
MEMORY USAGE+SCANvới ngưỡng byte size để tìm big key không phải collection lớn nhất (vì--bigkeyschỉ 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). - (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).
