البداية · فصل ٢

أنماط الكاش — Cache-Aside وأخواته

cache-aside · read-through · write-through · write-behind · write-around · TTL · keys
🎯 الهدف: تكتب cache-aside كامل بـ Redis والـ Postgres، وتعرف الأنماط التانية وإمتى تستخدم كل واحد.

النمط الأشهر: Cache-Aside

ده النمط الأشهر، والتطبيق هو اللي بيكلّم الكاش والداتابيز الاتنين بنفسه.

  • القراءة: اسأل الكاش الأول. لو لقيت (hit) رجّع على طول.
  • لو ملقتش (miss): هات من الداتابيز، حط في الكاش بـ TTL، ورجّع.
  • الكتابة: اكتب في الداتابيز، وامسح الـ key من الكاش (التفاصيل في الفصل الجاي).
  • ليه اسمه كمان Lazy Loading؟ لأن القيمة مبتدخلش الكاش غير لما حد يطلبها فعلًا.

الكود كامل

products.mjs
import Redis from "ioredis";import pg from "pg";const redis = new Redis(process.env.REDIS_URL ?? "redis://127.0.0.1:6379");const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });const TTL_SEC = 300; // 5 دقايقconst keyOf = (id) => `product:v1:${id}`;export async function getProduct(id) {  const key = keyOf(id);  // 1) الكاش الأول  const cached = await redis.get(key);  if (cached !== null) return JSON.parse(cached);  // 2) miss: روح للداتابيز  const { rows } = await pool.query(    "SELECT id, name, price_cents FROM products WHERE id = $1",    [id],  );  const product = rows[0] ?? null;  // 3) خزّن بـ TTL (EX بالثواني، PX بالملّي ثانية)  if (product) await redis.set(key, JSON.stringify(product), "EX", TTL_SEC);  return product;}export async function updatePrice(id, priceCents) {  await pool.query("UPDATE products SET price_cents = $2 WHERE id = $1", [id, priceCents]);  await redis.del(keyOf(id)); // الطلب الجاي هيجيب القيمة الجديدة}
test.mjs
import { getProduct, updatePrice } from "./products.mjs";// قبلها: CREATE TABLE products (id int PRIMARY KEY, name text, price_cents int);//        INSERT INTO products VALUES (1, 'iPhone', 5000000);console.log(await getProduct(1)); // miss → من Postgresconsole.log(await getProduct(1)); // hit → من Redisawait updatePrice(1, 4500000);console.log(await getProduct(1)); // miss تاني → السعر الجديدprocess.exit(0);
  • جرّبته: أول call راحت Postgres، التانية جت من Redis، وبعد الـ update رجع السعر الجديد.
  • cached !== null: مهم. ioredis بيرجّع null لو الـ key مش موجود، فمتكتبش if (cached) لأن ممكن تبقى قيمة فاضية صحيحة.
  • الـ cents: الفلوس بتتخزن integer، عشان numeric في pg بيرجع string وfloat بيغلط في الكسور.

الـ Read-Through

  • الفكرة: التطبيق بيكلّم الكاش بس، والكاش نفسه هو اللي بيروح للداتابيز لما ميلاقيش.
  • إزاي؟ بتدي الكاش function اسمها loader، وهو بيناديها وقت الـ miss.
  • أمثلة: مكتبات زي cache-manager بـ wrap()، أو DataLoader، أو كاش في الـ ORM. Redis نفسه مبيعملش كده لوحده.
  • الفرق عن cache-aside: الكود أنضف، بس المنطق هو هو. وفيه نفس مشكلة الـ stampede لو المكتبة مبتعملش coalescing (راجع الـ docs بتاعتها).

الـ Write-Through

  • الفكرة: كل كتابة بتروح للداتابيز وللكاش في نفس العملية.
  • الميزة: الكاش دايمًا فيه آخر قيمة، والقراءة بعد الكتابة على طول بتلاقيها.
  • العيب: كل write أبطأ (بتكتب في مكانين)، والكاش بيتملي ببيانات ممكن محدش يقراها.
  • خلي بالك: لو الكتابة في الداتابيز نجحت والكاش فشل، بقى عندك قيمتين مختلفين. فخلي فيه TTL برضه كشبكة أمان.

الـ Write-Behind (أو Write-Back)

  • الفكرة: تكتب في الكاش بس، وفيه حاجة في الخلفية بتنقل للداتابيز بعدين على دفعات.
  • الميزة: الكتابة سريعة جدًا، ومفيدة للعدّادات (views، likes) اللي بتتكتب كتير.
  • العيب الكبير: لو الكاش وقع قبل ما ينقل، البيانات ضاعت. ومفيش consistency بين الكاش والداتابيز لحد ما النقل يحصل.
  • استخدمه بحذر: للحاجات اللي تستحمل تضيع منها حبة، مش للفلوس ولا الطلبات.

الـ Write-Around

  • الفكرة: الكتابة بتروح للداتابيز بس، والكاش مبيتلمسش. القيمة بتدخل الكاش بعدين لما حد يقراها (يعني مع cache-aside).
  • الميزة: الكاش مبيتملاش ببيانات اتكتبت ومحدش قراها، زي logs أو uploads.
  • العيب: أول قراءة بعد الكتابة دايمًا miss. ولو فيه قيمة قديمة في الكاش لازم تمسحها أو تستنى الـ TTL.

مقارنة سريعة

النمطالقراءةالكتابةيكسب فييخسر في
Cache-asideالتطبيق: كاش ثم DBDB ثم مسح الكاشبسيط، الكاش فيه اللي بيتقري بسأول قراءة miss، stampede
Read-throughالكاش بيجيب من DBأي نمطكود أنضفمحتاج مكتبة/طبقة
Write-throughمن الكاشDB + كاش مع بعضالكاش دايمًا حديثwrite أبطأ، بيانات مش مقروءة
Write-behindمن الكاشكاش، والـ DB بعدينwrite سريع جدًاضياع بيانات لو الكاش وقع
Write-aroundكاش ثم DBDB بسالكاش مبيتملاش زبالةأول قراءة miss

لو مش متأكد، ابدأ بـ cache-aside + مسح الـ key وقت الكتابة + TTL. ده اللي معظم الأنظمة بتعمله.

تختار الـ TTL إزاي؟

  • اسأل سؤال واحد: القيمة دي تستحمل تبقى قديمة قد إيه؟ ده الحد الأقصى للـ TTL.
  • بيانات بتتغير نادر: (categories، إعدادات) ساعات أو يوم.
  • بيانات بتتغير كل شوية: (سعر، مخزون معروض) ثواني لدقايق.
  • الـ TTL قصير جدًا: hit rate واطي، والـ keys بتخلص كتير، وفرص الـ stampede أكتر.
  • الـ TTL طويل جدًا: بيانات قديمة لو الـ invalidation فوّت حاجة، والـ memory بتتملي.
  • دايمًا حط TTL: حتى لو بتمسح وقت الكتابة. ده الضمان إن أي غلطة في الـ invalidation هتتصلح لوحدها في الآخر.
  • ومتخليهوش رقم ثابت لكل الـ keys: ده موضوع فصل ٦ (jitter).

الـ Serialization

  • JSON.stringify / JSON.parse: الأسهل وبيتقري بالعين في redis-cli. بس Date بترجع string، وBigInt بيرمي error، وundefined بيختفي.
  • حجم القيمة مهم: object بـ 2MB كل ما يتقري بيتنقل على الشبكة ويتعمله parse. خزّن اللي محتاجه بس.
  • بدائل: MessagePack أو compression للقيم الكبيرة، بس ده تعقيد زيادة. ابدأ بـ JSON.
  • الـ Redis hashes: لو بتقرا أو بتعدل field واحد بس، HSET/HGET أحسن من إنك تقرا الـ object كله.

تسمية الـ keys والـ versioning

أمثلة keys
product:v1:42                 ← نوع : version : iduser:v3:42:profile            ← حاجة تبع يوزرsearch:v1:shoes:page=2:sort=pricetenant:7:product:v1:42        ← multi-tenant: الـ tenant في الأول
  • الشكل: نوع:version:id، ونقطتين بين الأجزاء. ده الـ convention المعروف في Redis.
  • الـ version في الـ key: لما تغيّر شكل الـ object في الكود (زودت field أو غيرت اسم)، غيّر v1 لـ v2. الكود الجديد مش هيقرا القيم القديمة اللي شكلها غلط، والقديمة هتخلص بالـ TTL.
  • كل حاجة بتغيّر النتيجة لازم تبقى في الـ key: اللغة، الصفحة، الفلتر، الـ tenant. وإلا يوزر هياخد نتيجة يوزر تاني.
  • خلي الـ key قصير ومتوقع: متحطش JSON كامل في الـ key. لو الفلاتر كتير اعملها hash.

خلّصت الفصل لما

🧠 اختبر نفسك
١) عدّاد views بيتكتب آلاف المرات في الثانية. أنهي نمط ينفع؟
write-behind: INCR في Redis، وjob بينقل للداتابيز كل شوية. لو ضاع شوية views مش مشكلة.
٢) زودت field جديد للـ product في الكود، والكاش فيه objects قديمة من غيره. تعمل إيه؟
غيّر version الـ key من product:v1 لـ product:v2، والقديمة هتخلص بالـ TTL.
٣) بتمسح الكاش وقت كل update. لسه محتاج TTL؟
أيوه. الـ TTL شبكة أمان لو مسح فشل أو حد عدّل الداتابيز من برة التطبيق.