ایزی ریت
ورود / ثبت‌نامورود

کش کردن API قیمت ارز با Redis؛ سایت سریع‌تر و مصرف سهمیهٔ کمتر

کش کردن API قیمت ارز با Redis: TTL مناسب، الگوی cache-aside، جلوگیری از هجوم هم‌زمان (stampede)، نگه داشتن آخرین مقدار معتبر و کد در Node.js، PHP و پایتون.

عرفان فضلی
· کارشناس توسعه و برنامه نویسی
انتشار ۸ آبان ۱۴۰۵، ۱۶:۰۰ · ۴ دقیقه مطالعه
کش کردن API قیمت ارز با Redis
خلاصه در سه خط
  • نرخ‌ها را با کلید ثابت و TTL برابر تازگی پلن (مثلاً ۶۰ ثانیه) در Redis نگه دارید.
  • یک کلید جدا بدون انقضا برای «آخرین مقدار معتبر» بسازید تا در قطعی API صفحه خالی نشود.
  • با قفل کوتاه یا تازه‌سازی پس‌زمینه جلوی هجوم هم‌زمان درخواست‌ها هنگام انقضای کش را بگیرید.

فرض کنید سایت شما روزی ۵۰ هزار بازدید دارد و قیمت دلار در هدر همهٔ صفحه‌ها نمایش داده می‌شود. اگر هر بازدید یک درخواست به API بفرستد، روزی ۵۰ هزار درخواست ثبت می‌شود؛ در حالی که نرخ‌ها فقط هر دقیقه عوض می‌شوند و ۱۴۴۰ درخواست کافی بود. کش کردن API قیمت ارز این شکاف را می‌بندد و Redis رایج‌ترین ابزار آن است.

★
پاسخ کوتاه
پاسخ API را با SET rates:all <json> EX 60 ذخیره کنید و هر صفحه اول از Redis بخواند. کلید دوم rates:last را بدون انقضا نگه دارید و در قطعی API از آن استفاده کنید. TTL را برابر meta.freshness_sec پلن خود بگذارید.

این آموزش بخشی از راهنمای کامل API قیمت ارز و طلا است. اگر Redis ندارید، کش فایلی در آموزش PHP و لاراول هم برای سایت‌های کوچک کافی است.

چرا Redis؟

روش کشسرعت خواندناشتراک بین چند سرورمناسب برای
حافظهٔ برنامه (متغیر)بسیار سریعنداردیک پروسهٔ Node.js یا ربات
فایلسریعنداردهاست اشتراکی، سایت کوچک
Redisبسیار سریعداردسایت پربازدید، چند سرور، صف
پایگاه دادهمتوسطداردوقتی تاریخچه هم ذخیره می‌کنید

الگوی پایه: cache-aside

منطق ساده است: اول از کش بخوان؛ اگر نبود از API بگیر و در کش بنویس. نسخهٔ Node.js با کتابخانهٔ ioredis:

javascript
import Redis from "ioredis"
const redis = new Redis(process.env.REDIS_URL)
const TTL = 60

export async function getRates() {
  const cached = await redis.get("rates:all")
  if (cached) return JSON.parse(cached)

  try {
    const r = await fetch("https://api.ezrate.me/v1/rates?category=fiat", {
      headers: { "X-API-Key": process.env.EZRATE_KEY }, signal: AbortSignal.timeout(8000) })
    if (!r.ok) throw new Error(String(r.status))
    const data = (await r.json()).data
    await redis.multi()
      .set("rates:all", JSON.stringify(data), "EX", TTL)
      .set("rates:last", JSON.stringify(data))      // آخرین مقدار معتبر، بدون انقضا
      .exec()
    return data
  } catch {
    const last = await redis.get("rates:last")
    return last ? JSON.parse(last) : []
  }
}

مشکل هجوم هم‌زمان (Cache Stampede)

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

راه حل ۱: قفل کوتاه

javascript
const got = await redis.set("rates:lock", "1", "NX", "EX", 10)
if (!got) {
  // کس دیگری در حال تازه‌سازی است: آخرین مقدار را برگردان
  return JSON.parse((await redis.get("rates:last")) ?? "[]")
}
// … دریافت از API و نوشتن در کش، سپس:
await redis.del("rates:lock")

راه حل ۲: تازه‌سازی پس‌زمینه (پیشنهادی)

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

نمودار تعداد درخواست به API قبل و بعد از کش Redis
کاهش درخواست‌ها پس از راه‌اندازی کش

همین الگو در PHP و پایتون

php
<?php
// PHP با افزونهٔ phpredis
$redis = new Redis();
$redis->connect('127.0.0.1');
$json = $redis->get('rates:all');
if ($json === false) {
    $json = fetch_from_api();              // تابع آموزش PHP
    $redis->setex('rates:all', 60, $json);
    $redis->set('rates:last', $json);
}
$rates = json_decode($json, true);
python
# پایتون با redis-py
import json, redis
r = redis.Redis()
data = r.get("rates:all")
if data is None:
    data = json.dumps(fetch_rates("usd,eur,g18"))   # تابع آموزش پایتون
    r.set("rates:all", data, ex=60)
    r.set("rates:last", data)
rates = json.loads(data)

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

TTL کمتر از تازگی پلن شما فایده‌ای ندارد؛ مقدار آن در فیلد meta.freshness_sec هر پاسخ آمده است. برای صفحه‌هایی که فقط نمایش می‌دهند، ۶۰ ثانیه تعادل خوبی است. برای گزارش‌های روزانه یا قیمت‌گذاری کالا، کش چند دقیقه‌ای هم کاملاً مناسب است.

✓
پیشنهاد
زمان دریافت داده را هم کنار نرخ‌ها ذخیره کنید و در صفحه نشان دهید («به‌روزرسانی: ۱۴:۳۲»). وقتی از rates:last استفاده می‌کنید، این زمان به کاربر می‌گوید عدد ممکن است چند دقیقه قدیمی باشد.

برای مدیریت خطاهای API هنگام تازه‌سازی کش، مدیریت خطا و Rate Limit در APIهای مالی را ببینید.

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

از همان ابتدا برای کلیدهای Redis یک قرارداد نام‌گذاری بگذارید تا بعداً قاطی نشوند. پیشنهاد ما: ezrate:{endpoint}:{params}. مثلاً ezrate:rates:fiat:IRT برای نرخ ارزها به تومان و ezrate:rates:gold:usd برای طلا بر حسب دلار. اگر پارامترها زیادند، از هش آن‌ها استفاده کنید. نسخه را هم در نام بگذارید (ezrate:v1:…) تا اگر ساختار ذخیره را عوض کردید، کلیدهای قدیمی مزاحم نشوند.

مانیتورینگ کش

دو عدد را زیر نظر داشته باشید: نسبت برخورد به کش (hit ratio) و تعداد درخواست واقعی به API. اولی را با شمارنده‌های ساده در کد (INCR stats:hit و INCR stats:miss) و دومی را با مسیر GET /usage خود API می‌بینید؛ این مسیر از سهمیه کم نمی‌شود. اگر hit ratio زیر ۹۵ درصد است، احتمالاً TTL کوتاه است یا کلیدها بیش از حد متنوع‌اند.

هاست اشتراکی بدون Redis

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

کش چند لایه

در سایت‌های خیلی پربازدید، کش چندلایه بهترین نتیجه را می‌دهد: Redis برای نرخ‌ها با TTL شصت ثانیه، کش صفحهٔ کامل یا CDN با TTL کوتاه‌تر برای صفحه‌هایی که نرخ را نمایش می‌دهند، و کش مرورگر برای مسیر داخلی نرخ‌ها (Cache-Control: max-age=30). هر لایه بار لایهٔ پشتی را کم می‌کند و API تنها یک درخواست در دقیقه می‌بیند.

شمارنده‌های hit و miss کش در Redis
نسبت برخورد به کش پس از راه‌اندازی

نرخ‌های لحظه‌ای مرتبط با این مطلب: قیمت دلار، قیمت طلای ۱۸ عیار، قیمت سکه امامی.

کش کردن API قیمت ارز باعث نمایش عدد قدیمی نمی‌شود؟
اگر TTL برابر تازگی پلن باشد، نه. نرخ‌ها هر دقیقه محاسبه می‌شوند و کش ۶۰ ثانیه‌ای عملاً عدد تازه را نشان می‌دهد.
اگر Redis از کار بیفتد چه می‌شود؟
کد را طوری بنویسید که در خطای Redis مستقیم از API بخواند و صفحه از کار نیفتد. کش باید بهبود باشد، نه نقطهٔ شکست جدید.
برای هر کاربر کش جدا لازم است؟
نه. نرخ‌ها برای همهٔ کاربران یکسان‌اند؛ یک کلید مشترک کافی است.
آیا می‌شود از CDN برای کش استفاده کرد؟
بله. اگر سرور شما نرخ‌ها را با هدر Cache-Control: max-age=60 ارائه دهد، CDN هم پاسخ را کش می‌کند. اما خود پاسخ مستقیم EZRate را که کلید دارد در CDN عمومی کش نکنید.
عرفان فضلی
من در حوزه توسعه نرم‌افزار و برنامه‌نویسی فعالیت می‌کنم و به طراحی و پیاده‌سازی راهکارهای کاربردی و بهینه علاقه‌مندم. تمرکز من بر توسعه قابلیت‌های جدید، بهبود عملکرد سیستم‌ها و حل چالش‌های فنی با استفاده از فناوری‌های روز است. در مسیر کاری خود تلاش می‌کنم با یادگیری مستمر و به‌کارگیری روش‌های نوین برنامه‌نویسی، محصولات نرم‌افزاری کارآمد، پایدار و قابل‌اعتماد توسعه دهم.
همهٔ مطالب نویسنده ←

نظرات (۰)

نظرها پس از تأیید نمایش داده می‌شوند؛ برای ارسال، ورود به حساب لازم است.

مطالب مرتبط

API و برنامه‌نویسی
وب سوکت قیمت لحظه ای ارز لازم است یا REST؟ معماری درست برای اپ‌های مالی
API و برنامه‌نویسی
تغییر قیمت ووکامرس بر اساس دلار؛ قیمت‌گذاری خودکار با نرخ لحظه‌ای
API و برنامه‌نویسی
سورس ربات تلگرام قیمت ارز با Node.js؛ آموزش ساخت ربات اعلام قیمت دلار و سکه