Systems Hub প্রোডাকশনে Redis
০১ / ২২
Redis in Production
Backend System Design · বাংলা · পর্ব ২

প্রোডাকশনে Redis
সার্ভারকে যেভাবে
হালকা রেখেছি

ক্যাশ, লক, কিউ, স্ট্রিম, Pub/Sub, রেট লিমিট, প্রেজেন্স — একটাই Redis, আটটা আলাদা কাজ। কোড থেকে, সাথে যা যা আরও ভালো করা যেত।

৮ ভূমিকা ২০ ক্যাশড কোয়েরি ০ DB হিট — অথ চেকে
ম্যাপ

Redis এখানে আটটা আলাদা কাজ করছে

01 · Cacheডাটাবেজ বাঁচানোঅথ ইউজার, পারমিশন, কোর্স ক্যাটালগ, নোটিফিকেশন ফিড — ২০টা কল সাইট
02 · Lockএকবারই যেন চলেSET NX + Lua রিলিজ। ক্যাশ ফিল, ক্যাম্পেইন, ডেস্কটপ লগইন
03 · Job QueueBullMQনোটিফিকেশন, ট্রানজেকশন সিঙ্ক — রিট্রাই আর backoff সহ
04 · Streamচ্যাট পারসিস্টXADD → consumer group → XACK, সাথে dead-letter
05 · Pub/SubSocket.IO adapter৬টা PM2 ইনস্ট্যান্স একটাই রুম শেয়ার করে
06 · Rate Limitশেয়ার্ড কাউন্টারrate-limit-redis — ১২টা লিমিটার, IP আর আইডেন্টিটি দুই স্তরে
07 · Presenceকে অনলাইনZSET + HyperLogLog — ছোট রুমে exact, বড় রুমে approximate
08 · Temp Stateঅস্থায়ী স্টেটডেস্কটপ লগইন হ্যান্ডশেক — TTL শেষে নিজেই মুছে যায়

এই আটটার মধ্যে শুধু প্রথমটাই "ক্যাশ"। বাকি সাতটা Redis-কে ব্যবহার করছে এমন কাজে যেগুলো PostgreSQL দিয়ে করলে হয় ধীর হতো, নয়তো একেবারেই সম্ভব হতো না।

ভিত্তি

একটা নয়, চারটা আলাদা কানেকশন

redisConnectionBullMQ, স্ট্রিম, প্রেজেন্স, রেট লিমিট — ব্লকিং কমান্ড চলে
redisPubClientSocket.IO adapter — শুধু publish
redisSubClientSocket.IO adapter — subscribe মোডে আটকে থাকে
redisCacheConnectionশুধু ক্যাশ — commandTimeout ১৫০০ms, maxRetries ১
  • Sub ক্লায়েন্ট আলাদা কেন — subscribe মোডে থাকা কানেকশনে Redis অন্য কোনো কমান্ডই নেয় না, তাই ওটা শুধু ওই কাজেই
  • ক্যাশ ক্লায়েন্ট আলাদা কেন — এটাই সবচেয়ে গুরুত্বপূর্ণ সিদ্ধান্ত। ক্যাশ রিড ১.৫ সেকেন্ডে উত্তর না দিলে হাল ছেড়ে দেয়, রিট্রাই মাত্র একবার
  • ফল — Redis ধীর হলে রিকোয়েস্ট ঝুলে থাকে না, সরাসরি DB থেকে পড়ে চলে যায়
  • lazyConnect — অ্যাপ বুটের সময় connectRedis() সবগুলো একসাথে ধরায়, তারপর হেলথ ইভেন্ট লগ করে
ক্যাশ ইঞ্জিন ১/৩

ভ্যালু না, একটা খাম জমা রাখি

{ "version": 1, "freshUntil": 1756800000000, "value": { … আসল ডেটা … } } // Redis-এ লেখা হয় SET key <envelope> PX <freshTtl + staleTtl>
±১০% jitter version মিসম্যাচ = auto purge
versionক্যাশের ফরম্যাট বদলালে পুরনো এন্ট্রি নিজে থেকেই বাতিল — ম্যানুয়ালি ফ্লাশ করতে হয় না
freshUntilRedis-এর TTL আর "ফ্রেশনেস" আলাদা দুইটা জিনিস। TTL শেষ হওয়ার আগেই ডেটা বাসি হতে পারে
jitter ±১০%একসাথে লেখা হাজারটা key যেন একই সেকেন্ডে এক্সপায়ার না করে — নাহলে প্রতি TTL-এ একটা করে ঢেউ
JSON.parse failভাঙা এন্ট্রি পেলে DEL করে miss রিটার্ন — এরর না
ক্যাশ ইঞ্জিন ২/৩

তিনটা অবস্থা, তিন রকম আচরণ

FRESHসাথে সাথে ফেরত। ডাটাবেজে যাওয়ার প্রশ্নই নেই — এটাই ৯৫% ক্ষেত্রে ঘটে
STALEপুরনো ডেটাই ফেরত দিই সাথে সাথে, আর পেছনে চুপচাপ রিফ্রেশ করি। ইউজার অপেক্ষা করে না
MISSএকজন লক নিয়ে DB-তে যায়, বাকিরা অপেক্ষা করে ফলাফলের — পরের স্লাইডে বিস্তারিত
  • stale মানে কী — freshUntil পার হয়েছে, কিন্তু Redis key এখনো বেঁচে আছে
  • কেন এই আলাদা করাটা দরকার — TTL শেষে ডেটা উধাও হয়ে গেলে কাউকে না কাউকে DB-র জন্য অপেক্ষা করতেই হয়। stale থাকলে কাউকেই করতে হয় না
  • একটা সৎ কথা — আমার ২০টা কল সাইটের ২০টাই getOrLoadStrictCache ব্যবহার করে, যেখানে staleTtlMs = 0। মানে এই STALE দরজাটা আসলে কখনো খোলেই না — শেষ স্লাইডে এটা নিয়ে ফিরব
ক্যাশ ইঞ্জিন ৩/৩

Cache Stampede — চার স্তরের প্রতিরোধ

  • ০১singleFlight — একই প্রসেসের ভেতর একই key চাইলে সবাই একটাই Promise শেয়ার করে
  • ০২Redis lock — প্রসেসের বাইরে, ছয়টা ইনস্ট্যান্সের মধ্যে একজনই DB-তে যায়
  • ০৩Poll & wait — বাকিরা ৪০–১০০ms র‍্যান্ডম গ্যাপে ক্যাশ চেক করে। র‍্যান্ডম, যাতে সবাই একসাথে না চেক করে
  • ০৪Rescue lock — অপেক্ষার সময় শেষেও ক্যাশ খালি? তখন আরেকবার লক নেওয়ার চেষ্টা, তারপরও না পারলে এরর
// লোডার ধীর হলে লক যেন এক্সপায়ার না করে const stop = startLockHeartbeat(lock, lockTtlMs); // প্রতি lockTtl/3 এ PEXPIRE বাড়ায়
  • heartbeat কেন জরুরি — লক ৫ সেকেন্ডের, কিন্তু কোয়েরি ৭ সেকেন্ড নিলে লক ছুটে যেত আর দুইজন একসাথে DB-তে ঢুকে পড়ত
  • ডাবল-চেক — লক পাওয়ার পরেও আরেকবার ক্যাশ পড়ি। ততক্ষণে অন্য কেউ ভরে ফেলে থাকতে পারে
  • Redis একেবারেই না থাকলে — readCache throw করে, তখন singleFlight("redis-down:…") দিয়ে সরাসরি DB, কিন্তু তখনও একটাই কোয়েরি
লক

লক ছাড়াটাই আসল কঠিন অংশ

Worker A
token = a1b2
Redis
lock = a1b2
Worker B
token = c3d4 ✗
-- শুধু নিজের লকই ছাড়তে পারবে if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end
  • সাধারণ DEL কেন বিপজ্জনক — A-র লক এক্সপায়ার হলো, B নতুন লক নিলো, এখন A কাজ শেষ করে DEL করলে সে B-র লক মুছে দিলো
  • তাই র‍্যান্ডম টোকেন — crypto.randomUUID(), আর ছাড়ার আগে মিলিয়ে দেখা
  • Lua কেন — GET আর DEL আলাদা করলে মাঝখানে রেস থেকে যেত। Lua স্ক্রিপ্ট Redis-এ একটাই অবিভাজ্য অপারেশন
  • একই প্যাটার্ন তিন জায়গায় — ক্যাশ ফিল, নোটিফিকেশন ক্যাম্পেইন, ডেস্কটপ লগইন এক্সচেঞ্জ
ইনভ্যালিডেশন · কৌশল ১

key-টা জানা থাকলে — সরাসরি DELETE

cache:auth:user:v1:student:a91f…
cache:authz:v1:course-student:a91f…:c47b…
cache:authz:v1:course-context:e22d…
cache:socket:restriction:v1:a91f…:CHAT
  • কখন এটা ঠিক — যখন key-এর নামটা ঠিক জানা আছে। প্রোফাইল আপডেট হলো? ওই একটা key মুছে দাও
  • এনরোলমেন্ট বাতিল হলে — invalidateCourseStudentAccess ওই স্টুডেন্ট-কোর্স জোড়ার key মুছে দেয়, পরের রিকোয়েস্টেই সে আর ঢুকতে পারবে না
  • বাল্ক ভার্সনও আছে — Promise.allSettled দিয়ে অনেকগুলো একসাথে, একটা ফেল করলেও বাকিগুলো হয়
  • সীমাবদ্ধতা — একটা কোর্স এডিট হলে কোন কোন লিস্ট বদলাবে সেটা তো জানা নেই। ওখানেই এই কৌশল ভেঙে পড়ে
ইনভ্যালিডেশন · কৌশল ২

key অসংখ্য হলে — মুছি না, ভার্সন বাড়াই

cache:course-catalog:v1:list:frb:web:7c2f:v41
cache:course-catalog:v1:list:frb:app:9ab1:v41
cache:course-catalog:v1:list:acad:web:3e88:v41
cache:course-catalog:v1:list:frb:web:7c2f:v42
await redis.incr("cache:course-catalog:v1:version"); // ব্যস। একটা কমান্ড, লাখ লাখ key অচল।
  • কেন DELETE চলবে না — কোয়েরি প্যারামিটার, হোস্ট, পেজ — সব মিলিয়ে হাজার হাজার key। খুঁজতে SCAN চালালে Redis ব্যস্ত হয়ে যাবে
  • ট্রিক — ভার্সন নম্বরটা key-এর নামের ভেতরেই। INCR করলেই নতুন নাম, পুরনোগুলো কেউ আর চায় না
  • পুরনো key-এর কী হয় — কেউ পড়ে না, TTL শেষে Redis নিজেই মুছে ফেলে। আমাদের কিছু করতে হয় না
  • চার জায়গায় ব্যবহার — কোর্স ক্যাটালগ, my-courses, লাইভ ক্লাস লিস্ট, নোটিফিকেশন ফিড
ইনভ্যালিডেশন · কৌশল ৩

দুইটা ভার্সন একসাথে — যেটা নিয়ে আমি সবচেয়ে খুশি

cache:student-my-courses:v1:list:a91f…:enrolled:7c2f:sv3:cv41
const [studentVersion, courseVersion] = await redis.mget( `…my-courses:v1:version:${studentId}`, // sv "cache:course-catalog:v1:version" // cv );
sv — শুধু এই স্টুডেন্টের cv — সবার জন্য এক
  • সমস্যাটা — "আমার কোর্স" লিস্ট দুইভাবে বদলাতে পারে। এক, স্টুডেন্ট নতুন কোর্স কিনলো। দুই, অ্যাডমিন কোর্সের নাম বা ছবি বদলালো
  • একটা ভার্সন হলে — কোর্স এডিট করলে ৫ লাখ স্টুডেন্টের key একসাথে অচল, সবাই একসাথে DB-তে। আর স্টুডেন্ট-ভিত্তিক করলে কোর্স এডিট কারো কাছেই পৌঁছাত না
  • দুইটা রাখায় — sv বাড়ে শুধু ওই একজনের জন্য, cv বাড়ে সবার জন্য। যেটা যতটুকু বদলেছে, ঠিক ততটুকুই অচল হয়
  • একটাই MGET — দুইটা ভার্সন এক রাউন্ড-ট্রিপে
সবচেয়ে বড় লাভ

প্রতিটা রিকোয়েস্টের অথ চেক

ক্যাশের আগে — প্রতি রিকোয়েস্টে৪টি DB কোয়েরি
ক্যাশের পরে — প্রতি রিকোয়েস্টে০টি DB কোয়েরি

টিল রঙ = Redis থেকে এলো। এটা একটা এন্ডপয়েন্টের হিসাব না — এটা সব প্রোটেক্টেড এন্ডপয়েন্টের হিসাব।

  • ১. ইউজার কে — getCachedAuthUser, TTL ৬০s। সাথে অ্যাক্টিভ ব্যান/রেস্ট্রিকশনও
  • ২. এই আইডিটা কোন কোর্সের — getCachedCourseContext, TTL ২ মিনিট। ক্লাস, চ্যাপ্টার, ভিডিও — যেকোনো আইডি থেকে কোর্স
  • ৩. তার কি এই কোর্সে অ্যাক্সেস আছে — hasCourseStudentAccess, TTL ২ মিনিট
  • ব্যান চেকের ট্রিক — মেয়াদোত্তীর্ণ রেস্ট্রিকশন ক্যাশ থেকে পড়ার পর ফিল্টার করা হয়, তাই ব্যান শেষ হওয়ার জন্য TTL-এর অপেক্ষা করতে হয় না
TTL

কোন ডেটা কতক্ষণ — আর কেন

notification feed২০sইউজার বারবার রিফ্রেশ করে, বাসি দেখালে চোখে পড়ে
course catalog · my-courses৩০sভার্সন বাম্প আছে, তাই TTL শুধু শেষ ভরসা
auth user · session৬০sব্যান বা ডিঅ্যাক্টিভেট ১ মিনিটের মধ্যে কার্যকর হতেই হবে
authz · membership২ মিনিটএনরোলমেন্ট বদলালে সরাসরি DELETE হয়, তাই লম্বা TTL নিরাপদ
live class list২ মিনিটরুটিন ঘন ঘন বদলায় না
push token list১০ মিনিটটোকেন প্রায় বদলায়ই না
socket user (chat)১২০sনাম আর অ্যাভাটার — একটু বাসি হলেও ক্ষতি নেই
  • নিয়মটা সহজ — TTL ঠিক করে ডেটার গুরুত্ব, আকার নয়। ভুল ডেটা দেখালে কতটা ক্ষতি, সেটাই প্রশ্ন
  • অথ কেন মাত্র ৬০s — কাউকে ব্যান করার পর সে যদি ৫ মিনিট ঢুকতে পারে, সেটা নিরাপত্তার সমস্যা। তাই TTL ছোট
  • পারমিশন কেন ২ মিনিট — কারণ এখানে সরাসরি DELETE-ও আছে। TTL শুধু সেফটি নেট, প্রধান ব্যবস্থা নয়
  • যেখানে ইনভ্যালিডেশন নেই, সেখানে TTL ছোট — এটাই মূল ট্রেড-অফ
রিয়েলটাইম ১/৫

Pub/Sub — ছয়টা প্রসেস, একটাই রুম

RedisPub/Subsharded adapter
৬টি
PM2
instance
  • সমস্যাটা — PM2 cluster মানে ছয়টা আলাদা Node প্রসেস। স্টুডেন্ট A ইনস্ট্যান্স ১-এ, স্টুডেন্ট B ইনস্ট্যান্স ৪-এ। A চ্যাট করলে B পাবে না
  • সমাধান — @socket.io/redis-adapter। যেকোনো ইনস্ট্যান্স emit করলে Redis-এ publish হয়, বাকি পাঁচটা subscribe করে নিজেদের ক্লায়েন্টদের পাঠায়
  • sharded adapter কেন — সাধারণ adapter একটাই চ্যানেল ব্যবহার করে, তাই প্রতিটা ইনস্ট্যান্স সব মেসেজ পায় — দরকার না হলেও। sharded ভার্সন Redis-এর shard channel ব্যবহার করে, অপ্রয়োজনীয় ট্রাফিক অনেক কমে
  • ট্রান্সপোর্ট শুধু websocket — polling বন্ধ, তাই HTTP round-trip-এর বাড়তি চাপ নেই
রিয়েলটাইম ২/৫

কতজন দেখছে — ZSET আর HyperLogLog

ZSET — প্রতিটা ইউজার আলাদা করেপ্রতি ইউজারে ~৭০ বাইট
HyperLogLog — শুধু সংখ্যাটাসবসময় ১২ KB

৫০০ জনের নিচে — ZSET, একদম নিখুঁত সংখ্যা আর প্রত্যেকের অ্যাভাটার।
৫০০-র উপরে — HyperLogLog, ~০.৮% ভুল হতে পারে, কিন্তু ৫ হাজার হোক বা ৫ লাখ, মেমোরি একই ১২ KB।

  • কেন হাইব্রিড — ছোট ক্লাসে টিচার প্রত্যেককে দেখতে চান। বড় লাইভে "১,২৪,৩৮২ জন দেখছে" — ২০০ জন এদিক-ওদিক হলে কেউ টেরও পাবে না
  • স্লাইডিং উইন্ডো — ৩০ সেকেন্ডের ৩টা bucket। PFCOUNT তিনটা একসাথে মার্জ করে, তাই "গত ৯০ সেকেন্ডে সক্রিয়" — এক ঝলকে চলে যাওয়া লোক গোনা হয় না
  • ZSET-এর score = timestamp — ZREMRANGEBYSCORE দিয়ে পুরনোদের ছাঁটাই এক কমান্ডেই
  • নিরাপত্তা — approx যদি exact-এর চেয়ে কম আসে, exact-টাই ফেরত দেওয়া হয়। কাউন্ট কখনো পিছিয়ে যায় না
রিয়েলটাইম ৩/৫

চারটা কমান্ড, একটাই রাউন্ড-ট্রিপ

আলাদা আলাদা পাঠালে
PFADDZCARDZADDEXPIRE ৪ × RTT
Lua স্ক্রিপ্টে
watchMarkActive ১ × RTT · আর পুরোটা atomic
-- ZSET-এ শুধু তখনই লিখি যখন রুম ছোট local size = redis.call('ZCARD', KEYS[1]) if size < limit then redis.call('ZADD', KEYS[1], unpack(zargs)) redis.call('EXPIRE', KEYS[1], zttl) end
  • কেন এত গুরুত্বপূর্ণ — Redis একটাই থ্রেডে চলে। কমান্ড দ্রুত, কিন্তু নেটওয়ার্ক রাউন্ড-ট্রিপ দ্রুত না। ৪টার বদলে ১টা মানে ৪ গুণ কম অপেক্ষা
  • সিদ্ধান্তটাও ভেতরে — "রুম কি ৫০০-র কম?" — এই চেকটা Redis-এর ভেতরেই হয়। Node-এ আনলে মাঝখানে সংখ্যা বদলে যেতে পারত
  • defineCommand — ioredis স্ক্রিপ্টটা একবার লোড করে EVALSHA দিয়ে চালায়, তাই প্রতিবার পুরো স্ক্রিপ্ট পাঠাতে হয় না
  • তিনটা স্ক্রিপ্ট — markActive, onlineCount, shouldEmit
রিয়েলটাইম ৪/৫

Redis-কেও বাঁচাতে হয় — বাফারিং

১pipeline
প্রেজেন্স ১ সেকেন্ড চ্যাট ১২০ms প্রোগ্রেস → DB ৩০ সেকেন্ড
  • সমস্যাটা — ৫০ হাজার স্টুডেন্ট ভিডিও দেখছে, প্রত্যেকে সেকেন্ডে একটা করে প্রোগ্রেস ইভেন্ট পাঠাচ্ছে। প্রতিটা সরাসরি Redis-এ লিখলে Redis-ই বটলনেক
  • প্রেজেন্স — ১ সেকেন্ডের বাফার, তারপর একটা pipeline-এ সব। ৩০০ জন করে চাঙ্কে ভাগ, যাতে একটা কমান্ড অতিরিক্ত বড় না হয়
  • প্রোগ্রেস — মেমোরিতে Map-এ জমে, একই ইউজারের নতুন ইভেন্ট পুরনোটা ওভাররাইট করে। ৩০ সেকেন্ড পর ১০০০ করে DB-তে
  • ফেল করলে — requeue() রো-গুলো বাফারে ফিরিয়ে দেয়, পরের ফ্লাশে আবার চেষ্টা
  • টাইমার সব unref() — প্রসেস বন্ধ হওয়ার সময় এগুলো আটকে রাখে না
রিয়েলটাইম ৫/৫

চ্যাট — Redis Streams, একটাও মেসেজ হারায় না

XADDমেসেজ স্ট্রিমে, MAXLEN ~ 100000 — পুরনো নিজে থেকেই ছাঁটাই
XREADGROUPওয়ার্কার ২০০টা করে পড়ে, BLOCK 5000 — খালি থাকলে অপেক্ষা, পোলিং নয়
saveChats()একসাথে সব DB-তে
XACKসফল হলেই কেবল স্ট্রিম থেকে "পড়া হয়েছে" চিহ্ন
ব্যাচ ফেলতখন একটা একটা করে আবার চেষ্টা — একটা খারাপ মেসেজের জন্য ২০০টা যেন না হারায়
XADD :deadতাতেও ফেল? dead-letter স্ট্রিমে কারণসহ, তারপর ACK
XAUTOCLAIMওয়ার্কার ক্র্যাশ করলে ৩০ সেকেন্ড পর অন্য ওয়ার্কার তার কাজ দখল করে নেয়
  • Pub/Sub না কেন — Pub/Sub-এ কেউ না শুনলে মেসেজ হারিয়ে যায়। Stream ধরে রাখে যতক্ষণ না ACK হয়
  • consumer group — একাধিক ওয়ার্কার চালালে প্রত্যেকে আলাদা মেসেজ পায়, একই মেসেজ দুইবার সেভ হয় না
  • MAXLEN ~ কেন — ~ মানে "প্রায়"। Redis-কে নিখুঁত ছাঁটাই করতে বললে সেটা ধীর, আনুমানিক বললে দ্রুত
  • হেলথ লগ — প্রতি মিনিটে XLEN আর XPENDING। pending বাড়তে থাকলে বোঝা যায় ওয়ার্কার পিছিয়ে পড়ছে
সুরক্ষা

রেট লিমিট — কাউন্টারটা Redis-এ কেন

export const logInRouteLimiter = rateLimit({ store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }), windowMs: 5 * 60 * 1000, max: 5000, keyGenerator: (req) => normalizeIdentifier(req?.body?.emailOrPhone) || req?.ip });
১২টি আলাদা লিমিটার ২ স্তর — IP + আইডেন্টিটি
  • মেমোরিতে রাখলে যা হতো — ছয়টা ইনস্ট্যান্স, ছয়টা আলাদা কাউন্টার। লিমিট ৫০০ দিলে অ্যাটাকার আসলে ৩০০০ রিকোয়েস্ট পেত
  • Redis-এ রাখায় — কাউন্টার একটাই, লোড ব্যালেন্সার যেখানেই পাঠাক
  • দুই স্তরের কারণ — শুধু IP দিলে একই অফিস বা মোবাইল অপারেটরের সবাই একসাথে ব্লক হয়ে যেত। তাই ফোন/ইমেইল ধরেও আলাদা লিমিট
  • OTP রিসেন্ডে চালাকি — কুকির রিফ্রেশ টোকেন ডিকোড করে ইউজার আইডি দিয়ে লিমিট, তাই নম্বর বদলে বদলে চেষ্টা করেও লাভ নেই
ব্যর্থতা

Redis মরে গেলে সাইট মরে যায় না

Redis সুস্থ — ক্যাশ থেকে উত্তর, ডাটাবেজ প্রায় বসে থাকে
Redis ধীর — ১৫০০ms-এ টাইমআউট, catch ধরে সরাসরি DB। ইউজার শুধু একটু দেরি টের পায়
Redis একেবারে ডাউন — প্রতিটা ক্যাশ ফাংশন loader() কল করে। সাইট ধীর হয়, বন্ধ হয় না
  • নিয়মটা — ক্যাশ একটা অপ্টিমাইজেশন, নির্ভরতা নয়। ক্যাশ ফেল করলে ফিচার ফেল করা উচিত না
  • কোডে যেভাবে — প্রতিটা getCached… ফাংশনে try/catch, catch-এ return loader()
  • সৎ কথা — এটা ক্যাশের জন্য সত্যি। কিন্তু কিউ, স্ট্রিম আর Socket.IO adapter Redis ছাড়া চলবে না — ওখানে Redis একটা আসল নির্ভরতা
  • তাই — Redis-এর আপটাইম আর মেমোরি মনিটর করাটা ঐচ্ছিক না
উন্নতি ১/২

যেগুলো আমি এখনই ঠিক করব

১
ক্যাশ আর কিউ একই Redis-এ — এটাই সবচেয়ে বড় ঝুঁকি ক্যাশ key মুছে গেলে কিছু হয় না, কিন্তু BullMQ-র জব বা চ্যাট স্ট্রিম মুছে গেলে ডেটা হারায়। মেমোরি ভরলে allkeys-lru দুইটার পার্থক্য বোঝে না। সমাধান: দুইটা আলাদা Redis — ক্যাশেরটায় allkeys-lru, কিউ/স্ট্রিমেরটায় noeviction + AOF।
২
maxmemory সেট করা লিমিট না দিলে Redis RAM ভরে যাওয়া পর্যন্ত বাড়তেই থাকে, তারপর OS একদিন প্রসেসটাকে মেরে ফেলে। RAM-এর ~৭৫% এ maxmemory বেঁধে দিলে Redis নিজেই পুরনো key ছাঁটাই শুরু করে — নিয়ন্ত্রিতভাবে।
৩
hit / miss মেট্রিক — এখন একটাও নেই Prometheus আছে, কিন্তু সেটা শুধু API ওয়ার্কার স্ক্রেপ করে। কোন ক্যাশটা আসলে কাজ করছে সেটা এখন অনুমান। redis_exporter + প্রতিটা getOrLoadCache-এ একটা কাউন্টার বসালে হিট রেট দেখা যাবে — ৪০% হিট রেটের ক্যাশ রাখার চেয়ে ফেলে দেওয়াই ভালো।
৪
BullMQ-র জন্য আলাদা কানেকশন এখন redisConnection একাই BullMQ, রেট লিমিট, প্রেজেন্স, স্ট্রিম আর ডেস্কটপ অথ চালাচ্ছে। BullMQ নিজে ব্লকিং কমান্ড ব্যবহার করে, তাই একটা ধীর অপারেশন বাকি সবাইকে আটকে দিতে পারে। ক্যাশের জন্য যেমন আলাদা ক্লায়েন্ট করেছি, ঠিক তেমনই।
উন্নতি ২/২

যেগুলো পরে করলেও চলবে

৫
stale-while-revalidate আসলে ব্যবহারই হচ্ছে না cache.aside-এ সুন্দর ব্যাকগ্রাউন্ড-রিফ্রেশ কোড লেখা আছে, কিন্তু ২৩টার মধ্যে ২০টা কল সাইট getOrLoadStrictCache ব্যবহার করে, যেখানে staleTtlMs = 0 — মানে key ফ্রেশনেস শেষ হওয়ার ঠিক সেই মুহূর্তেই মুছেও যায়। ফলে প্রতি TTL-এ একজনকে DB-র জন্য অপেক্ষা করতেই হয়। কোর্স ক্যাটালগ বা লাইভ ক্লাস লিস্টে staleTtl ৬০s দিলে ওই অপেক্ষাটাও থাকত না।
৬
বড় রুমে HGETALL getWatchUsers ৫০০ ফিল্ড পর্যন্ত একবারে টানে। Redis একটাই থ্রেডে চলে, তাই বড় HGETALL চলাকালীন বাকি সব কমান্ড অপেক্ষা করে। HSCAN দিয়ে ভাগে ভাগে, অথবা অ্যাভাটার লিস্টটা কম ঘন ঘন পাঠানো।
৭
key প্রিফিক্স একটা কনভেনশনে আনা এখন cache:, presence:, hll:, meta:, rl:, lock:, dedup:, metrics: — সবই আলাদা। সব ক্যাশ key যদি cache: দিয়েই শুরু হতো, তাহলে শুধু ক্যাশটুকু ফ্লাশ করা বা আলাদা করে মাপা সহজ হতো।
৮
অথ ইউজারের জন্য প্রসেস-লোকাল একটা ছোট LRU getCachedAuthUser প্রতিটা রিকোয়েস্টে চলে — Redis দ্রুত, কিন্তু ০ নয়। ২–৩ সেকেন্ডের একটা ইন-মেমোরি LRU সামনে বসালে সবচেয়ে ব্যস্ত ইউজারদের জন্য Redis রাউন্ড-ট্রিপটাও বেঁচে যেত। ঝুঁকি — ব্যান কার্যকর হতে ওই কয়েক সেকেন্ড বেশি লাগবে।
৯
রেপ্লিকা আর স্লো-লগ এখন একটাই Redis ইনস্ট্যান্স — এটা single point of failure। একটা রেপ্লিকা + Sentinel দিলে ফেইলওভার হয়। আর SLOWLOG চালু রাখলে কোন কমান্ড ধীর সেটা ধরা পড়ে সমস্যা হওয়ার আগেই।
সারসংক্ষেপ

Redis-এর কাজ ডাটাবেজকে
বিশ্রাম দেওয়া

৮আলাদা ভূমিকা
২৩ক্যাশড কোয়েরি
৪স্তরের stampede রক্ষা
৯উন্নতির জায়গা
← → স্লাইড A অটোপ্লে N স্ক্রিপ্ট F ফুলস্ক্রিন