পাঠ ১৯ · ৫১-এর মধ্যে · মডিউল ৫
Home / Courses / System Design / ক্যাশিং স্ট্র্যাটেজি

ক্যাশিং স্ট্র্যাটেজি ও প্যাটার্ন

Caching strategies & patterns
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ক্যাশিং কেন latency ও ডেটাবেস লোড দুটোই কমায়
  • চারটি ক্যাশিং প্যাটার্ন — Cache-aside, Write-through, Write-back, Read-through — এবং তাদের ট্রেড-অফ
  • কোন প্যাটার্ন কখন বেছে নেওয়া উচিত (consistency বনাম speed বিবেচনায়)
  • Python দিয়ে Cache-aside প্যাটার্ন বাস্তবায়ন ও cache-hit/miss যাচাই

১ · ক্যাশিং কেন দরকার — সংক্ষিপ্ত স্মরণ

L01-এর ফ্লো-ডায়াগ্রামে আমরা দেখেছিলাম App Server-এর পাশে একটি Cache বসানো আছে, Database-এর ঠিক আগে। কারণ — একই ডেটা বারবার ডিস্ক বা ডেটাবেস থেকে পড়া ধীরগতির এবং ডেটাবেসের উপর অপ্রয়োজনীয় লোড তৈরি করে। L03-এর latency সংখ্যা মনে করুন — মেমরি থেকে পড়া (~১০০ন্যানোসেকেন্ড) ডিস্ক থেকে পড়ার (~১০মিলিসেকেন্ড) চেয়ে বহু গুণ দ্রুত। ক্যাশCacheঘন ঘন-ব্যবহৃত ডেটার একটি দ্রুত-অ্যাক্সেসযোগ্য (সাধারণত in-memory) কপি — যাতে প্রতিবার ধীরগতির মূল ডেটাসোর্স (ডেটাবেস/ডিস্ক) থেকে পড়তে না হয়। ব্যবহার করলে বেশিরভাগ রিকোয়েস্ট দ্রুত মেমরি থেকেই সার্ভ হয়ে যায় — শুধু প্রথমবার বা ক্যাশ-মিস হলে ডেটাবেসে যেতে হয়। প্রশ্ন হলো — ক্যাশ ও ডেটাবেস কীভাবে সিঙ্ক থাকবে? এই পাঠের চারটি প্যাটার্ন সেই প্রশ্নের চারটি ভিন্ন উত্তর।

২ · চারটি ক্যাশিং প্যাটার্ন

Cache-aside (Lazy Loading)
অ্যাপ প্রথমে ক্যাশ চেক করে; মিস হলে ডেটাবেস থেকে পড়ে ক্যাশে বসিয়ে দেয়। সবচেয়ে জনপ্রিয় ও সহজ প্যাটার্ন — কিন্তু ডেটাবেসে সরাসরি পরিবর্তন হলে ক্যাশ পুরনো (stale) থেকে যেতে পারে।
Write-through
প্রতিটি রাইট একইসাথে ক্যাশ ও ডেটাবেস দুটোতেই সিঙ্ক্রোনাসভাবে যায়। সবসময় সামঞ্জস্যপূর্ণ (consistent) থাকে — কিন্তু প্রতিটি রাইট দুটি সিস্টেমের জন্য অপেক্ষা করে বলে ধীর।
Write-back (Write-behind)
রাইট প্রথমে শুধু ক্যাশে যায় (দ্রুত রেসপন্স), ডেটাবেস আপডেট হয় পরে, async-ভাবে (ব্যাচে বা কিছুক্ষণ পর)। দ্রুততম রাইট — কিন্তু ডেটাবেসে ফ্লাশ হওয়ার আগে ক্যাশ ক্র্যাশ করলে ডেটা হারানোর ঝুঁকি।
Read-through
অ্যাপ শুধু ক্যাশকেই জিজ্ঞেস করে — মিস হলে ক্যাশ নিজেই ডেটাবেস থেকে পড়ে নিজেকে ভরে ফেলে, অ্যাপের কাছে পুরো প্রক্রিয়াটি স্বচ্ছ (transparent)। Cache-aside-এর মতোই যুক্তি, তবে লোডিং লজিক অ্যাপ নয়, ক্যাশ লেয়ারের দায়িত্ব।
Consistency বনাম Speed — মূল ট্রেড-অফ

Write-through সবচেয়ে নিরাপদ (সবসময় সিঙ্ক) কিন্তু সবচেয়ে ধীর রাইট। Write-back সবচেয়ে দ্রুত রাইট কিন্তু সবচেয়ে ঝুঁকিপূর্ণ। Cache-aside মাঝামাঝি — রিড-ভারী ওয়ার্কলোডে (বেশিরভাগ ওয়েব অ্যাপ) সবচেয়ে ব্যবহারিক পছন্দ, তাই এটিই শিল্পে সবচেয়ে বেশি ব্যবহৃত হয়। সঠিক প্যাটার্ন নির্বাচন নির্ভর করে আপনার নন-ফাংশনাল রিকোয়ারমেন্টের উপর (L01) — ডেটা কতটা স্টেল হওয়া সহনীয়, রাইট লেটেন্সি কতটা গুরুত্বপূর্ণ।

ক্লায়েন্ট Client অ্যাপ সার্ভার App Server ১. ক্যাশ চেক Cache miss ৩. ক্যাশ পূরণ populate() ২. ডেটাবেস Database
Cache miss হলে অ্যাপ ডেটাবেস থেকে পড়ে ফলাফল ক্যাশে বসায় — পরের বার একই কী-এর জন্য সরাসরি ক্যাশ থেকে সার্ভ হবে।

৩ · Python-এ Cache-aside বাস্তবায়ন

নিচের কোডে একটি cache ডিকশনারি ও একটি database ডিকশনারি আছে। get(key) ফাংশন প্রথমে ক্যাশ চেক করে; মিস হলে ডেটাবেস থেকে পড়ে (এবং একটি কাউন্টার বাড়িয়ে DB-hit ট্র্যাক করে), তারপর ফলাফল ক্যাশে বসিয়ে দেয়।

Python
cache = {}
database = {"user:1": "Alice", "user:2": "Bob"}
db_hit_count = 0

def get(key):
    global db_hit_count
    if key in cache:
        print(f"[CACHE HIT] {key} → {cache[key]}")
        return cache[key]

    db_hit_count += 1
    print(f"[CACHE MISS] {key} → ডেটাবেস থেকে পড়া হচ্ছে (DB hit #{db_hit_count})")
    value = database[key]
    cache[key] = value
    return value

print("প্রথম কল — 'user:1':")
get("user:1")

print("\nদ্বিতীয় কল — একই key 'user:1':")
get("user:1")

print("\nতৃতীয় কল — নতুন key 'user:2':")
get("user:2")

print(f"\nমোট DB hits: {db_hit_count} (ক্যাশ-হিটের জন্য DB hit বাড়েনি)")

    
লক্ষ্য করুন — "user:1"-এর প্রথম কল একটি cache miss (DB hit #1), দ্বিতীয় কল সরাসরি cache hit (DB hit counter বাড়েনি)। "user:2" একটি নতুন key হওয়ায় আবার একটি নতুন miss (DB hit #2)। মোট DB hits হয় ২ — ৩টি কল সত্ত্বেও, কারণ একটি key দ্বিতীয়বার ক্যাশ থেকেই সার্ভ হয়েছে।
মূল কথা · Key takeaway

ক্যাশিং প্যাটার্ন বেছে নেওয়া মানে একটি নির্দিষ্ট consistency/speed ট্রেড-অফ বেছে নেওয়া — কোনো একটি প্যাটার্ন সবসময় "সেরা" নয়। পরের দুই পাঠে (L20, L21) আমরা দেখব কীভাবে ক্যাশিং ভৌগোলিকভাবে ছড়িয়ে দেওয়া যায় (CDN) এবং ক্যাশ পূর্ণ হয়ে গেলে বা ডেটা বদলে গেলে কী করা হয় (invalidation ও eviction)।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ Cache-aside-এ স্টেল ডেটার ঝুঁকি থাকা সত্ত্বেও এটি কেন সবচেয়ে জনপ্রিয় প্যাটার্ন?

কারণ বেশিরভাগ ওয়েব অ্যাপ্লিকেশন রিড-ভারী (read-heavy) — একই ডেটা বারবার পড়া হয়, কিন্তু তুলনামূলক কম পরিবর্তন হয়। Cache-aside শুধু "actually requested" ডেটাই ক্যাশ করে (lazy loading), তাই মেমরি অপচয় হয় না এবং বাস্তবায়ন সহজ — শুধু একটি if-check যোগ করলেই হয়। সামান্য স্টেলনেসের ঝুঁকি TTL (L21) বা explicit invalidation দিয়ে নিয়ন্ত্রণযোগ্য, তাই বেশিরভাগ সিস্টেমে এই সরলতা ও দক্ষতার ভারসাম্যই যথেষ্ট।

প্র ০২ Write-back-এর ডেটা হারানোর ঝুঁকি কোন পরিস্থিতিতে গ্রহণযোগ্য হতে পারে?

যখন রাইট থ্রুপুট অত্যন্ত বেশি এবং প্রতিটি ডেটার সাময়িক ক্ষতি ব্যবসায়িকভাবে বিপর্যয়কর নয় — যেমন একটি ভিডিওর "ভিউ কাউন্ট" বা একটি পোস্টের "লাইক কাউন্ট" আপডেট করা। যদি ক্যাশ ক্র্যাশ করার আগে সাম্প্রতিক কয়েকটি ভিউ/লাইক ডেটাবেসে ফ্লাশ না হয়ে থাকে, তাহলে সেগুলো হারিয়ে যেতে পারে — কিন্তু এটি একটি সমালোচনামূলক আর্থিক লেনদেন নয়, তাই স্পিডের বিনিময়ে এই ছোট ঝুঁকি মেনে নেওয়া হয়। বিপরীতে, ব্যাংকিং ব্যালেন্সের মতো ডেটায় write-back কখনোই ব্যবহার করা উচিত নয়।

প্র ০৩ Cache-aside ও Read-through আসলে একই সমস্যার সমাধান করে — তাহলে মূল পার্থক্য কোথায়?

দুটোই cache miss-এ ডেটাবেস থেকে পড়ে ক্যাশ ভরে — কিন্তু কে এই লোডিং লজিক পরিচালনা করে তা ভিন্ন। Cache-aside-এ অ্যাপ্লিকেশন কোড নিজে দায়িত্ব নেয় (যেমন আমাদের কোড সেলের get() ফাংশন)। Read-through-এ এই দায়িত্ব ক্যাশ লেয়ার/লাইব্রেরি নিজেই নেয় — অ্যাপ শুধু ক্যাশকে জিজ্ঞেস করে, ক্যাশ নিজে থেকেই প্রয়োজনে ডেটাবেস থেকে পড়ে। ফলাফল একই, কিন্তু Read-through অ্যাপ্লিকেশন কোড থেকে ক্যাশিং লজিক লুকিয়ে রাখে (cleaner separation of concerns)।

অনুশীলন

  1. কোড বদলান: উপরের cache-aside কোডে একটি write_through(key, value) ফাংশন যোগ করুন যা একইসাথে cache[key] ও database[key] দুটোই আপডেট করে সিঙ্ক্রোনাসভাবে।

    উদাহরণ বাস্তবায়ন:

    def write_through(key, value):
        database[key] = value   # প্রথমে (বা একইসাথে) ডেটাবেস আপডেট
        cache[key] = value      # তারপর ক্যাশ আপডেট — এখন দুটোই সবসময় সিঙ্ক
        print(f"[WRITE-THROUGH] {key} = {value} → cache ও database উভয়ই আপডেট হলো")

    এখন যেকোনো পরবর্তী get(key) কল সবসময় cache-এ সর্বশেষ মান পাবে — কোনো stale window থাকবে না, কিন্তু প্রতিটি রাইটে দুটি অপারেশনের খরচ যোগ হলো।

  2. সিদ্ধান্ত নিন: একটি সোশ্যাল মিডিয়া অ্যাপে "পোস্টের লাইক সংখ্যা" ও "ইউজারের প্রোফাইল তথ্য" — এই দুটির জন্য আপনি কোন ক্যাশিং প্যাটার্ন বেছে নেবেন এবং কেন?

    লাইক সংখ্যা: অত্যন্ত উচ্চ-ফ্রিকোয়েন্সি আপডেট, সাময়িক অসামঞ্জস্য সহনীয় — write-back বা এমনকি শুধু ক্যাশে রেখে পর্যায়ক্রমে (periodic batch) ডেটাবেসে ফ্লাশ করাই ব্যবহারিক। প্রোফাইল তথ্য: তুলনামূলক কম পরিবর্তন হয় কিন্তু বারবার পড়া হয় (নাম, প্রোফাইল ছবি প্রতিটি পোস্টে দেখানো হয়) — এখানে cache-aside আদর্শ, কারণ এটি রিড-ভারী ওয়ার্কলোডের জন্য অপ্টিমাইজড এবং প্রোফাইল আপডেট বিরল হওয়ায় সামান্য stale window ঝুঁকিপূর্ণ নয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
L18 · ডিস্ট্রিবিউটেড ট্রানজেকশন ও টু-ফেজ কমিট