DEV Community

Monirul Islam
Monirul Islam

Posted on Originally published at mislam-dev.vercel.app

Day 16 — Cache Invalidation — Computer Science-এর সবচেয়ে কঠিন সমস্যা

আগের আর্টিকেলে আমরা Caching এবং Redis নিয়ে অনেক চমৎকার জিনিস জেনেছি। কিন্তু আসল জটিলতা শুরু হয় যখন ক্যাশে রাখা ডাটা পরিবর্তন করার প্রয়োজন পড়ে!

কম্পিউটার সায়েন্সের কিংবদন্তি ইঞ্জিনিয়ার Phil Karlton একবার একটি বিখ্যাত উক্তি করেছিলেন:

"There are two hard things in Computer Science: cache invalidation, and naming things."

মজার বিষয় হলো—ক্যারিয়ারের শুরুতে এই কথাটা শুনলে অনেকেই হয়তো হেসে উড়িয়ে দেবেন। ভাববেন, "ধুর! ক্যাশ ডিলিট করা আর এমন কী কঠিন কাজ?"

কিন্তু যখন আপনি প্রোডাকশনে একটি জটিল হাই-ট্রাফিক সিস্টেমে কাজ করবেন এবং ইউজারদের স্টেল (Stale/পুরানো) ডাটা দেখতে পাবেন বা সার্ভার ডাউন হয়ে যাবে—তখন বুঝবেন Cache Invalidation কেন এত মাথাব্যথার কারণ!


আসল সমস্যাটা কোথায়?

আপনি সিস্টেমে Caching করেছেন, অথচ কখনো Cache Invalidation নিয়ে কোনো প্যারায় পড়েননি—এর মানে হলো আপনি এখনো রিয়েল-ওয়ার্ল্ড জটিল হাই-কনকারেন্সি সিস্টেমের মুখোমুখি হননি।

Caching-এর মূল নীতি হলো: ডাটাবেসের ওপর চাপ কমিয়ে দ্রুত রেসপন্স পাঠানো।

কিন্তু ধরুন, আপনার সিস্টেমে এমন এক প্রোডাক্ট টেবিল আছে যেখানে প্রতি ৫ সেকেন্ড পর পর প্রাইস বা স্টক পরিবর্তন হচ্ছে। এখন ইউজারকে যদি ১ মিনিট আগের পুরানো ক্যাশ করা দাম দেখানো হয়, তবে ইউজার ৫০০০ টাকার প্রোডাক্ট ৩০০০ টাকায় কিনে ফেলবে! কোম্পানির বিশাল লস!

আবার যদি প্রতিটা আপডেটের সাথে সাথে ক্যাশ ভেঙে দেওয়া হয়, তবে ক্যাশ রাখার উপকারিতাটাই বা কী থাকলো?

এই যে ডাটাবেস ও ক্যাশের মধ্যে ডাটার Freshness (তাজাত্ব)Consistency (সামঞ্জস্য) বজায় রাখার ব্যালেন্সিং অ্যাক্ট—এটাই হলো Cache Invalidation-এর মূল চ্যালেঞ্জ।


Cache Invalidation-এর প্রধান ৪টি স্ট্র্যাটেজি 🛠️

সিস্টেমের প্রয়োজন অনুযায়ী কোন স্ট্র্যাটেজি বেছে নেবেন তা নিচে বিস্তারিত আলোচনা করা হলো:

1. TTL-Based Invalidation (টাইম-টু-লিভ) ⏱️

সবচেয়ে সহজ এবং নিরাপদ উপায়। এখানে ক্যাশ সেট করার সময়ই একটি এক্সপায়ারি টাইম বলে দেওয়া হয়। টাইম পার হলেই Redis স্বয়ংক্রিয়ভাবে ক্যাশটি ডিলিট করে দেয়।

// ৫ মিনিট (৩০০ সেকেন্ড) পর ক্যাশ স্বয়ংক্রিয়ভাবে মুছে যাবে
await redis.setex("product:1:price", 300, "500");
Enter fullscreen mode Exit fullscreen mode

✅ কখন ব্যবহার করবেন?

  • যেসব ডাটা কিছুটা পুরানো দেখালেও সমস্যা নেই (যেমন: ব্লগের লাইক কাউন্ট, আবহাওয়া বা খবরের ভিউ)।

❌ অসুবিধা:

  • ডাটা আপডেট হওয়ার পরও এক্সপায়ারি টাইম শেষ না হওয়া পর্যন্ত ইউজার পুরানো ডাটাই দেখতে থাকবে।

2. Write-Through Invalidation ✍️

যখনই অ্যাপ্লিকেশনে ডাটাবেসের কোনো তথ্য আপডেট বা নতুন ডাটা ইনসার্ট করা হয়, সাথে সাথেই ক্যাশ মেমোরির ডাটাকেও নতুন ভ্যালু দিয়ে ওভাররাইট করে দেওয়া হয়।

async function updateProductPrice(productId, newPrice) {
  // ১. মেইন ডাটাবেস আপডেট
  await db.product.update({ price: newPrice }, { where: { id: productId } });

  // ২. ক্যাশ মেমোরিও সাথে সাথে নতুন ভ্যালু দিয়ে আপডেট
  await redis.set(`product:${productId}:price`, newPrice.toString());
}
Enter fullscreen mode Exit fullscreen mode

✅ কখন ব্যবহার করবেন?

  • যেখানে রিড ট্রাফিক অনেক বেশি এবং আপডেটের সাথে সাথেই নতুন ডাটা দেখানো বাধ্যতামূলক (যেমন: রিয়েল-টাইম প্রোডাক্ট প্রাইসিং)।

3. Cache-Aside + Delete Pattern (Invalidate on Write) 🗑️

ডাটাবেসে কোনো পরিবর্তন হলে ক্যাশে নতুন ভ্যালু রাইট না করে, সোজা সেই ক্যাশ কী-টি ডিলিট (redis.del) করে দেওয়া হয়! পরবর্তীতে কোনো ইউজার যখন রিকোয়েস্ট করবে, তখন ক্যাশ মিস (Cache Miss) হবে এবং ফ্রেশ ডাটা ডাটাবেস থেকে এসে পুনরায় ক্যাশ হবে।

async function updateProduct(productId, updatedData) {
  // ১. মেইন ডাটাবেস আপডেট
  await db.product.update(updatedData, { where: { id: productId } });

  // ২. পুরানো ক্যাশ কী-টি সোজা ডিলিট (পরের রিকোয়েস্টে ফ্রেশ ডাটা লোড হবে)
  await redis.del(`product:${productId}`);
}
Enter fullscreen mode Exit fullscreen mode

💡 কেন Update না করে Delete করা শ্রেয়?

যদি ক্যাশ ঘনঘন আপডেট করা হয় কিন্তু পরবর্তী ১০ মিনিটে কেউ তা রিড না করে, তবে ক্যাশ রাইট করাটা মেমোরির অপচয়। তাই Delete Pattern অনেক বেশি প্র্যাকটিক্যাল!


4. Event-Driven Invalidation 📡

মাইক্রোসার্ভিস বা ডিস্ট্রিবিউটেড আর্কিটেকচারে যখন এক সার্ভিস ডাটা আপডেট করে, তখন সে একটি Event Fire করে (product.updated)। ক্যাশ সার্ভিস বা অন্যান্য সার্ভিস সেই ইভেন্ট শুনে যার যার Redis ক্যাশ ইনভ্যালিডেট করে দেয়।

// Order/Inventory Service
eventBus.emit("product.updated", { productId: 1 });

// Dedicated Cache Invalidation Listener
eventBus.on("product.updated", async ({ productId }) => {
  await redis.del(`product:${productId}`);
});
Enter fullscreen mode Exit fullscreen mode

💥 Cache Stampede (Thundering Herd Problem)

Cache Invalidation-এর অন্যতম মারাত্মক বিপদের নাম হলো Cache Stampede

মনে করুন, আপনার প্ল্যাটফর্মের সবচেয়ে পপুলার কোনো প্রোডাক্টের ক্যাশ এক্সপায়ার হয়ে গেল ঠিক সেই মুহূর্তে, যখন ১০,০০০ জন ইউজার একসাথে পেজটি রিফ্রেশ করছে!

যেহেতু ক্যাশটি খালি (Expired), ১০,০০০টি রিকোয়েস্টই একসাথে ডাটাবেসে হিট করবে! ডাটাবেস এক সেকেন্ডের মধ্যে ক্র্যাশ করে পুরো সিস্টেম ডাউন হয়ে যাবে!

🛡️ কীভাবে Cache Stampede ঠেকাবেন?

  1. Mutex Lock (Locking Mechanism): ক্যাশ মিস হলে প্রথম রিকোয়েস্টটি ডাটাবেসে যাওয়ার আগে একটি Distributed Lock নেবে। বাকি ৯,৯৯৯টি রিকোয়েস্ট লক রিলিজ হওয়া পর্যন্ত ওয়েট করবে এবং প্রথম রিকোয়েস্টের ক্যাশ হওয়া ডাটাই ব্যবহার করবে।
  2. Probabilistic Early Expiration (XFetch Algorithm): এক্সপায়ারি টাইম শেষ হওয়ার কিছু আগেই ব্যাকগ্রাউন্ডে অলক্ষ্যে ক্যাশ রিফ্রেশ করে নেওয়া।

বটম লাইন

Cache Invalidation একটি চতুর ও সংবেদনশীল বিষয়। আপনার অ্যাপ্লিকেশনের Read vs Write Ratio এবং ডাটা কনসিস্টেন্সির প্রয়োজনীয়তা বুঝে সঠিক স্ট্র্যাটেজি সিলেক্ট করতে হবে। সবসময় যতটা সম্ভব সিম্পল স্ট্র্যাটেজি (যেমন: TTL বা Cache-Aside Delete) দিয়ে শুরু করুন, জটিল ইভেন্ট-ড্রাইভেন ইনভ্যালিডেশনে যাওয়ার আগে বাস্তব প্রয়োজন যাচাই করে নিন।

আপনার প্রোডাকশনে কি কখনো Cache Stampede বা Stale Data-র মুখোমুখি হয়েছেন? কীভাবে ফিক্স করেছিলেন? কমেন্টে জানান! 👇

Top comments (0)