Thiết kế tên key Redis — quy tắc đầy đủ, số đo thật, và những chỗ hay hỏng

Bài toán: Redis không có bảng, không có schema, không có namespace. Thứ duy nhất giúp người vận hành biết một key là gì, của ai, xoá được không, nằm ở node nào trong Cluster — chính là tên key. Đặt tên sai thì không báo lỗi ngay. Nó nổ trễ: script xoá nhầm dữ liệu, RAM tăng không rõ do đâu, chuyển sang Cluster thì Lua báo CROSSSLOT, deploy xong một nửa server đọc cache sai format.

Trả lời ngắn (30 giây):

  1. Format cố định: <domain>:<entity>:<id>[:<phần con>][:v<N>], chữ thường, : chỉ dùng làm dấu phân cách. Ví dụ order:9812:items, category:children:v2:15.
  2. Mỗi đoạn phải "sạch": dùng id, không dùng chuỗi người dùng nhập. Buộc phải dùng chuỗi tự do (từ khoá tìm kiếm, URL) thì băm nó (sha1/xxh3), đừng "lọc bỏ ký tự lạ" — lọc bỏ làm hai chuỗi khác nhau thành một key.
  3. Tên key tốn RAM thật: đo được 52 byte/key với tên 7 ký tự, 95 byte/key với tên 47 ký tự. Một triệu key thì chênh ~43 MB. Ngắn vừa đủ đọc, không viết tắt đến mức không ai hiểu.
  4. Cluster: chỉ dùng hash tag {…} khi cần lệnh nhiều key trên cùng một thực thể, và tag phải là id có nhiều giá trị ({order:9812}), không phải hằng số ({global}).
  5. Version nằm trong tên khi đổi format value, và mọi key cache phải có TTL, để key cũ tự biến mất.

Mọi output trong bài là chạy thật: Redis 8.8.0 (container tạm, một bản thường và một bản bật cluster-enabled), Laravel 12.64.0 + phpredis 6.3.0 chạy code của project laravel-livewire nối vào Redis tạm đó. Chỗ nào chưa verify có ghi rõ. Phần "blog nói gì" có dẫn nguồn ở cuối bài, kèm nhận xét chỗ nào blog nói chưa đúng.

Thuật ngữ dùng trong bài

Thuật ngữNghĩa ngắn
keyspaceToàn bộ key trong một database của Redis. Bên dưới là một bảng băm (dict) lớn.
binary-safeTên key là dãy byte bất kỳ. Redis không hiểu chữ hoa/thường, Unicode hay dấu cách — so từng byte.
namespace (quy ước)Redis không có namespace thật. "Namespace" chỉ là phần đầu tên key giống nhau, vd order:.
separatorKý tự phân cách các đoạn trong tên key. Quy ước chung là dấu :.
cardinalitySố giá trị khác nhau một đoạn có thể nhận. user_id cardinality cao, status cardinality thấp.
hash slot / hash tagCluster chia key vào 16384 slot. Nếu tên có {…} thì chỉ phần trong ngoặc được băm — gọi là hash tag.
CROSSSLOTLỗi Cluster trả về khi một lệnh (hoặc Lua script) đụng các key thuộc slot khác nhau.
hot slot / hot keyMột slot hoặc một key nhận phần lớn traffic, làm một node quá tải dù cluster còn nhiều node rảnh.
glob patternMẫu dùng trong SCAN MATCH, KEYS, ACL: *, ?, [abc].
ACL key patternLuật ~pattern trong ACL: user Redis chỉ được đụng key khớp mẫu.
orphan keyKey "mồ côi": không còn code nào đọc/xoá nó, nhưng vẫn chiếm RAM.

1. Nó là gì / chạy thế nào

1.1. Redis tìm key thế nào — và vì sao tên là thứ duy nhất bạn có

Mỗi database của Redis là một bảng băm. GET order:1 băm chuỗi byte order:1, nhảy vào ô tương ứng, so sánh từng byte. Không có index phụ, không có "thư mục", không có cột type. Hệ quả:

SET user:Alice 1 ; SET user:alice 2 ; SET "user:alice " 3
SET tag:café (NFC) ; SET tag:café (NFD)
DBSIZE                 → 5 key (cộng các key khác trong test)
SCAN ... | cat -A
tag:cafM-CM-)$         ← é = 2 byte  C3 A9
tag:cafeM-LM-^A$       ← e + dấu = 1 + 2 byte  CC 81

Nghĩa là mọi đoạn lấy từ dữ liệu người dùng phải được chuẩn hoá (chữ thường, trim, Unicode NFC) trước khi ghép vào tên key — hoặc tốt hơn, không dùng chúng mà dùng id.

1.2. Giải phẫu một tên key

flowchart LR
  P["prefix app
(framework tự thêm)
laravel-database-"] --> D["domain
catalog / order / forum"] D --> E["entity / mục đích
category:children"] E --> V["version
v2"] V --> I["id
15"] I --> S["phần con
items / lock / count"]
ĐoạnĐể làm gìVí dụBắt buộc?
prefix appTách key của các app dùng chung một Redis; làm ACL theo applaravel-database-, shop:Chỉ khi nhiều app dùng chung. Framework thường tự thêm
domainBiết key thuộc module/team nào, ai chịu trách nhiệmcatalog, forumNên có khi codebase có nhiều module
entity / mục đíchBiết value là gìcategory:children, home:popularCó
versionĐổi format value mà không đọc nhầm dữ liệu cũv2Có với key cache chứa object/JSON
idPhân biệt từng thực thể15, {order:9812}Có (trừ key toàn cục)
phần conCác key khác nhau quanh cùng một thực thểitems, lockTuỳ

Ví dụ từ code thật của project laravel-livewire (app/Services). Lưu ý {$…} ở đây là chèn biến của PHP, không phải hash tag (mục 2.9):

category:subtree:{$category->id}
category:children:v2:{$parentId}          // v2: nav view-model đổi slug → id (issue #54)
home:popular:{$limit}
product:variants:{$product->id}
price:v1:{$product->id}:{$product->pricing_version}   // version theo dữ liệu, xem mục 2.5
cat:{$categoryId}:price:{$min}-{$max}     // CategoryBrowseCatalog: band giá lấy từ config

Chỗ tốt: format đều, có version, tên key được dựng trong một method cacheKey() riêng. Chỗ cần để ý: home:popular:{$limit} — nếu $limit đến từ request thì số key không bị chặn (mục 2.7). Trong project, $limit là hằng số gọi từ code nên ổn.

1.3. Tên key tốn RAM thật — số đo

Mỗi key phải lưu nguyên chuỗi tên (trong object key/entry của dict). Test: 100.000 key SET <tên> 1, đo used_memory trước/sau, chia cho số key.

Tên key (ví dụ)Độ dài tênbyte / keyQuy ra 1 triệu key
u:12345~752~50 MB
user:session:12345~1860~57 MB
order:<uuid 36 ký tự>4279~75 MB
myshop-production-cache-user-session-data:12345~4795~91 MB
tên ~91 ký tự~91132~126 MB
MEMORY USAGE user:1                                          → 32
MEMORY USAGE myshop-production-cache-user-session-data:1     → 67

Đọc số này thế nào:

1.4. Tên key quyết định node trong Redis Cluster

Cluster tính slot = CRC16(tên) mod 16384. Nếu tên có { và sau đó có }, và giữa hai ngoặc có ít nhất 1 ký tự, thì chỉ phần trong ngoặc đầu tiên được băm (theo HASH_SLOT trong cluster spec).

flowchart TD
  K["Tên key"] --> Q1{"Có dấu { ?"}
  Q1 -- "không" --> H1["Băm cả tên"]
  Q1 -- "có" --> Q2{"Có } phía sau
và ở giữa không rỗng?"} Q2 -- "không" --> H1 Q2 -- "có" --> H2["Chỉ băm phần giữa
{ đầu tiên và } đầu tiên sau nó"]

Chạy CLUSTER KEYSLOT trên Redis 8.8 bật cluster mode:

Tên keySlotGhi chú
user:123:profile8490Không tag → băm cả tên
user:{123}:profile5970Cùng tag 123 → cùng slot
user:{123}:cart5970
{user:123}:cart12893Tag là user:123, khác 123 → khác slot
user:{}:x14781Ngoặc rỗng → băm cả tên
user:{1238567Thiếu } → băm cả tên
{a}{b} và {a}x15495Chỉ lấy cặp ngoặc đầu
user:{789:456}:meta và 789:45614151Tag có dấu : vẫn đúng — lấy tới } đầu tiên
laravel-database-{order:9}:items / :lock14638Prefix ở ngoài ngoặc không ảnh hưởng
queues:default / queues:default:reserved11255 / 5202Khác slot → Lua của queue lỗi CROSSSLOT
queues:{default} / queues:{default}:reserved5718 / 5718Vì vậy docs Laravel bảo đặt tên queue là {default} khi chạy Cluster

Thêm: SELECT 1 trên node cluster → ERR SELECT is not allowed in cluster mode. Tách dữ liệu bằng số DB (REDIS_DB=8, REDIS_CACHE_DB=9) sẽ không chuyển sang Cluster được — chỉ tách bằng tên key mới đi theo được.

1.5. Tên key mà Laravel thật sự ghi vào Redis

Đây là phần hay bị bỏ qua nhất: bạn viết Cache::put('home:popular:8') nhưng tên thật trong Redis không phải home:popular:8. Test với .env của project (REDIS_DB=8, REDIS_CACHE_DB=9, CACHE_PREFIX=clicon_local), code:

Cache::store('redis')->put('home:popular:8', 'x', 60);
Redis::set('forum:liked:{7}:3', 1);
Cache::store('redis')->lock('order:9:pay', 10)->get();
RateLimiter::hit('storefront|1.2.3.4', 60);
Cache::store('redis')->tags(['product:5'])->put('card', 'x', 60);
Queue::connection('redis')->push('App\\Jobs\\Nope');
Queue::connection('redis')->later(60, 'App\\Jobs\\Nope');

Output redis-cli --scan từng DB:

== db8 (connection default)
laravel-database-clicon_localorder:9:pay          ← lock nằm ở DB 8, không phải DB cache
laravel-database-forum:liked:{7}:3
laravel-database-queues:default
laravel-database-queues:default:delayed
laravel-database-queues:default:notify

== db9 (connection cache)
laravel-database-clicon_localhome:popular:8
laravel-database-clicon_localstorefront|1.2.3.4
laravel-database-clicon_localstorefront|1.2.3.4:timer
laravel-database-clicon_localece4b669a7fbe4664e188bb670019a45566ffa83:card
laravel-database-clicon_localtag:product:5:entries   → type=zset ttl=-1
flowchart LR
  A["Cache::put('home:popular:8')"] --> B["RedisStore thêm cache.prefix
clicon_local (không tự thêm dấu phân cách)"] B --> C["phpredis OPT_PREFIX thêm
database.redis.options.prefix
laravel-database-"] C --> D["Tên thật trong Redis DB 9
laravel-database-clicon_localhome:popular:8"]

Năm điều rút ra (đều đã thấy trong output):

  1. Hai lớp prefix chồng nhau: prefix của connection Redis (laravel-database-) + prefix của cache (clicon_local). Code RedisStore::setPrefix() ở Laravel 12.64 gán thẳng, không tự thêm dấu phân cách. .env ghi CACHE_PREFIX=clicon_local (thiếu - hoặc : cuối) → tên dính liền clicon_localhome. Nguy hiểm: prefix app + key le:1 trùng với prefix apple:... khi SCAN/ACL theo mẫu. Config mặc định của Laravel có dấu cuối (-cache-), chỉ hỏng khi ghi đè bằng env.
  2. Lock không nằm cùng DB với cache: lock_connection mặc định là default (DB 8). cache:clear xoá DB 9 nhưng lock vẫn còn — đúng ý đồ, nhưng người đi tìm key lock trong DB cache sẽ không thấy.
  3. Cache tags làm tên key không đọc được: key thật là sha1(tên tag):card. Không SCAN theo tag được. Tag còn tạo ZSET tag:product:5:entries không có TTL — phải chạy php artisan cache:prune-stale-tags theo lịch, nếu không nó phình dần.
  4. Redis::keys() trả tên đã có prefix, truyền lại vào Redis::del() thì bị thêm prefix lần nữa: Redis::del('laravel-database-forum:liked:{7}:3') → 0 (không xoá gì, không báo lỗi). Phải cắt prefix trước khi dùng lại.
  5. Có đường đi vòng qua prefix: rawCommand('SET','raw:key',…) ghi ra raw:key không prefix. Trong Lua, key truyền qua KEYS được prefix (laravel-database-outer:7), nhưng key tự ghép trong script ('inner:' .. ARGV[1]) thì không (inner:7). Key đó nằm ngoài "namespace" của app: ACL theo prefix chặn nó, SCAN theo prefix bỏ sót nó, và trên Cluster nó là lỗi vì không khai báo trong KEYS.

1.6. Tên key là "công cụ vận hành": SCAN, ACL, đo RAM

SCAN theo prefix không nhanh hơn — vẫn phải đi qua mọi key. Test: 1.000.100 key, chỉ 100 key khớp tenant:42:*:

SCAN cursor MATCH tenant:42:* COUNT 1000   → 1000 lần gọi mới quét hết, tìm được 100 key
SCAN 0 MATCH tenant:42:* COUNT 1000000      → 100 key, SLOWLOG: 553677 µs (≈ 553 ms chặn server)

Nên prefix giúp lọc, không giúp tìm nhanh. Nếu thường xuyên phải "lấy mọi key của tenant 42" thì lưu danh sách đó ra một SET riêng (tenant:42:keys), hoặc gom vào một hash.

ACL theo prefix — lý do mạnh nhất để có prefix rõ ràng khi nhiều app/tenant dùng chung:

ACL SETUSER app42 on >pw42 ~tenant:42:* +@read +@write
GET tenant:42:k1   (user app42) → 1
GET tenant:43:k1   (user app42) → NOPERM No permissions to access a key
GET noise:1        (user app42) → NOPERM No permissions to access a key

Đo RAM theo nhóm: khi Redis đầy, câu hỏi đầu tiên là "nhóm key nào chiếm RAM?". Chỉ trả lời được nếu đoạn đầu tên key nói lên chủ sở hữu. Cách làm: SCAN có COUNT nhỏ trên replica, lấy mẫu MEMORY USAGE, cộng dồn theo 2 đoạn đầu của tên. redis-cli --bigkeys / --memkeys cho key lớn nhất theo type (chưa chạy thử trong bài này).

2. Bộ quy tắc đặt tên — mỗi quy tắc kèm bẫy

2.1. Một format duy nhất, chữ thường, : chỉ làm dấu phân cách

<domain>:<entity>[:v<N>]:<id>[:<phần con>]
order:9812            order:9812:items        order:9812:lock
category:children:v2:15                       rl:login:ip:1.2.3.4
forum:liked:{7}:3     (user 7, course 3 — hash tag theo user)

Tên nhiều từ trong một đoạn dùng - hoặc _ (best-sellers), chọn một và giữ nguyên cả codebase.

Bẫy:

2.2. Không đoạn nào được chứa dấu phân cách — dùng id, hoặc băm

Ghép chuỗi có dấu : vào tên thì hai bộ giá trị khác nhau cho ra cùng một key:

# route cache: (from="a:b", to="c")  và  (from="a", to="b:c")
SET rt:a:b:c "owner=(a:b , c)"   → OK
SET rt:a:b:c "owner=(a , b:c)"   → OK
GET rt:a:b:c                     → "owner=(a , b:c)"   ← ghi đè im lặng, người 1 đọc nhầm dữ liệu người 2

Cách đúng, theo thứ tự ưu tiên:

  1. Dùng id số/uuid thay vì slug, email, tên.
  2. Chuỗi tự do (từ khoá tìm kiếm, bộ lọc, URL) → chuẩn hoá rồi băm: search:v1:{sha1(json_encode($normalizedFilters))}. Hex sha1 = 40 ký tự, md5 = 32; cần ngắn hơn thì dùng hash('xxh128', …) (32 hex) — trong PHP 8.1+ có sẵn.

Bẫy:

2.3. Prefix phải kết thúc bằng dấu phân cách — cả khi đặt tên lẫn khi SCAN/xoá

Có: order:1  order:1:items  order:12  order:12:items  order:100  order:1999 ...

SCAN MATCH order:1*    → order:1 order:1:items order:12 order:12:items order:100
                         order:100:items order:1999 order:1999:items     ← xoá nhầm 3 đơn khác
SCAN MATCH order:1:*   → order:1:items                                    ← đúng, nhưng thiếu chính order:1

Cách đúng: xoá theo danh sách tên cụ thể (order:1, order:1:items, order:1:lock) do code sinh ra, không xoá theo pattern. Nếu cần xoá theo pattern, đặt mọi key của thực thể dưới cùng một nhánh: order:1:data, order:1:items → order:1:* bắt đủ.

Bẫy: CACHE_PREFIX/REDIS_PREFIX thiếu dấu cuối (mục 1.5) là đúng lỗi này ở tầng framework.

2.4. Ngắn vừa đủ đọc

Bẫy: viết tắt không có tài liệu → 6 tháng sau không ai biết ps: là product-stock hay payment-session, không ai dám xoá → orphan key sống mãi.

2.5. Version nằm trong tên khi format value thay đổi

Có hai kiểu version, project laravel-livewire dùng cả hai:

KiểuVí dụKhi nào đổiTác dụng
Version schema (sửa tay trong code)category:top-level:v2Đổi cấu trúc JSON/object lưu trong valueCode mới không đọc phải value format cũ
Version dữ liệu (lấy từ DB)price:v1:{id}:{pricing_version}Mỗi lần giá sản phẩm đổi, pricing_version tăngKhông cần xoá cache: key cũ tự hết hạn, key mới được nạp
sequenceDiagram
  participant Old as Pod bản cũ
  participant New as Pod bản mới
  participant R as Redis
  Note over Old,New: Đang rolling deploy, 2 bản chạy song song
  Old->>R: GET category:top-level (format có slug)
  New->>R: GET category:top-level:v2 (format có id)
  R-->>New: miss, query DB, SET ...:v2
  Note over R: Hai key sống song song, không ai đọc nhầm
  Note over R: Key cũ hết TTL thì tự biến mất

Bẫy:

2.6. Nhìn tên là biết loại dữ liệu: cache hay state

LoạiMất thì saoTTLVí dụ tên
CacheNạp lại từ DBBắt buộchome:popular:8
Lock / idempotency / rate limitCó thể chạy trùngBắt buộc, ngắnorder:9812:lock, rl:login:ip:…
State (queue, stream, counter chưa flush)Mất dữ liệuKhôngqueues:{default}, forum:like-events

Bẫy: để cache và state chung một instance với maxmemory-policy allkeys-lru thì eviction xoá cả queue. Tên key tách được loại dữ liệu để nhìn, nhưng eviction không đọc tên — muốn an toàn thật thì tách instance (hoặc ít nhất tách connection/DB khi chưa lên Cluster).

2.7. Số key phải bị chặn: mọi tham số trong tên đến từ tập hữu hạn

CategoryBrowseCatalog trong project làm đúng: tên key có band giá, nhưng band lấy từ config, và scope lọc giá tự do thì không cache. Comment trong code: "so the key space stays bounded to real categories times configured bands".

Bẫy: products:page:{$request->page}, home:popular:{$request->limit}, search:{$q} → bot gửi ?page=1..10000000 là tạo 10 triệu key, đẩy RAM đầy và eviction xoá cache thật. Chặn giá trị (min($limit, 50), whitelist) trước khi đưa vào tên key.

2.8. Tách môi trường bằng instance, không bằng prefix

Nhiều blog khuyên dev:user:1, staging:user:1, prod:user:1 trên cùng Redis. Đây là lời khuyên dở cho production: chung CPU (một luồng chạy lệnh — một KEYS ở dev làm prod đứng), chung maxmemory (dev đầy RAM làm prod bị evict), FLUSHALL xoá cả hai, chung failover. Prefix môi trường chỉ chấp nhận được cho dev/staging dùng chung với nhau.

Prefix app thì có lý khi nhiều service dùng chung một Redis — kèm ACL ~app:* cho từng service.

2.9. Hash tag: chỉ khi cần, và tag phải có nhiều giá trị

Bẫy:

2.10. Dựng tên key ở một chỗ

Đủ dùng: một method private trong service sở hữu key, như project đang làm:

private static function cacheKey(Product $product): string
{
    return "price:v1:{$product->id}:{$product->pricing_version}";
}

Chỗ đọc, chỗ ghi, chỗ xoá (listener khi model đổi) đều gọi method này → không lệch tên vì gõ nhầm. Không cần dựng "KeyRegistry"/"KeyBuilder" dùng chung cho cả app khi mỗi service chỉ có vài key — đó là abstraction không ai cần. Khi key được dùng ở nhiều service khác nhau thì mới đưa method ra một class chung.

2.11. Có bảng key trong tài liệu

Tên keyTypeTTLAi ghi / ai xoáCỡ dự kiếnMất thì sao
price:v1:{id}:{pricing_version}string1hPricedVariants / hết TTL~1 KB × số SP xemNạp lại
queues:{default}list—Laravel queuetheo backlogMất job

(Hai dòng trên là ví dụ minh hoạ cách ghi bảng.) Bảng này là thứ người trực đêm cần nhất khi Redis đầy RAM.

3. Lợi ích

4. Tác hại / hạn chế

5. Trade-off — chọn cái nào

Lựa chọnPhương án APhương án BTiêu chí chọn
Độ dài tênDài, dễ đọcNgắn, viết tắt< vài triệu key → A. Hàng chục triệu key nhỏ → đổi sang hash bucket trước, rút tên sau
Mỗi object một key vs gom hashprogress:{sv}progress:{sv/500} field svCần TTL/eviction riêng từng object → A. RAM là vấn đề và object nhỏ → B (xem bài Redis cần biết)
Invalidate cacheVersion trong tênXoá key khi dữ liệu đổiCó version sẵn trong DB (pricing_version, updated_at) → A, không race. Key ít, sự kiện đổi rõ ràng → B
Tách app / môi trườngPrefix + ACLInstance riêngKhác môi trường (prod vs dev) → luôn B. Nhiều service cùng prod, cùng SLA → A chấp nhận được
Tách cache / queueSố DB (SELECT)Instance riêngChưa và sẽ không lên Cluster, cùng policy → A. Cần eviction policy khác nhau → B. Cluster → chỉ B
Hash tagCó tag theo thực thểKhông tagCó lệnh nhiều key cần atomic → A. Không có → B (phân bố đều hơn)
Tên chứa chuỗi tự doBămChuỗi gốc (đã escape)Mặc định A. B chỉ khi cần đọc tên để debug và tập giá trị nhỏ, đã validate

6. Khó khăn khi vận hành thực tế

7. Case thực tế và cách xử lý

Các case dưới là tình huống minh hoạ dựng từ các bẫy đã test ở trên, không phải sự cố của một hệ thống cụ thể.

Case 1 — Script "xoá cache đơn hàng 1" làm khách của đơn 12, 100, 1999 thấy dữ liệu cũ/trống

Triệu chứng: sau khi support chạy lệnh dọn cache cho đơn 1, vài đơn khác phải nạp lại, một số trang báo lỗi vì key lock của đơn khác cũng bị xoá.

  1. Xem script: SCAN MATCH order:1* + DEL. Pattern thiếu dấu phân cách (mục 2.3).
  2. Chạy lại pattern ở chế độ chỉ in (không DEL) để đếm phạm vi đã xoá nhầm.
  3. Cache thì tự nạp lại. Key lock/idempotency bị xoá → kiểm tra có job nào chạy trùng trong khoảng đó.
  4. Sửa lâu dài: code có method xoá theo danh sách tên cụ thể của một đơn; cấm xoá theo pattern trong runbook.

Case 2 — Rolling deploy xong, 50% request lỗi 500 trong 15 phút rồi tự hết

Triệu chứng: lỗi Undefined array key "id" chỉ ở pod mới; hết đúng bằng TTL cache.

  1. Bản mới đổi cấu trúc value (slug → id) nhưng giữ tên category:top-level.
  2. Pod cũ ghi format cũ, pod mới đọc phải → lỗi. Pod mới ghi format mới, pod cũ đọc phải → lỗi.
  3. Chữa nhanh: bump version tên key (:v2) và deploy lại; không cache:clear giữa lúc 2 bản cùng chạy (pod cũ sẽ ghi lại format cũ ngay).
  4. Phòng: checklist PR "đổi shape value → bump version tên key". Đây đúng là lý do của comment issue #54 trong project.

Case 3 — RAM Redis tăng đều 2%/ngày, không giảm dù traffic ổn định

  1. Lấy mẫu SCAN trên replica, gom theo prefix → thấy nhóm search:* chiếm 60%, và …tag:*:entries nhiều ZSET lớn.
  2. search:{$q}: không chặn cardinality và không có TTL → mỗi từ khoá bot gửi là một key vĩnh viễn. Sửa: băm + TTL + chỉ cache query phổ biến.
  3. ZSET tag của Laravel không có TTL (ttl=-1 trong test) → thêm $schedule->command('cache:prune-stale-tags')->hourly().
  4. Dọn key cũ: SCAN + UNLINK theo lô nhỏ, có sleep giữa các lô, chạy giờ thấp điểm.

Case 4 — Chuyển sang Redis Cluster, queue và Lua báo CROSSSLOT

  1. Queue Laravel dùng Lua đụng queues:default và queues:default:reserved — hai slot khác nhau (11255 vs 5202).
  2. Đổi tên queue thành {default}. Job còn trong queue cũ phải drain trước (worker chạy hết queue cũ trên Redis cũ), rồi mới chuyển.
  3. Lua script tự viết: mọi key phải truyền qua KEYS và có cùng tag. Tìm key tự ghép trong script ('inner:' .. ARGV[1]) — vừa sai Cluster vừa không có prefix.
  4. SELECT không dùng được → bỏ tách bằng REDIS_CACHE_DB, tách bằng instance/prefix.

Case 5 — Dev chạy "dọn key cũ" bằng Redis::keys() + Redis::del(), không xoá được gì

  1. Redis::keys('forum:*') trả laravel-database-forum:… (đã có prefix).
  2. Redis::del($k) thêm prefix lần nữa → xoá key không tồn tại → trả 0, không lỗi. Test thật: 0.
  3. Sửa: cắt prefix Str::after($k, config('database.redis.options.prefix')) trước khi del; và thay keys() bằng SCAN (keys() chặn server, trên Cluster còn là FatalError).

8. Câu hỏi phụ interviewer hay hỏi tiếp

Vì sao dùng dấu :?
Không có lý do kỹ thuật trong Redis — chỉ là quy ước được docs Redis và công cụ GUI dùng. Quan trọng là một dấu duy nhất và không xuất hiện trong dữ liệu.
Key dài tối đa bao nhiêu?
512 MB theo docs, nhưng vô nghĩa. Tên dài tốn RAM mỗi key và tốn CPU băm/so sánh. Giữ dưới ~100 byte; chuỗi dài thì băm.
Dùng số DB (SELECT) hay prefix?
Prefix. DB number không có trong Cluster (ERR SELECT is not allowed in cluster mode), không có ACL riêng theo DB, vẫn chung CPU/RAM. DB number chỉ tiện cho FLUSHDB cache riêng khi còn một instance.
Hash tag đặt ở đâu trong tên?
Vị trí không quan trọng, chỉ cặp ngoặc đầu tiên có nội dung được tính. Quy ước là bọc id thực thể: cart:{u:42}:items. Đừng có hai cặp ngoặc — cặp thứ hai bị bỏ qua, người đọc dễ hiểu nhầm.
Khi nào nên dùng hash thay vì nhiều key?
Nhiều object nhỏ, không cần TTL riêng từng object (hoặc Redis 7.4+ có HEXPIRE), cần tiết kiệm RAM. Không dùng khi object cần evict riêng hoặc hash sẽ thành big key.
Có nên đưa user input vào tên key?
Không trực tiếp. Validate, chuẩn hoá, chặn tập giá trị, rồi băm. Rủi ro: key trùng, pattern glob sai, hash tag do người dùng điều khiển, và bùng nổ số key.
Invalidate cache theo nhóm (vd mọi cache của category 15) thế nào?
Tránh SCAN + DEL theo pattern. Cách tốt: đưa "version nhóm" vào tên (category:15:ver → …:v{ver}:…), tăng số đó là mọi key cũ thành orphan và hết TTL. Hoặc cache tags (có giá: ZSET không TTL, phải prune).
Làm sao biết key nào không còn dùng?
OBJECT IDLETIME (khi policy không phải LFU) lấy mẫu theo nhóm prefix; nhóm nào idle lâu và không có trong bảng key → orphan. Phòng từ gốc: mọi key cache có TTL.
Prefix của Laravel có tác dụng với Lua không?
Có với key truyền qua KEYS, không với key tự ghép trong script, và không với rawCommand. Đã test: laravel-database-outer:7 nhưng inner:7.

Tóm tắt — trả lời trong 60 giây

Redis không có schema, nên tên key là schema. Em dùng một format cố định domain:entity:version:id:phần-con, chữ thường, : chỉ làm dấu phân cách. Mọi đoạn là id hoặc giá trị từ tập hữu hạn; chuỗi tự do thì chuẩn hoá rồi băm, vì ghép thẳng có thể làm hai dữ liệu khác nhau trùng key, và bot có thể tạo hàng triệu key. Key cache luôn có TTL và có version trong tên, để hai bản code chạy song song khi deploy không đọc nhầm format của nhau. Tên dài tốn RAM thật — em đo 52 so với 95 byte/key — nhưng dưới vài triệu key em ưu tiên dễ đọc; nhiều key nhỏ thì gom hash. Trên Cluster, hash tag chỉ dùng khi cần lệnh nhiều key, và tag là id thực thể, không phải hằng số, để tránh hot slot. Cuối cùng là biết tên thật trong Redis: Laravel chồng prefix connection và prefix cache, Lua tự ghép key thì mất prefix — những cái đó phải có trong runbook.

Nguồn tham khảo

Bài liên quan: Redis — những thứ cần biết · Forum like không trùng (bảng key + CROSSSLOT) · Cache nâng cao · Cache khi rollback blue-green.