📖 درس هفتم: Caching و افزایش عملکرد API
🎯 هدف این درس: آشنایی با تکنیکهای بهینهسازی عملکرد API شامل پیادهسازی کش با Redis، استفاده از ETag، Cache-Control، Query Optimization و روشهای افزایش سرعت پاسخدهی.
مهمان عزیز! 👋
تا اینجا یک API کامل با امنیت، احراز هویت و مستندات حرفهای ساختهایم. اما یک API خوب باید سریع هم باشد!
وقتی کاربران از API شما استفاده میکنند، زمان پاسخدهی (Response Time) یکی از مهمترین عواملی است که تجربه کاربری را تعیین میکند. در این درس، با تکنیکهای مختلفی آشنا میشویم که API شما را تا چندین برابر سریعتر میکنند.
💡 چرا Caching مهم است؟
کش کردن (Caching) یعنی ذخیره موقت نتایج محاسبات سنگین یا کوئریهای دیتابیس، تا درخواستهای بعدی بتوانند پاسخ را سریعتر دریافت کنند. این کار باعث میشود:
- ✅ زمان پاسخدهی تا ۱۰ برابر کاهش یابد.
- ✅ بار روی دیتابیس کاهش پیدا کند.
- ✅ هزینههای سرور کمتر شود.
- ✅ تجربه کاربری بهبود یابد.
🗄️ آشنایی با Redis
Redis یک دیتابیس NoSQL درونحافظهای (In-Memory) است که برای کش کردن و ذخیره موقت دادهها استفاده میشود. سرعت بالای Redis به دلیل ذخیره دادهها در RAM است.
نصب و راهاندازی Redis
- روش ۱ – در لینوکس:
sudo apt-get install redis-server - روش ۲ – در ویندوز: از Docker یا WSL استفاده کنید.
- روش ۳ – با Docker:
docker run -p 6379:6379 --name redis -d redis
نصب کتابخانه PhpRedis
Install Predis
Composer
# نصب Predis (کتابخانه PHP برای Redis)
composer require predis/predis
# یا استفاده از PhpRedis Extension (پیشفرض نیست)
# sudo pecl install redis
🔗 کلاس Cache Manager
یک کلاس برای مدیریت کش با Redis میسازیم:
Cache.php
PHP
8.2
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Predis\Client;
class Cache
{
private static $instance = null;
private $redis;
private $enabled = true;
private function __construct()
{
try {
$this->redis = new Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 2.5
]);
// تست اتصال
$this->redis->ping();
} catch (Exception $e) {
// اگر Redis در دسترس نبود، کش غیرفعال میشود
$this->enabled = false;
error_log('Redis connection failed: ' . $e->getMessage());
}
}
// Singleton Pattern
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// ذخیره داده در کش
public function set($key, $value, $ttl = 3600)
{
if (!$this->enabled) {
return false;
}
$serialized = serialize($value);
return $this->redis->setex($key, $ttl, $serialized);
}
// دریافت داده از کش
public function get($key)
{
if (!$this->enabled) {
return null;
}
$data = $this->redis->get($key);
if ($data === null) {
return null;
}
return unserialize($data);
}
// حذف یک کلید از کش
public function delete($key)
{
if (!$this->enabled) {
return false;
}
return $this->redis->del($key);
}
// پاک کردن کش بر اساس الگو
public function deletePattern($pattern)
{
if (!$this->enabled) {
return false;
}
$keys = $this->redis->keys($pattern);
if (empty($keys)) {
return true;
}
return $this->redis->del($keys);
}
// بررسی وجود کلید در کش
public function exists($key)
{
if (!$this->enabled) {
return false;
}
return $this->redis->exists($key) > 0;
}
// بررسی فعال بودن کش
public function isEnabled()
{
return $this->enabled;
}
}
⚡ پیادهسازی کش در کنترلر
حالا از کش برای ذخیره نتایج کوئریهای سنگین استفاده میکنیم:
UserController.php (با کش)
PHP
8.2
<?php
require_once __DIR__ . '/../Core/Cache.php';
class UserController
{
private $userModel;
private $cache;
public function __construct()
{
require_once __DIR__ . '/../Models/User.php';
$this->userModel = new User();
$this->cache = Cache::getInstance();
}
// GET /users - دریافت لیست کاربران با کش
public function index($request)
{
// احراز هویت
require_once __DIR__ . '/../Core/AuthMiddleware.php';
AuthMiddleware::authenticate();
AuthMiddleware::authorize(['admin']);
// ساخت کلید کش بر اساس پارامترها
$page = $_GET['page'] ?? 1;
$limit = $_GET['limit'] ?? 10;
$filters = $_GET ?? [];
// حذف پارامترهای غیرمرتبط با فیلتر
unset($filters['page']);
unset($filters['limit']);
$cacheKey = 'users:' . md5($page . ':' . $limit . ':' . json_encode($filters));
// بررسی وجود داده در کش
$cachedData = $this->cache->get($cacheKey);
if ($cachedData !== null) {
// ارسال پاسخ از کش
header('X-Cache: HIT');
return Response::success($cachedData);
}
// داده در کش وجود ندارد - از دیتابیس دریافت کن
$users = $this->userModel->getAll($page, $limit, $filters);
$total = $this->userModel->getTotalCount($filters);
$responseData = [
'data' => $users,
'pagination' => [
'current_page' => $page,
'per_page' => $limit,
'total' => $total,
'total_pages' => ceil($total / $limit)
]
];
// ذخیره در کش به مدت ۵ دقیقه (۳۰۰ ثانیه)
$this->cache->set($cacheKey, $responseData, 300);
header('X-Cache: MISS');
return Response::success($responseData);
}
// GET /users/{id} - دریافت یک کاربر با کش
public function show($request, $params)
{
$id = $params[0] ?? null;
if (!$id || !is_numeric($id)) {
return Response::error('Valid user ID is required', 400);
}
$cacheKey = 'user:' . $id;
// بررسی کش
$cachedData = $this->cache->get($cacheKey);
if ($cachedData !== null) {
header('X-Cache: HIT');
return Response::success($cachedData);
}
$user = $this->userModel->find($id);
if (!$user) {
return Response::notFound('User not found');
}
// ذخیره در کش به مدت ۱۰ دقیقه
$this->cache->set($cacheKey, $user, 600);
header('X-Cache: MISS');
return Response::success($user);
}
// POST /users - ایجاد کاربر (پاک کردن کش)
public function store($request)
{
// ... کد ایجاد کاربر ...
// پس از ایجاد کاربر، کش لیست کاربران را پاک کن
$this->cache->deletePattern('users:*');
return Response::created($user);
}
// PUT /users/{id} - بروزرسانی کاربر (پاک کردن کش)
public function update($request, $params)
{
$id = $params[0] ?? null;
// ... کد بروزرسانی کاربر ...
// پاک کردن کش کاربر و لیست کاربران
$this->cache->delete('user:' . $id);
$this->cache->deletePattern('users:*');
return Response::success($user, 'User updated successfully');
}
// DELETE /users/{id} - حذف کاربر (پاک کردن کش)
public function delete($request, $params)
{
$id = $params[0] ?? null;
// ... کد حذف کاربر ...
// پاک کردن کش کاربر و لیست کاربران
$this->cache->delete('user:' . $id);
$this->cache->deletePattern('users:*');
return Response::noContent();
}
}
📌 ETag و Cache-Control
ETag و Cache-Control هدرهایی هستند که به کلاینت اجازه میدهند دادهها را به صورت محلی کش کند و از ارسال درخواستهای تکراری جلوگیری کنند.
ETag & Cache-Control
PHP
8.2
<?php
class CacheControl
{
// تنظیم هدرهای Cache-Control
public static function setCacheHeaders($maxAge = 3600)
{
header('Cache-Control: public, max-age=' . $maxAge);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $maxAge) . ' GMT');
header('Pragma: cache');
}
// تنظیم هدر برای عدم کش (مخصوص دادههای حساس)
public static function setNoCacheHeaders()
{
header('Cache-Control: no-store, no-cache, must-revalidate, private');
header('Pragma: no-cache');
header('Expires: -1');
}
// تولید ETag بر اساس داده
public static function generateETag($data)
{
$etag = md5(json_encode($data));
header('ETag: "' . $etag . '"');
return $etag;
}
// بررسی If-None-Match برای ETag
public static function checkETag($data)
{
$etag = md5(json_encode($data));
$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
if (strpos($ifNoneMatch, $etag) !== false) {
// داده تغییر نکرده است، پاسخ 304 Not Modified
http_response_code(304);
exit;
}
header('ETag: "' . $etag . '"');
}
}
// استفاده در کنترلر
public function getUserWithETag($request, $params)
{
$user = $this->userModel->find($params[0]);
// بررسی ETag
CacheControl::checkETag($user);
// تنظیم هدرهای کش
CacheControl::setCacheHeaders(600); // ۱۰ دقیقه
return Response::success($user);
}
⚡ Query Optimization
بهینهسازی کوئریهای دیتابیس نقش مهمی در افزایش سرعت API دارد:
Query Optimization
SQL
MySQL 8.x
-- ❌ کوئری بدون ایندکس (کند)
SELECT * FROM users WHERE email = 'younes@example.com';
-- ✅ اضافه کردن ایندکس روی email
CREATE INDEX idx_users_email ON users(email);
-- ❌ استفاده از SELECT * (دریافت ستونهای اضافی)
SELECT * FROM users WHERE id = 1;
-- ✅ فقط ستونهای مورد نیاز را انتخاب کن
SELECT id, name, email, role FROM users WHERE id = 1;
-- ❌ کوئری با JOIN بدون ایندکس
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = 1;
-- ✅ اضافه کردن ایندکس روی کلید خارجی
CREATE INDEX idx_orders_user_id ON orders(user_id);
📊 مقایسه عملکرد با و بدون کش
| سناریو | بدون کش | با کش (Redis) | بهبود |
|---|---|---|---|
| دریافت لیست ۱۰۰ کاربر | ~۱۵۰ms | ~۵ms | ۳۰x |
| دریافت یک کاربر با JOIN | ~۸۰ms | ~۲ms | ۴۰x |
| جستجوی پیشرفته با فیلتر | ~۲۵۰ms | ~۸ms | ۳۱x |
🔴 خطاهای رایج در Caching
- ❌ اشتباه: کش کردن دادههای حساس بدون کنترل دسترسی.
- ✅ درست: دادههای حساس را با
privateCache-Control کش کن.
- ❌ اشتباه: عدم پاک کردن کش پس از بروزرسانی داده.
- ✅ درست: پس از هر عملیات CRUD، کش مرتبط را پاک کن.
- ❌ اشتباه: زمان TTL بسیار طولانی.
- ✅ درست: TTL را متناسب با میزان تغییرات داده تنظیم کن.
- ❌ اشتباه: کش کردن همه چیز بدون استراتژی.
- ✅ درست: فقط دادههایی که هزینه محاسباتی بالایی دارند را کش کن.
💎 نکات کلیدی درس
- ✅ Redis یک دیتابیس In-Memory برای کش کردن است.
- ✅ Cache Manager با Singleton Pattern مدیریت کش را ساده میکند.
- ✅ ETag از ارسال مجدد دادههای بدون تغییر جلوگیری میکند.
- ✅ Cache-Control زمان کش شدن دادهها را در کلاینت کنترل میکند.
- ✅ پس از هر تغییر داده، کش مرتبط باید پاک شود (Cache Invalidation).
- ✅ کوئریهای دیتابیس را با ایندکسگذاری بهینه کن.
- ✅ از هدر
X-Cacheبرای نشان دادن وضعیت کش استفاده کن.
🛠️ پروژه عملی درس هفتم
مهمان عزیز، حالا سیستم کش را به API محصولات اضافه کن!
مسئله:
یک سیستم کش کامل برای API محصولات پیادهسازی کن:
- ✅ کش کردن لیست محصولات با Redis (TTL: ۵ دقیقه).
- ✅ کش کردن جزئیات هر محصول (TTL: ۱۰ دقیقه).
- ✅ استفاده از ETag برای محصولات.
- ✅ پاک کردن کش پس از ایجاد، بروزرسانی یا حذف محصول.
- ✅ Cache-Control مناسب برای دادههای مختلف.
- ✅ اضافه کردن هدر X-Cache به پاسخها.
🧪 راهحل پروژه (پاسخ)
مراحل پیادهسازی:
- 1️⃣ کلاس
Cacheرا با Redis پیادهسازی کن. - 2️⃣ در
ProductControllerاز کش برایindexوshowاستفاده کن. - 3️⃣ کلاس
CacheControlرا برای ETag و Cache-Control ایجاد کن. - 4️⃣ در متدهای
store،updateوdeleteکش مرتبط را پاک کن. - 5️⃣ Redis را نصب و راهاندازی کن و کش را تست کن.
📝 تمرینهای عملی
🧪 تمرین ۱ (ساده):
Redis را روی سیستم خود نصب کن و کلاس Cache را تست کن. یک کلید با مقدار مشخص ذخیره کن و سپس آن را دریافت کن.
🧪 تمرین ۲ (متوسط):
یک سیستم کش هوشمند پیادهسازی کن که بر اساس پارامترهای category و price کلیدهای کش متفاوت تولید کند.
🧪 تمرین ۳ (چالشی):
یک سیستم کش دینامیک با TTL متغیر پیادهسازی کن. دادههایی که بیشتر درخواست میشوند، TTL بیشتری داشته باشند و دادههای کماستفاده TTL کمتری داشته باشند.
🏁 جمعبندی درس
مهمان عزیز، در این درس یاد گرفتی:
- ✅ Redis چیست و چگونه نصب و استفاده میشود.
- ✅ چگونه یک Cache Manager با Singleton Pattern بسازی.
- ✅ چگونه از کش در کنترلرها برای افزایش سرعت استفاده کنی.
- ✅ ETag و Cache-Control چیست و چگونه پیادهسازی میشود.
- ✅ چگونه کوئریهای دیتابیس را با ایندکس بهینه کنی.
- ✅ چگونه کش را پس از تغییر داده پاک کنی (Cache Invalidation).
حالا API شما با استفاده از کش، چندین برابر سریعتر شده است. در درس بعدی، با API Versioning و مدیریت چرخه حیات API آشنا میشویم. آمادهای؟ 🚀
🗺️ نقشه راه دوره: شما اینجا هستید!
برای اینکه بدانی دقیقاً کجای مسیر هستی و چه درسهایی در انتظار توست، به جدول زیر نگاه کن. درس فعلی با رنگ متفاوت مشخص شده است.
| درس | عنوان درس | آنچه یاد میگیرید |
|---|---|---|
| ۱ | مفاهیم پایه API | HTTP، REST، معماری Client-Server |
| ۲ | پیادهسازی RESTful API با PHP خام | مسیریابی، متدها، پارامترها، پاسخهای JSON |
| ۳ | مدیریت دادهها و پایگاهداده | اتصال PDO، CRUD، تراکنشها، Pagination |
| ۴ | اعتبارسنجی و احراز هویت | JWT، Roles/Permissions، Middleware |
| ۵ | امنیت API پیشرفته | OWASP Top 10، SQLi، XSS، CSRF، Rate Limiting |
| ۶ | مستندسازی API با OpenAPI | Swagger، OpenAPI Specification، مستندات تعاملی |
| ۷ | Caching و افزایش عملکرد | Redis، ETag، Cache-Control، Query Optimization |
| ۸ | API Versioning و مدیریت چرخه حیات | استراتژیهای نسخهبندی، Deprecation، Backward Compatibility |
| ۹ | تست و دیباگ API حرفهای | PHPUnit، Mocking، Logging، Postman Collection |
| ۱۰ | پروژه نهایی: API فروشگاهی | پروژه کامل فروشگاه اینترنتی با تمام قابلیتها |




نظر خود را بنویسید
با ثبت نظر، به بهبود محتوای ما کمک کنید