30 վայրկյանում
Redis-ը շատ արագ տվյալների բազա է, որը տվյալները պահում է հիշողության մեջ, ոչ թե դիսկի վրա։ Թիմերն այն օգտագործում են դանդաղ հարցերի պատասխանները հիշելու (caching), session-ներ պահելու, rate limit-ների համար հարցումները հաշվելու և process-ների միջև job-եր ու հաղորդագրություններ փոխանցելու համար։ Բարդ մասը արագությունը չէ, այլ որոշելը, թե որքան կարող է ապրել cache-ավորված տվյալը և ինչպես այն դեն նետել, երբ ճշմարտությունը փոխվում է։
Ինչու եմ օգտագործում
- Ես օգտագործում եմ Redis, որովհետև այն hot path-երից հեռացնում է ամենաթանկ աշխատանքը․ նույն մենյուի, գնացուցակի կամ կարգավորումների հարցումը չպետք է րոպեում հազարավոր անգամներ դիպչի տվյալների բազային։
- Այն օգտագործում եմ որպես background աշխատանքի և real-time ֆունկցիաների հիմք՝ queue-ներ job-երի համար և pub/sub, որպեսզի սերվերի յուրաքանչյուր instance կարողանա նույն event-ը push անել միացված client-ներին։
- Այն օգտագործում եմ counter-ների և կարճաժամկետ վիճակի համար (rate limit-ներ, OTP փորձեր, lock-եր), որովհետև հիշողության մեջ ատոմար գործողություններն ավելի պարզ և անվտանգ են, քան նույնը SQL տողերով անելը։
- Caching-ին վերաբերվում եմ նախ որպես ճշգրտության խնդրի․ արագ սխալ պատասխանն ավելի վատ է, քան դանդաղ ճիշտը, ուստի իմ ավելացրած յուրաքանչյուր cache ունի հստակ տեր, TTL և invalidation-ի կանոն։
Ինչպես է աշխատում
01Ինչ է իրականում Redis-ը
Պատկերացրեք Redis-ը որպես RAM-ում ապրող հսկա ընդհանուր բառարան։ Հիշողությունից կարդալը միկրովայրկյաններ է տևում, իսկ տիպիկ database query-ն՝ միլիվայրկյաններ, ուստի պարզ որոնումների համար Redis-ը հաճախ 100 անգամ ավելի արագ է։ Բայց Redis-ը key → string-ից ավելին է։ Այն առաջարկում է տվյալների կառուցվածքներ՝ string-ներ (և counter-ներ), hash-եր (դաշտերով օբյեկտ), list-եր (queue-ներ), set-եր (եզակի անդամներ), sorted set-եր (leaderboard-ներ, ժամանակով դասավորված տարրեր), stream-ներ (append-only log՝ consumer group-երով), ինչպես նաև bitmap-ներ, HyperLogLog և geo ինդեքսներ։ Յուրաքանչյուր հրաման ատոմար կատարվում է մեկ հիմնական thread-ի վրա, ահա թե ինչու counter-ի վրա INCR-ն անվտանգ է նույնիսկ շատ client-ների դեպքում։ Լիցենզիայի մասին՝ 2026-ի դրությամբ Redis 8-ը հասանելի է AGPLv3-ով (իր source-available լիցենզիաների կողքին), իսկ Valkey-ն BSD լիցենզիայով, Linux Foundation-ի fork է, որը cloud-երի մեծ մասն առաջարկում է որպես drop-in փոխարինող։
- String՝ cache-ավորված JSON, counter-ներ, դրոշակներ
- Hash՝ օգտատիրոջ session կամ կարգավորումների օբյեկտ
- List / Stream՝ job queue-ներ և event log-եր
- Sorted set՝ վարկանիշներ, պլանավորված տարրեր, sliding-window rate limit-ներ
SET product:42 '{"name":"Latte","price":1500}' EX 300
GET product:42
INCR rate:203.0.113.10:2026-11-05T10:01
HSET session:abc user_id 7 role cashier
EXPIRE session:abc 1800
ZADD leaderboard 980 player:1 1200 player:2
ZREVRANGE leaderboard 0 9 WITHSCORES
TTL product:42
02Caching pattern-ներ՝ cache-aside, write-through, write-behind
Cache-aside-ը (lazy loading) default-ն է․ հավելվածը նախ հարցնում է Redis-ին, miss-ի դեպքում կարդում է տվյալների բազան, պահում արդյունքը TTL-ով և վերադարձնում այն։ Cache է արվում միայն այն տվյալը, որն իրականում հարցվում է, և եթե Redis-ը ընկնի, հավելվածը դեռ աշխատում է, պարզապես ավելի դանդաղ։ Write-through-ը նշանակում է, որ յուրաքանչյուր գրառում միաժամանակ գնում է տվյալների բազա և cache, այնպես որ ընթերցումները միշտ «տաք» են․ լավ է փոքր, շատ կարդացվող տվյալների համար, ինչպիսիք են կարգավորումները։ Write-behind-ը նախ գրում է cache-ում և ավելի ուշ flush անում տվյալների բազա․ արագ է, բայց կարող է տվյալներ կորցնել, ուստի ես խուսափում եմ դրանից ցանկացած ֆինանսական բանի համար։ Ինչ pattern էլ լինի, տվյալների բազան մնում է ճշմարտության աղբյուրը։ Cache-ը պատճեն է, և դուք պետք է կարողանաք ջնջել այն ամբողջությամբ՝ առանց որևէ բան կորցնելու։
async function getMenu(branchId: string) {
const key = `menu:v3:${branchId}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const menu = await db.menu.findForBranch(branchId);
// TTL with jitter so many keys do not expire at the same moment
const ttl = 600 + Math.floor(Math.random() * 60);
await redis.set(key, JSON.stringify(menu), "EX", ttl);
return menu;
}
async function updateMenu(branchId: string, data: MenuInput) {
await db.menu.update(branchId, data);
await redis.del(`menu:v3:${branchId}`); // invalidate after the write commits
}
03TTL, invalidation և cache stampede
Cache-ավորված տվյալը երկու ձևով է անհետանում՝ այն սպառվում է (TTL) կամ դուք այն դիտմամբ ջնջում եք (invalidation)։ Օգտագործեք երկուսն էլ՝ invalidate արեք այն գրառումների ժամանակ, որոնք դուք եք վերահսկում, և TTL պահեք որպես անվտանգության ցանց այն գրառումների համար, որոնք մոռացել եք։ Key-երի անուններում դրեք տարբերակ (menu:v3:…), որպեսզի տվյալների ձևի փոփոխությունը երբեք հին blob-եր չկարդա։ Ջնջեք տվյալների բազայի transaction-ի commit-ից հետո, ոչ թե առաջ, այլապես զուգահեռ ընթերցողը կարող է cache-ը նորից լցնել հնացած տվյալներով։ Cache stampede-ը (thundering herd) տեղի է ունենում, երբ hot key-ը սպառվում է, և հարյուրավոր հարցումներ միաժամանակ miss են անում ու բոլորը դիպչում տվյալների բազային։ Լուծումներ՝ TTL-ներին ավելացրեք պատահական jitter, թույլ տվեք միայն մեկ հարցման վերակառուցել արժեքը՝ կարճ lock-ով (SET key NX EX), մատուցեք հնացած արժեքը, մինչ մեկ worker-ը թարմացնում է այն, կամ հայտնի key-երը թարմացրեք background-ում՝ մինչև սպառվելը։
// One rebuilder per key; others wait briefly or serve stale data
const lock = await redis.set(`lock:${key}`, "1", "NX", "EX", 10);
if (lock === "OK") {
try {
const fresh = await loadFromDb();
await redis.set(key, JSON.stringify(fresh), "EX", 600);
} finally {
await redis.del(`lock:${key}`);
}
}
04Session-ներ, rate limiting և lock-եր
Redis-ը բնական տուն է կարճաժամկետ ընդհանուր վիճակի համար։ Session-ներ՝ session-ը պահեք hash-ում TTL-ով, որպեսզի հավելվածի ցանկացած սերվեր կարողանա կարդալ այն, իսկ logout-ը լինի մեկ DEL։ Rate limiting՝ fixed window-ը INCR է rate:{ip}:{minute}-ի նման key-ի վրա EXPIRE-ով․ պարզ է, բայց թույլ է տալիս burst-եր պատուհանների եզրերին։ Sliding window-ն օգտագործում է timestamp-ների sorted set կամ token bucket փոքր Lua script-ում՝ ավելի հարթ սահմանափակումների համար։ Նույն գաղափարն օգտագործեք OTP փորձերը և login-ի ձախողումները սահմանափակելու համար՝ ըստ հեռախոսի կամ հաշվի, ոչ միայն ըստ IP-ի։ Lock-եր՝ SET lock:order:42 token NX EX 30-ը մեկ worker-ին տալիս է բացառիկ աշխատանք 30 վայրկյանով․ ազատեք այն միայն այն դեպքում, եթե token-ը դեռ համընկնում է, այլապես կարող եք ջնջել ուրիշի lock-ը։ Իրական գումարի համար database constraint-ը կամ idempotency key-ը դեռ վերջնական պաշտպանությունն է։
-- Fixed-window limit: 100 requests per 60 s, atomic in one call
-- KEYS[1] = rate key, ARGV[1] = limit, ARGV[2] = window seconds
local n = redis.call("INCR", KEYS[1])
if n == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[2])
end
if n > tonumber(ARGV[1]) then
return 0
end
return 1
05Queue-ներ և pub/sub
List-երն ու stream-երը Redis-ը դարձնում են ընդունակ job broker։ BullMQ (Node) և Laravel queues (PHP) գրադարանները job-երը պահում են Redis-ում և տալիս են retry-ներ backoff-ով, ուշացումներ, առաջնահերթություններ և dead-letter մշակում։ Վեբ հարցումը մնում է արագ, որովհետև email ուղարկելը կամ ERP-ի հետ sync անելը տեղի է ունենում worker-ում։ Pub/sub-ն այլ է․ հաղորդագրությունը broadcast է արվում բոլոր նրանց, ովքեր հենց այս պահին subscribe են արած, և չի պահվում։ Այն կատարյալ է real-time event-ների fan-out-ի համար մի քանի WebSocket սերվերների միջև (socket.io-ի Redis adapter-ը հենց դա է անում), բայց եթե ոչ ոք չի լսում, հաղորդագրությունը կորչում է։ Երբ ձեզ պետք են առաքման երաշխիքներ կամ replay, օգտագործեք Streams՝ consumer group-երով, կամ իսկական queue։ Պարզ կանոն՝ pub/sub՝ «հիմա ասա բոլորին», queue-ներ՝ «համոզվիր, որ սա կկատարվի»։
06Persistence, հիշողություն և eviction
Redis-ը տվյալները պահում է RAM-ում, բայց կարող է դրանք պահպանել դիսկի վրա։ RDB-ն պարբերական snapshot-ներ է գրում՝ կոմպակտ և արագ վերականգնվող, բայց կորցնում եք վերջին snapshot-ից հետո կատարված գրառումները։ AOF-ը (append-only file) log է անում յուրաքանչյուր գրառում․ appendfsync everysec-ով կորցնում եք առավելագույնը մոտ մեկ վայրկյան։ Շատ setup-ներ օգտագործում են երկուսն էլ։ Մաքուր cache-ի համար persistence-ը կարող է անջատված լինել։ Queue-ների և session-ների համար միացրեք AOF-ը։ Միշտ սահմանեք maxmemory և eviction policy՝ allkeys-lru կամ allkeys-lfu cache-ի համար, noeviction՝ queue-ի համար (ուզում եք սխալներ, ոչ թե լուռ ջնջված job-եր)։ Իդեալում cache-ը և queue-ն պահեք առանձին instance-ներում կամ տվյալների բազաներում՝ տարբեր policy-ներով։ Երբեք 6379 port-ը մի բացեք ինտերնետին․ bind արեք localhost-ին կամ private ցանցին, պահանջեք գաղտնաբառ կամ ACL օգտատերեր և ցանցերի միջև օգտագործեք TLS։
# /etc/redis/redis.conf (cache + small queue on a private host)
bind 127.0.0.1
protected-mode yes
requirepass change_me_long_random
maxmemory 1gb
maxmemory-policy allkeys-lfu
appendonly yes
appendfsync everysec
save 900 1
07Երբ չպետք է cache անել
Caching-ը ճշմարտության երկրորդ պատճեն է ավելացնում, և յուրաքանչյուր պատճեն կարող է սխալ լինել։ Մի cache արեք նախքան չափելը․ հաճախ ինդեքսը կամ ուղղված N+1 query-ն լուծում է խնդիրը՝ առանց նոր շարժվող մասերի։ Խուսափեք այն տվյալների cache-ից, որոնք պետք է ճշգրիտ լինեն կարդալու պահին՝ մնացորդը checkout-ում, wallet-ի մնացորդները, վճարման կարգավիճակը, կամ cache արեք դրանք միայն ցուցադրման համար և transaction-ի ներսում նորից ստուգեք տվյալների բազայից։ Մի cache արեք օգտատիրոջ անձնական պատասխանները ընդհանուր key-ի տակ (մի հաճախորդի տվյալների դասական արտահոսք մյուսին)։ Զգույշ եղեք այն բաների cache-ի հետ, որոնք փոխվում են գրեթե յուրաքանչյուր հարցման ժամանակ․ hit rate-ը մոտ կլինի զրոյի, իսկ գրառումների համար դուք վճարում եք։ Վերջապես, եթե տվյալների ծավալը փոքր է, և տվյալների բազան պարապ է, in-process memory cache-ը կամ HTTP caching header-ները կարող են լինել այն ամենը, ինչ ձեզ պետք է։
Օգտագործեք, երբ
- Շատ կարդացվող տվյալներ, որոնք հազվադեպ են փոխվում՝ մենյուներ, կատալոգներ, կարգավորումներ, թարգմանություններ, փոխարժեքներ։
- Թանկ հաշվարկներ կամ third-party API-ների արդյունքներ, որոնց համար մի քանի րոպե հնացած լինելը նորմալ է։
- Սերվերների միջև ընդհանուր կարճաժամկետ վիճակ՝ session-ներ, OTP կոդեր, rate-limit counter-ներ, lock-եր։
- Job queue-ներ և real-time fan-out հավելվածի կամ WebSocket-ի մի քանի instance-ների միջև։
Մի՛ օգտագործեք, երբ
- Որպես կարևոր տվյալների միակ պատճեն, ինչպիսիք են պատվերները, վճարումները կամ մնացորդները։
- Երբ դանդաղության իրական պատճառը բացակայող ինդեքսն է կամ N+1 query-ն։
- Այն արժեքների համար, որոնք պետք է ճշգրիտ լինեն որոշման պահին, եթե միայն նորից չեք ստուգում տվյալների բազայի դեմ։
Այլընտրանքներ
- Valkey
Redis-ի BSD լիցենզիայով fork․ drop-in համատեղելի է և առաջարկվում է cloud պրովայդերների մեծ մասի կողմից։
- Memcached
Պարզ, multi-threaded key-value cache՝ առանց տվյալների կառուցվածքների կամ persistence-ի։
- In-process cache (LRU map)
Հնարավորինս արագ է, բայց յուրաքանչյուր սերվեր ունի իր սեփական պատճենը, և invalidation-ն ավելի բարդ է։
- HTTP / CDN caching
Ամբողջ պատասխանները cache արեք edge-ում Cache-Control-ով․ հիանալի է հանրային, ոչ անձնական էջերի համար։
Լավագույն պրակտիկաներ
- Տվյալների բազային վերաբերվեք որպես ճշմարտության աղբյուրի․ դուք պետք է կարողանաք ցանկացած պահի անվտանգ flush անել cache-ը։
- Key-երն անվանեք prefix-ով և տարբերակով (app:menu:v3:{id}) և միշտ սահմանեք TTL։
- Invalidate արեք գրառման commit-ից հետո և TTL-ին jitter ավելացրեք՝ համաժամանակյա սպառումից խուսափելու համար։
- Hot key-երը պաշտպանեք stampede-ներից rebuild lock-ով կամ stale-while-revalidate-ով։
- Սահմանեք maxmemory և ճիշտ eviction policy․ queue-ների համար օգտագործեք noeviction։
- Redis-ը պահեք private (localhost կամ VPC)՝ գաղտնաբառով/ACL-ով, երբեք հանրային port-ի վրա։
- Մոնիտորինգ արեք hit rate-ը, հիշողությունը, eviction-ները և դանդաղ հրամանները․ production-ում խուսափեք KEYS *-ից (օգտագործեք SCAN)։
Տարածված սխալներ
- Օգտատիրոջ անձնական տվյալները cache անել ընդհանուր key-ի տակ և մի օգտատիրոջ ցույց տալ մեկ ուրիշի տվյալները։
- Cache-ը ջնջել մինչև տվյալների բազայի transaction-ի commit-ը, այնպես որ հնացած տվյալները հետ են գրվում։
- Pub/sub օգտագործել այն job-երի համար, որոնք չպետք է կորչեն․ հաղորդագրություններն անհետանում են, երբ ոչ ոք subscribe արած չէ։
- Աշխատել առանց maxmemory-ի, այնպես որ Redis-ը աճում է, մինչև սերվերի RAM-ը վերջանում է։
- Պահել հսկա արժեքներ (մեգաբայթանոց JSON blob-եր), որոնք յուրաքանչյուր ընթերցման ժամանակ արգելափակում են միակ thread-ը։
Խորը վերլուծություն
08Մեկ thread․ ինչու է մեկ դանդաղ հրամանը վնասում բոլորին
Redis-ը հրամանները կատարում է հերթով՝ իր main thread-ում։ Redis 6-ից սկսած՝ ըստ ցանկության միացվող I/O thread-երը (io-threads) զուգահեռ կարդում և գրում են network socket-ները, բայց հրամաններն իրենք դեռ կատարվում են հաջորդաբար։ Այդ դիզայնի շնորհիվ է յուրաքանչյուր հրաման ատոմար, և հենց դրա պատճառով էլ մեկ O(N) հրամանը կանգնեցնում է մնացած բոլոր client-ներին։ KEYS *-ը տասը միլիոն key-ի վրա, SMEMBERS-ը հսկայական set-ի վրա, LRANGE 0 -1-ը երկար list-ի վրա կամ DEL-ը մեծ hash-ի վրա կարող են արգելափակել հարյուրավոր միլիվայրկյաններ, իսկ հավելվածի համար դա նման է Redis-ի outage-ի։ Անցեք key-երի վրայով SCAN-ով, HSCAN-ով և SSCAN-ով՝ փոքր batch-երով։ Մեծ key-երը ջնջեք UNLINK-ով, որը հիշողությունն ազատում է background thread-ում։ Գտեք խնդրահարույց հրամանները նախքան դրանք կվնասեն․ SLOWLOG-ը գրանցում է շեմից դանդաղ հրամանները, LATENCY DOCTOR-ը բացատրում է spike-երը, իսկ redis-cli --bigkeys-ը և --memkeys-ը նմուշառում են keyspace-ը՝ առանց այն արգելափակելու։
# Find big keys and memory hogs (samples with SCAN, safe in production)
redis-cli --bigkeys
redis-cli --memkeys
# Commands slower than slowlog-log-slower-than (microseconds)
redis-cli SLOWLOG GET 10
redis-cli LATENCY DOCTOR
# Memory health and cache efficiency
redis-cli INFO memory | grep -E 'used_memory_human|mem_fragmentation_ratio'
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses|evicted_keys'
# Remove one big key without blocking the main thread
redis-cli UNLINK report:2026:full
09Replication, Sentinel և Cluster
Replica-ն primary-ն պատճենում է ասինխրոն՝ primary-ն նախ պատասխանում է client-ին, իսկ գրառումն ուղարկում է հետո։ Եթե primary-ն մահանում է գրառումը հաստատելուց անմիջապես հետո, failover-ը կարող է առաջ քաշել մի replica, որը երբեք չի տեսել այդ գրառումը։ WAIT հրամանն արգելափակում է, մինչև N replica-ներ հաստատեն, ինչը նեղացնում է այդ պատուհանը, բայց Redis-ը չի դարձնում strongly consistent տվյալների բազա։ Sentinel-ը հետևում է primary-ին և նրա replica-ներին, քվեարկում է խափանման մասին և առաջ է քաշում replica-ն․ client-ները Sentinel-ին են հարցնում, թե որտեղ է ընթացիկ primary-ն։ Cluster-ը լուծում է այլ խնդիր՝ մեկ մեքենայից մեծ տվյալներ՝ keyspace-ը բաժանելով 16,384 hash slot-ի, որոնք բաշխված են մի քանի primary-ների վրա։ Multi-key հրամանները, transaction-ները և Lua script-երն աշխատում են միայն այն դեպքում, երբ բոլոր key-երը hash են լինում նույն slot-ում, հակառակ դեպքում ստանում եք CROSSSLOT սխալ։ Hash tag-երը լուծում են դա․ hash է արվում միայն {ձևավոր փակագծերի} ներսի մասը, ուստի կապված key-երը հայտնվում են միասին։
# Only "user:42" inside the braces is hashed -> same slot
SET {user:42}:profile '{"name":"Ana"}'
SET {user:42}:cart '{"items":3}'
MGET {user:42}:profile {user:42}:cart
CLUSTER KEYSLOT {user:42}:cart
# Without a shared hash tag, MGET across slots returns CROSSSLOT
10Race condition, որն ունի ամեն cache-aside համակարգ
Cache-aside-ն ունի դասական race։ A ընթերցողը miss է ստանում cache-ում և կարդում է հին տողը։ B գրողը թարմացնում է տողը և ջնջում key-ը։ A ընթերցողը, մի փոքր ուշացած, հիմա հին արժեքը հետ է գրում cache, որտեղ այն մնում է մինչև TTL-ի ավարտը։ Commit-ից հետո ջնջելը պատուհանը փոքրացնում է, բայց զրո չի դարձնում։ Մի քանի պաշտպանություն՝ էժանից ուժեղ․ կարճ պահեք TTL-ները այն տվյալների համար, որտեղ հնացածությունը կարևոր է․ մի պահ անց նորից ջնջեք (delayed double delete) queue-ի job-ից․ cache-ված արժեքի հետ պահեք version կամ updated_at և փոքր Lua script-ով հրաժարվեք ավելի նորը վերագրելուց․ կամ invalidation-ը ղեկավարեք հենց տվյալների բազայից՝ transactional outbox-ով կամ change data capture-ով, որպեսզի ոչ մի code path չկարողանա մոռանալ այն։ Շատ hot ընթերցումների համար Redis-ի client-side caching-ը (CLIENT TRACKING, Redis 6-ից) թույլ է տալիս սերվերին invalidation հաղորդագրություններ ուղարկել հավելվածի մակարդակի near cache-երին։
11Հիշողության ներքին կառուցվածքը․ eviction, encoding-ներ և դաշտային TTL
Eviction-ը մոտավոր է։ Կատարյալ LRU հերթականությանը հետևելու փոխարեն Redis-ը նմուշառում է մի քանի key (maxmemory-samples, default-ով 5) և հեռացնում լավագույն թեկնածուին․ նմուշի չափը մեծացնելը մոտեցնում է իրական LRU-ին՝ որոշակի CPU-ի գնով։ LFU-ն յուրաքանչյուր key-ի համար պահում է փոքր լոգարիթմական հասանելիության հաշվիչ, որը ժամանակի ընթացքում նվազում է, ուստի երեկ հայտնի key-ը ի վերջո դառնում է eviction-ի ենթակա։ Փոքր hash-երը, list-երը, set-երը և sorted set-երը պահվում են կոմպակտ listpack encoding-ներով, և դրա համար էլ շատ փոքր hash-եր հաճախ ավելի քիչ հիշողություն են զբաղեցնում, քան շատ առանձին string key-եր։ Մեկ key-ը ստուգեք MEMORY USAGE-ով և հետևեք mem_fragmentation_ratio-ին․ active defragmentation-ը (activedefrag yes) կարող է online վերադարձնել ֆրագմենտացված հիշողությունը։ Redis 7.4-ից hash-ի դաշտերը կարող են առանձին expire լինել HEXPIRE-ով, ինչը հարմար է session-ների համար, որտեղ OTP դաշտը պետք է անհետանա 60 վայրկյան հետո, իսկ session-ն ինքը շարունակի ապրել։ Key-ի մակարդակի TTL-ը միևնույն է ջնջում է ամբողջ hash-ը՝ անկախ դաշտերի TTL-ներից։
Իրական իրավիճակներ
Deploy-ը բոլոր cache key-երը դարձրեց սառը
Խնդիր
Release-ը key-ի prefix-ը menu:v3-ից փոխեց menu:v4-ի։ Մեկ րոպեի ընթացքում ամեն request miss էր ստանում cache-ում, տվյալների բազայի CPU-ն հասավ 100%-ի, իսկ checkout-ի latency-ն 80 ms-ից դարձավ մի քանի վայրկյան։
Ինչպես լուծել
Կարճաժամկետ՝ rollback արեք հին prefix-ին կամ ընթերցման traffic-ը տեղափոխեք replica, որպեսզի տվյալների բազան վերականգնվի։ Ապա շտկեք գործընթացը։ Key-ի version-ի փոփոխությունները պետք է տաքացվեն նախքան traffic-ի փոխարկումը․ job-ը անցնում է ակտիվ մասնաճյուղերով և լցնում menu:v4-ը, մինչդեռ հին կոդը դեռ կարդում է v3-ը։ Ավելացրեք request coalescing (մեկ rebuild յուրաքանչյուր key-ի համար՝ SET lock NX EX-ի հետևում), որպեսզի զուգահեռ miss-երը երբեք չբազմապատկվեն տվյալների բազայի բեռի։ Ավելացրեք TTL jitter, որպեսզի նոր key-երը հետագայում միաժամանակ չexpire լինեն։ Վերջում cache hit rate-ը և տվյալների բազայի connection-ները դրեք deploy-ի dashboard-ի վրա՝ alert-ով, որպեսզի սառը cache-ը երևա վայրկյանների ընթացքում, այլ ոչ թե հաճախորդների բողոքներից։
Background job-երն անհետացան հիշողության ճնշման տակ
Խնդիր
Մեկ Redis instance պահում էր և՛ cache-ը, և՛ job queue-ն՝ maxmemory-policy allkeys-lru-ով։ Traffic spike-ի ժամանակ Redis-ը evict արեց queue-ի key-երը, և որոշ հաստատման email-ներ ու webhook-ներ այդպես էլ չուղարկվեցին։
Ինչպես լուծել
Բաժանեք workload-ները։ Queue-ն տեղափոխեք իր սեփական instance (կամ առանձին managed database)՝ maxmemory-policy noeviction-ով և միացված AOF-ով, որպեսզի լցված queue-ն առաջացնի սխալներ, որոնց վրա կարող եք alert դնել, այլ ոչ թե լուռ կորցնի job-երը։ Cache-ը թողեք allkeys-lfu-ի վրա։ Երկուսի չափն էլ որոշեք իրական metric-ներից՝ used_memory, evicted_keys և queue depth։ Քանի որ email-ներ և webhook-ներ են կորել, replay արեք դրանք source of truth-ից․ query արեք ազդված ժամանակահատվածի պատվերները, որոնց notification-ի կարգավիճակը դեռ pending է, և նորից enqueue արեք idempotency key-երով, որպեսզի հաղորդագրություն ստացած հաճախորդները այն երկրորդ անգամ չստանան։
Հաճախորդները թարմացումից հետո տեսնում էին հին գներ
Խնդիր
Admin-ները փոխեցին գները, API-ն commit-ից հետո ջնջեց cache key-ը, բայց մի քանի մասնաճյուղ տասը րոպե ցույց էին տալիս հին գինը։ Դա տեղի էր ունենում միայն բեռի տակ և չէր վերարտադրվում լոկալ։
Ինչպես լուծել
Սա cache-aside-ի race-ն է․ դանդաղ ընթերցողը բեռնել էր հին տողը թարմացումից առաջ և հետ գրել այն ջնջումից հետո։ Հաստատեք դա՝ log անելով cache-ի գրառումները տողի updated_at-ի հետ։ Ապա գրառումները դարձրեք պայմանական․ պահեք արժեքը իր version-ի հետ միասին և սահմանեք այն կարճ Lua script-ով, որը հրաժարվում է ավելի նոր version-ը վերագրելուց։ Որպես էժան safety net՝ ավելացրեք ուշացված երկրորդ ջնջում queue-ի job-ից՝ թարմացումից մի քանի վայրկյան անց, և կրճատեք գնային տվյալների TTL-ը։ Checkout-ի ժամանակ, միևնույն է, գները կարդացեք տվյալների բազայից transaction-ի ներսում, որպեսզի հնացած cache-ը կարողանա ազդել միայն ցուցադրման վրա, բայց երբեք՝ գանձման։
Արագ ուղեցույց
- Անցնել key-երով՝ առանց արգելափակելու
SCAN 0 MATCH menu:v3:* COUNT 200- Ջնջել մեծ key-ը background-ում
UNLINK key- Lock՝ owner token-ով և timeout-ով
SET lock:order:42 <token> NX EX 30- Expire անել hash-ի մեկ դաշտը (Redis 7.4+)
HEXPIRE session:abc 60 FIELDS 1 otp- Ստուգել մնացած կյանքի տևողությունը
TTL key (-1 = no TTL, -2 = missing)- Մեկ key-ի զբաղեցրած հիշողությունը
MEMORY USAGE key- Անվտանգ գտնել մեծ key-երը
redis-cli --bigkeys- Տեսնել դանդաղ հրամանները
SLOWLOG GET 10- Cache hit rate
INFO stats -> keyspace_hits / (keyspace_hits + keyspace_misses)- Cluster-ի որ slot-ն է օգտագործում key-ը
CLUSTER KEYSLOT {user:42}:cart
Իմ իրական նախագծերում
- Multi-tenant ռեստորանային POS SaaS-ում Redis pub/sub-ը real-time event-ները fan-out է անում socket.io room-ների մեջ՝ ըստ ընկերության, մասնաճյուղի, տերմինալի և օգտատիրոջ։
- Նույն պլատֆորմն աշխատեցնում է մոտ 20 BullMQ queue Redis-ի վրա, որոնք սնվում են transactional outbox-ից, ներառյալ HMAC-ով ստորագրված webhook-ներ, որոնք առաքվում են queue-ի միջոցով։
- Այդ պրոդուկտի dedicated սերվերում Redis-ն աշխատում է Docker Compose-ում՝ PostgreSQL-ի, BullMQ worker-ների և Prometheus, Grafana և Loki observability stack-ի կողքին։
Հարցեր և պատասխաններ
Q1Ո՞րն է տարբերությունը cache-aside-ի և write-through-ի միջև։
Cache-aside-ի դեպքում տրամաբանությունը պատկանում է հավելվածին՝ կարդալ cache-ից, իսկ miss-ի դեպքում բեռնել տվյալների բազայից, պահել արժեքը TTL-ով և վերադարձնել այն։ Գրառումները գնում են տվյալների բազա, ապա invalidate անում key-ը։ Այն cache է անում միայն օգտագործվողը և դիմանում է cache-ի outage-ին։ Write-through-ի դեպքում յուրաքանչյուր գրառում միասին թարմացնում է տվյալների բազան և cache-ը, այնպես որ ընթերցումները միշտ տաք են՝ այն տվյալները գրելու գնով, որոնք գուցե ոչ ոք չկարդա։ Default-ով ես ընտրում եմ cache-aside՝ բացահայտ invalidation-ով և TTL-ով, իսկ write-through-ը օգտագործում եմ փոքր, շատ hot կոնֆիգուրացիոն տվյալների համար։
Q2Ինչպե՞ս եք կանխում cache stampede-ը։
Stampede-ը տեղի է ունենում, երբ հայտնի key-ը սպառվում է, և շատ հարցումներ միաժամանակ վերակառուցում են այն՝ ծանրաբեռնելով տվյալների բազան։ Ես համակցում եմ պաշտպանության մի քանի միջոց։ TTL-ները ստանում են պատահական jitter, որպեսզի key-երը միասին չսպառվեն։ Վերակառուցումը պաշտպանված է կարճ lock-ով SET NX EX-ով, այնպես որ մեկ հարցումը վերահաշվարկում է, իսկ մյուսները կարճ սպասում են կամ ստանում նախորդ արժեքը։ Ամենա hot key-երի համար օգտագործում եմ stale-while-revalidate՝ շարունակում եմ մատուցել հին արժեքը և թարմացնում այն background-ում՝ մինչև սպառվելը։ Տվյալների բազայի կողմից connection pool-ի խելամիտ սահմանափակումները թույլ չեն տալիս, որ մեկ վատ րոպեն վերածվի outage-ի։
Q3Redis-ը հիշողության մեջ է։ Ի՞նչ է լինում, եթե այն restart լինի։
Կախված է persistence-ի կարգավորումներից։ RDB snapshot-ներով կորցնում եք վերջին snapshot-ից հետո կատարված գրառումները, AOF-ով և appendfsync everysec-ով՝ առավելագույնը մոտ մեկ վայրկյան, իսկ persistence-ն անջատված լինելու դեպքում՝ ամեն ինչ։ Մաքուր cache-ի համար դա ընդունելի է, որովհետև տվյալների բազան ճշմարտության աղբյուրն է, և cache-ը նորից տաքանում է, թեև warm-up-ի ընթացքում պետք է սպասել լրացուցիչ բեռի։ Queue-ների և session-ների համար միացնում եմ AOF-ը, իսկ կրիտիկական workload-ների համար օգտագործում եմ replica՝ ավտոմատ failover-ով Sentinel-ի կամ managed ծառայության միջոցով։ Կարևոր բիզնես տվյալները երբեք չեն ապրում միայն Redis-ում։
Q4Ինչպե՞ս կիրականացնեիք rate limiting Redis-ով։
Ամենապարզ տարբերակը fixed window-ն է՝ INCR արեք rate:{client}:{minute}-ի նման key և սահմանեք EXPIRE, երբ counter-ն առաջին անգամ ստեղծվում է, ամբողջը մեկ Lua script-ում, որպեսզի ատոմար լինի։ Եթե քանակը գերազանցում է սահմանը, վերադարձրեք HTTP 429՝ Retry-After header-ով։ Fixed window-ները թույլ են տալիս burst-եր եզրերին, ուստի ավելի խիստ սահմանափակումների համար օգտագործում եմ sliding window՝ timestamp-ների sorted set-ով, կամ token bucket։ Սահմանափակում եմ ճիշտ նույնականացմամբ՝ IP, API key, հաշիվ կամ հեռախոսահամար OTP-ի համար, և զգայուն endpoint-ների, օրինակ՝ login-ի և վճարումների համար կիրառում եմ ավելի խիստ սահմանափակումներ։
Q5Ե՞րբ կընտրեիք Redis pub/sub-ը, և ե՞րբ queue։
Pub/sub-ը հաղորդագրությունը broadcast է անում այդ պահին միացված յուրաքանչյուր subscriber-ի և ոչինչ չի պահում։ Դա հարմար է real-time fan-out-ի համար, օրինակ՝ պատվերի կարգավիճակի event-ն ուղարկել յուրաքանչյուր WebSocket սերվերի, որպեսզի ճիշտ client-ը ստանա այն։ Queue-ն պահում է job-ը, մինչև worker-ը մշակի այն՝ retry-ներով, backoff-ով և ձախողումների հետևմամբ։ Դա հարմար է այն աշխատանքի համար, որը պետք է կատարվի ճիշտ այնպես, ինչպես խնդրված է՝ email-ներ, webhook-ներ, ERP sync, կտրոններ։ Եթե ինձ պետք է replay կամ մի քանի consumer group, որոնք կարդում են նույն log-ը, օգտագործում եմ Redis Streams կամ առանձին broker, օրինակ՝ Kafka։
Q6Ի՞նչ eviction policy եք սահմանում և ինչո՞ւ։
Cache-ի համար սահմանում եմ maxmemory և սովորաբար allkeys-lfu կամ allkeys-lru, որպեսզի հիշողությունը լցվելիս Redis-ը հեռացնի ամենաքիչ օգտակար key-երը՝ գրառումները ձախողելու փոխարեն։ LFU-ն պահում է հաճախ օգտագործվող տարրերը, նույնիսկ եթե դրանց վերջին վայրկյանին չեն դիպել։ Queue-ների, session-ների և lock-երի համար օգտագործում եմ noeviction, որովհետև job-ը կամ session-ը լուռ ջնջելն ավելի վատ է, քան սխալ ստանալը, որի համար կարող եք alert դնել։ Քանի որ մեկ instance-ն ունի մեկ policy, նախընտրում եմ առանձին instance-ներ կամ տրամաբանական բաժանում cache-ի և queue-ի workload-ների համար և մոնիտորինգ եմ անում evicted_keys metric-ը։
Սենիոր մակարդակի հարցեր
S1Արդյո՞ք Redis lock-ը բավականաչափ անվտանգ է վճարումը կամ մնացորդի նվազեցումը պաշտպանելու համար։
Միայնակ՝ ոչ։ SET NX EX lock-ը լավ efficiency lock է․ դեպքերի մեծ մասում այն թույլ չի տալիս երկու worker-ի կատարել նույն թանկ job-ը։ Այն correctness-ի երաշխիք չէ։ Պրոցեսը կարող է կանգ առնել՝ garbage collection, դանդաղ network, VM-ի սառեցում, lock-ի expire-ից ավելի երկար, մինչդեռ մեկ այլ worker վերցնում է lock-ը, և երկուսն էլ գրում են։ Failover-ը նույնպես կարող է կորցնել lock, որը երբեք չի replicate արվել։ Redlock-ը մի քանի instance-ների վրա նվազեցնում է որոշ failure mode-եր, բայց դեռ կախված է ժամանակային ենթադրություններից։ Correctness-ի համար վերջնական պաշտպանությունը պետք է լինի system of record-ում՝ unique constraint, idempotency key, պայմանական update, օրինակ՝ UPDATE stock SET qty = qty - 1 WHERE qty > 0, կամ տվյալների բազայի կողմից ստուգվող fencing token։ Redis lock-երը ես պահում եմ կրկնվող աշխատանքը կանխելու համար, իսկ invariant-ը թողնում եմ տվյալների բազային։
S2Sentinel, Cluster, թե managed ծառայություն․ ինչպե՞ս եք ընտրում։
Սկսեք խնդրից։ Եթե տվյալները հանգիստ տեղավորվում են մեկ մեքենայի հիշողության մեջ, և ձեզ պետք է high availability, replica-ներով primary-ն և Sentinel-ը կամ դրա managed համարժեքը ավելի պարզ են․ ամեն հրաման աշխատում է, Lua script-երը և transaction-ները slot-ի կանոններ չունեն, իսկ client-ները մնում են պարզ։ Ընտրեք Cluster, երբ ձեզ պետք է ավելի շատ հիշողություն կամ write throughput, քան կարող է տալ մեկ node-ը․ գինը hash slot-երի դիզայնն է, CROSSSLOT սահմանափակումները, cluster-aware client-ները և ավելի բարդ գործողություններ, ինչպիսին resharding-ն է։ Managed ծառայությունները վերացնում են patching-ը, backup-ները և failover-ի փորձարկումները՝ ավելի բարձր գնով և version-ների ու module-ների նկատմամբ ավելի քիչ վերահսկողությամբ։ Իմ տեսած product backend-ների մեծ մասի համար ճիշտ չափի մեկ primary-ն՝ persistence-ով և replica-ով, շատ հեռու է տանում․ sharding-ը չափումներով պայմանավորված որոշում է, ոչ թե մոդայով։
S3Ինչպե՞ս եք նախագծում caching-ը multi-tenant SaaS-ի համար։
Երեք մտահոգություն՝ isolation, fairness և invalidation։ Isolation․ tenant ID-ն միշտ key-ի մաս է, որը կառուցվում է մեկ helper function-ով, երբեք պատահական տեղերում string concatenation-ով, իսկ օգտատիրոջ տվյալները ներառում են user ID-ն։ Բացակայող tenant prefix-ը տվյալների արտահոսք է, ուստի ես key builder-ները թեստավորում եմ ուղղակիորեն։ Fairness․ մեկ մեծ tenant կարող է լցնել հիշողությունը և evict անել մնացած բոլորի key-երը, ուստի ես հետևում եմ հիշողությանը ըստ key prefix-ի, սահմանափակում եմ cache-ված արժեքների չափը, իսկ շատ մեծ tenant-ների համար դիտարկում եմ առանձին instance-ներ։ Invalidation․ tenant-ի մակարդակի փոփոխությունները, օրինակ՝ նոր մենյու կամ գնացուցակ, պետք է մեծացնեն Redis-ում պահվող և key-երի մեջ ներառված per-tenant version համարը, որպեսզի մեկ INCR-ը invalidate անի այդ tenant-ի ամեն ինչ՝ առանց հազարավոր key-եր scan կամ ջնջելու․ հին գրառումներն այնուհետև պարզապես հնանում են TTL-ով։
S4Ե՞րբ կփոխարինեիք pub/sub-ը Redis Streams-ով, և ի՞նչ է փոխվում։
Երբ հաղորդագրություն կորցնելն այլևս ընդունելի չէ, կամ consumer-ները պետք է հասնեն բաց թողածին։ Pub/sub-ը fire-and-forget է․ subscriber-ը, որը deploy-ից հետո վերամիանում է, բաց է թողնում այդ ընթացքում հրապարակված ամեն ինչ։ Streams-ը գրառումները պահում է append-only log-ում․ consumer group-երը հետևում են, թե ինչ է կարդացել յուրաքանչյուր consumer, գրառումները մնում են pending, մինչև XACK-ով հաստատվեն, իսկ XAUTOCLAIM-ը թույլ է տալիս մեկ այլ consumer-ի վերցնել այն հաղորդագրությունները, որոնք crash եղած worker-ը չի ավարտել։ Ծախսերն իրական են․ պետք է trim անեք stream-ը (MAXLEN կամ MINID), այլապես հիշողությունը հավերժ կաճի, պետք է նախագծեք idempotent handler-ներ, քանի որ redelivery տեղի է ունենում, և պետք է monitor անեք pending-entry list-երը։ WebSocket սերվերների միջև live UI fan-out-ի համար ես դեռ օգտագործում եմ pub/sub, քանի որ բաց թողնված event-ը շտկվում է հաջորդ fetch-ով։ Այն ամենի համար, ինչը գործարկում է բիզնես գործողություններ, ես ուզում եմ Streams, job queue կամ առանձին broker։
Բառարան
- TTL
- Time to live․ քանի վայրկյան է key-ը գոյություն ունենում, մինչև Redis-ն այն ավտոմատ ջնջի։
- Hit ratio
- Ընթերցումների բաժինը, որոնք պատասխանվում են cache-ից, այլ ոչ տվյալների բազայից․ cache-ի օգտակարության հիմնական չափանիշը։
- Eviction
- Redis-ի կողմից key-երի հեռացումը maxmemory-ից ցածր մնալու համար՝ ըստ կարգավորված policy-ի, օրինակ՝ LRU կամ LFU։
- Cache stampede
- Երբ շատ request-ներ միաժամանակ miss են ստանում նույն expire եղած key-ի վրա և բոլորը վերակառուցում են այն տվյալների բազայից։
- Hot key
- Մեկ key, որն այնքան հաճախ է կարդացվում կամ գրվում, որ բեռը կենտրոնացնում է մեկ thread-ի կամ cluster-ի մեկ node-ի վրա։
- Big key
- Key, որը պահում է շատ մեծ արժեք կամ collection, ինչի պատճառով դրա վրա հրամանները դանդաղ են և արգելափակող։
- Hash slot
- Redis Cluster-ի 16,384 bucket-ներից մեկը․ յուրաքանչյուր key համապատասխանում է մեկ slot-ի, յուրաքանչյուր slot՝ մեկ node-ի։
- Hash tag
- Key-ի անվան {մասը}, որը միայն hash է արվում՝ կապված key-երին ստիպելով հայտնվել cluster-ի նույն slot-ում։
- Sentinel
- Redis-ի պրոցեսների խումբ, որը հետևում է primary-ին, հայտնաբերում խափանումը և ավտոմատ առաջ քաշում replica-ն։
- Fencing token
- Lock-ի հետ տրվող աճող թիվ, որպեսզի storage-ը կարողանա մերժել այն holder-ի գրառումները, որի lock-ը expire է եղել։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։