بک اند

کش کردن هوشمند با Redis

کش اگر درست طراحی نشود، فقط یک منبع جدید برای داده‌ی کهنه و باگ‌های عجیب است. در این مقاله نشان می‌دهیم کش با Redis را چطور و برای چه داده‌هایی بسازیم که واقعاً سرعت بدهد و خراب نشود.

  • نویسنده تیم تتا
  • ۸ دقیقه مطالعه
  • به‌روزرسانی:
کش کردن هوشمند با Redis

چه چیزی را با Redis کش کنیم؟

Redis پایگاه‌داده‌ی درون‌حافظه‌ای است و خواندن یک کلید معمولاً در کسری از میلی‌ثانیه انجام می‌شود، در حالی که یک پرس‌وجوی سنگین SQL ممکن است صدها میلی‌ثانیه طول بکشد. اما سرعت تنها معیار نیست؛ داده باید «کش‌پذیر» باشد.

  • خوب برای کش: فهرست محصولات، نتیجه‌ی گزارش‌های سنگین، پاسخ APIهای خارجی، تنظیمات سایت، نتیجه‌ی محاسبه‌های گران.
  • بد برای کش: موجودی لحظه‌ای انبار، مانده‌ی حساب، هر چیزی که خواندن کهنه‌ی آن خسارت مالی بسازد.
  • ملاک ساده: نسبت خواندن به نوشتن؛ اگر دست‌کم ده برابر باشد، کش معمولاً می‌ارزد.

پیش از افزودن کش، ابتدا ایندکس و پرس‌وجو را بهینه کنید؛ کش نباید پوششی بر پرس‌وجوی بد باشد. این موضوع در بخش گلوگاه‌ها در طراحی API های مقیاس‌پذیر هم آمده است.

الگوی cache-aside و جایگزین‌های آن

در cache-aside برنامه خودش کش را مدیریت می‌کند: اول کش را می‌خواند؛ اگر نبود (miss) از پایگاه‌داده می‌گیرد، در کش می‌گذارد و برمی‌گرداند.

js
// Node.js with node-redis
async function getProduct(id) {
  const key = "product:" + id;
  const hit = await redis.get(key);
  if (hit) return JSON.parse(hit);

  const row = await db.query("SELECT * FROM products WHERE id = $1", [id]);
  if (!row) return null;
  // 10 min base TTL + up to 60 s jitter
  const ttl = 600 + Math.floor(Math.random() * 60);
  await redis.set(key, JSON.stringify(row), { EX: ttl });
  return row;
}
سه الگوی رایج کش
الگونحوه‌ی کارریسک
Cache-asideبرنامه کش را پر می‌کنداولین درخواست کند است
Write-throughهر نوشتن، هم‌زمان کش را به‌روز می‌کندنوشتن کندتر؛ کش پر از داده‌ی کم‌خوانده
Write-behindنوشتن اول در کش، بعد دسته‌ای در پایگاه‌دادهخطر از دست رفتن داده با خرابی Redis

برای شروع cache-aside را انتخاب کنید؛ مدل ذهنی ساده‌ای دارد و خرابی Redis فقط به کندشدن منجر می‌شود، نه از دست رفتن داده.

تصویر انتزاعی و رنگارنگ از یک لایه‌ی حافظه‌ی درخشان میان برنامه و پایگاه‌داده که جریان داده را سریع می‌کند
کش وقتی می‌ارزد که نرخ برخورد (hit rate) بالا و ابطال آن قابل‌پیش‌بینی باشد.

TTL، کلیدگذاری و حافظه

TTL و سیاست حذف

هر کلید باید TTL داشته باشد؛ در غیر این صورت کش به‌مرور حافظه را پر می‌کند. مقدار TTL تعادلی میان تازگی و بار پایگاه‌داده است: تنظیمات سایت ممکن است ساعتی باشد و فهرست محصولات یک تا پنج دقیقه. برای اینکه کلیدهای هم‌زمان همگی یک لحظه منقضی نشوند، چند ثانیه نوسان تصادفی (jitter) اضافه کنید.

برای محدودکردن حافظه، maxmemory و سیاست allkeys-lru را تنظیم کنید تا Redis هنگام پرشدن، کم‌استفاده‌ترین کلیدها را حذف کند.

bash
# redis.conf
maxmemory 512mb
maxmemory-policy allkeys-lru

نام‌گذاری کلیدها

از ساختار نوع:شناسه:نسخه استفاده کنید، مثل product:42:v3. بخش نسخه به شما اجازه می‌دهد با عوض‌کردن یک عدد در کد، همه‌ی کلیدهای قدیمی را بدون حذف دستی بی‌اثر کنید. از دستور KEYS * در محیط واقعی دوری کنید، چون Redis را مسدود می‌کند؛ به‌جای آن SCAN بزنید.

ابطال کش: سخت‌ترین بخش

سه راهبرد اصلی وجود دارد و معمولاً ترکیبی از آن‌ها بهترین است:

  1. انقضای زمانی: فقط به TTL تکیه کنید. ساده است و برای داده‌ای که چند دقیقه کهنگی‌اش مهم نیست کافی است.
  2. حذف هنگام نوشتن: پس از به‌روزرسانی رکورد، کلید مربوط را با DEL حذف کنید تا درخواست بعدی دوباره بسازد.
  3. کلید نسخه‌دار: شماره‌ی نسخه‌ی دسته را در یک کلید جدا نگه دارید و با افزایش آن همه‌ی کلیدهای دسته را یک‌جا بی‌اثر کنید.

جلوگیری از cache stampede

وقتی یک کلید پرترافیک منقضی می‌شود، صدها درخواست هم‌زمان miss می‌گیرند و همگی همان پرس‌وجوی سنگین را اجرا می‌کنند. پایگاه‌داده زیر بار می‌رود، پاسخ‌ها کندتر می‌شوند و مشکل بدتر می‌شود. ساده‌ترین دفاع، قفل کوتاه است: فقط یک درخواست کش را بازسازی می‌کند و بقیه کمی صبر می‌کنند.

js
// SET NX = only if absent; EX = auto-release after 10 s
const got = await redis.set("lock:" + key, "1", { NX: true, EX: 10 });
if (got) {
  try { /* rebuild value and SET it */ }
  finally { await redis.del("lock:" + key); }
} else {
  await new Promise(r => setTimeout(r, 50)); // wait, then re-read cache
}

راه مکمل، jitter در TTL است که در بخش قبل گفتیم و «تازه‌سازی پیش از انقضا» برای کلیدهای بسیار داغ.

عملیات: ماندگاری، امنیت و پایش

  • ماندگاری: اگر Redis فقط کش است، می‌توانید RDB/AOF را خاموش کنید؛ ولی اگر صف یا شمارنده هم در آن است، فعال نگه دارید و مثل پایگاه‌داده پشتیبان بگیرید.
  • امنیت: Redis را هرگز مستقیم روی اینترنت باز نگذارید؛ رمز و فایروال یا شبکه‌ی خصوصی بگذارید.
  • پایش: نرخ برخورد (keyspace_hits و keyspace_misses در INFO stats)، حافظه‌ی مصرفی و تعداد حذف‌ها را ببینید. نرخ برخورد زیر حدود ۸۰٪ معمولاً یعنی کلیدگذاری یا TTL بد است.

مستندات رسمی دستورها و سیاست‌های حافظه در redis.io/docs آمده است. برای ساخت داشبورد این شاخص‌ها به مانیتورینگ و لاگ‌گیری حرفه‌ای سر بزنید، و اگر سایتتان به‌خاطر کندی در نتایج و تجربه‌ی کاربر آسیب می‌بیند، بهینه‌سازی سرعت بارگذاری صفحه سمت کلاینت را هم ببینید.

اگر سیستم شما با ترافیک بالا کند شده و نمی‌دانید از کجا شروع کنید، خدمات توسعه و بهینه‌سازی وب ما شامل بررسی لایه‌ی کش هم هست.

پرسش‌های متداول

Redis برای کش بهتر است یا Memcached؟

برای کش ساده هر دو خوب‌اند. Redis ساختارهای داده‌ی غنی‌تر (hash، set، sorted set)، TTL انعطاف‌پذیر و ماندگاری اختیاری دارد، به همین دلیل در اغلب پروژه‌ها انتخاب پیش‌فرض است.

TTL مناسب برای کش چقدر است؟

عدد ثابتی وجود ندارد. به میزان تحمل کهنگی داده بستگی دارد: چند ثانیه برای داده‌ی پویا، چند دقیقه برای فهرست‌ها و چند ساعت برای تنظیمات. همیشه چند ثانیه jitter تصادفی اضافه کنید.

اگر Redis از کار بیفتد چه می‌شود؟

در الگوی cache-aside برنامه باید به پایگاه‌داده برگردد و فقط کندتر شود. برای جلوگیری از ازکارافتادن کل سیستم، timeout کوتاه روی اتصال Redis بگذارید و خطای کش را نادیده بگیرید.

چرا نرخ برخورد کش پایین است؟

معمولاً به‌خاطر TTL بسیار کوتاه، کلیدهایی که پارامترهای متغیر (مثل زمان) دارند، یا کش‌کردن داده‌ای که بیش از یک بار خوانده نمی‌شود. کلیدها و الگوی دسترسی را بررسی کنید.

دیدگاه‌ها

    هنوز دیدگاهی ثبت نشده است. اولین نفر باشید.

    دیدگاه‌ها پس از بررسی منتشر می‌شوند.