404 Not Found

404 Not Found


nginx

تحسين أداء Laravel العملي

الأداء هو شريان حياة SaaS — سيغادر المستخدمون إذا اضطروا للانتظار 3 ثوانٍ، والاستعلامات البطيئة قد تحرق مئات الدولارات من رسوم قاعدة البيانات في يوم واحد.

1. ما ستتعلمه


2. القصة الحقيقية لشركة SaaS سريعة النمو

(1) المشكلة: زمن استجابة ShopMetrics ارتفع من 200 مللي ثانية إلى 5 ثوانٍ

نما عدد مستخدمي ShopMetrics من 1,000 إلى 50,000، وزادت بيانات الطلبات من 100,000 إلى 5 ملايين. لاحظت Alice أن لوحة المعلومات تستغرق 5 ثوانٍ للتحميل، ورأى Bob أن رسوم قاعدة البيانات الشهرية قفزت من 50 دولارًا إلى 500 دولار — كل ذلك بسبب الاستعلامات البطيئة التي تحرق الميزانية. كشف تحليل Charlie أن استعلامات N+1، والفهارس المفقودة، وغياب التخزين المؤقت كانت المتهمين الرئيسيين الثلاثة.

(2) أساليب التحسين المنهجية

من خلال أربع طبقات من التحسين — الاستعلامات، التخزين المؤقت، الفهارس، وOPcache — كل طبقة قلصت زمن الاستجابة إلى النصف، ليصل في النهاية من 5 ثوانٍ إلى 200 مللي ثانية.

TEXT
قبل:  5000مللي ثانية  (استعلامات N+1، بدون تخزين مؤقت، بدون فهارس)
الخطوة 1:  2500مللي ثانية  (eager loading، إصلاح N+1)
الخطوة 2:   500مللي ثانية  (إضافة فهارس + تحسين الاستعلام)
الخطوة 3:   200مللي ثانية  (تخزين مؤقت Redis + OPcache)
الخطوة 4:   100مللي ثانية  (CDN + نسخة قراءة)

(3) النتيجة

بعد تحسين Bob، انخفضت تكاليف قاعدة البيانات من 500 دولار/شهر إلى 150 دولار/شهر، وارتفع معدل الاحتفاظ بالمستخدمين من 70% إلى 90%.


3. تحسين استعلامات Eloquent

(1) مشكلة استعلامات N+1

PHP
// ❌ N+1: استعلام واحد للمستأجرين + N استعلام للمتاجر
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
    echo $tenant->shops->count(); // استعلام إضافي واحد لكل مستأجر!
}
// الإجمالي: 1 + N استعلام

// ✅ Eager loading: استعلامان فقط
$tenants = Tenant::with('shops')->get();
foreach ($tenants as $tenant) {
    echo $tenant->shops->count(); // بدون استعلام إضافي
}
// الإجمالي: 2 استعلام

(2) مقارنة طرق تحسين الاستعلامات

الطريقة حالة الاستخدام استخدام الذاكرة الوصف
all() كمية بيانات صغيرة عالي تحميل الكل في الذاكرة دفعة واحدة
chunk() تكرار بيانات كبيرة منخفض معالجة في كتل من N سجل
chunkById() تكرار بيانات كبيرة (أكثر استقرارًا) منخفض تقسيم حسب المعرف، غير متأثر بتغييرات البيانات
cursor() قراءة قائمة على التدفق منخفض جدًا قراءة سطر بسطر قائمة على المولد
lazy() بيانات كبيرة، لكن تحتاج عمليات تجميع متوسط تحميل كتل عند الطلب
each() تكرار + معالجة منخفض chunk + استدعاء راجع

(1) ▶ مثال: إصلاح N+1 في تحليل طلبات ShopMetrics

PHP
// ❌ قبل: لوحة المعلومات تحمل في 5 ثوانٍ
// المتحكم: 1 + 3*N استعلام لكل مستأجر
public function dashboard(Tenant $tenant)
{
    $orders = $tenant->orders()
        ->whereMonth('created_at', now()->month)
        ->get();                          // استعلام 1

    $total = $orders->sum('total');
    $byShop = $orders->groupBy('shop_id');  // تحميل كسول للمتاجر: N استعلام
    $topProducts = $orders->flatMap->items; // تحميل كسول للعناصر: N استعلام
    $customers = $orders->pluck('customer'); // تحميل كسول للعميل: N استعلام
}

// ✅ بعد: لوحة المعلومات تحمل في 500 مللي ثانية
// المتحكم: 4 استعلامات فقط
public function dashboard(Tenant $tenant)
{
    $orders = $tenant->orders()
        ->with(['shop:id,name', 'items.product:id,name,price', 'customer:id,name'])
        ->whereMonth('created_at', now()->month)
        ->select(['id', 'shop_id', 'customer_id', 'total', 'created_at'])
        ->get();

    $total = $orders->sum('total');
    $byShop = $orders->groupBy('shop.name');
    $topProducts = $orders->flatMap->items->take(10);
    $customers = $orders->pluck('customer.name');
}

الناتج:

TEXT
// تم التنفيذ بنجاح

(2) ▶ مثال: استخدام chunks و cursors مع مجموعات بيانات كبيرة

PHP
// معالجة 5 ملايين طلب بدون تجاوز الذاكرة
// الخيار 1: chunk (يعالج 500 في كل مرة)
Order::where('tenant_id', $tenant->id)
    ->chunk(500, function ($orders) {
        foreach ($orders as $order) {
            $this->calculateOrderMetrics($order);
        }
    });

// الخيار 2: chunkById (أكثر استقرارًا مع الكتابة المتزامنة)
Order::where('tenant_id', $tenant->id)
    ->select(['id', 'total', 'status'])
    ->chunkById(500, function ($orders) {
        $metrics = $orders->groupBy('status')->map->sum('total');
        Cache::put("metrics:{$tenant->id}", $metrics, 3600);
    });

// الخيار 3: cursor (تدفق، أقل ذاكرة)
$total = 0;
foreach (Order::where('tenant_id', $tenant->id)->cursor() as $order) {
    $total += $order->total;
}

// الخيار 4: lazy (طرق المجموعة متاحة)
$highValue = Order::where('tenant_id', $tenant->id)
    ->lazy(500)
    ->filter(fn ($o) => $o->total > 1000)
    ->count();

الناتج:

TEXT
// تم التنفيذ بنجاح

4. استراتيجية التخزين المؤقت

(1) هرمية التخزين المؤقت في Laravel

100%
flowchart TD
    A[طلب] --> B{تخزين مؤقت للمسارات؟}
    B -->|إصابة| C[مسارات مخزنة مؤقتًا]
    B -->|فقد| D[تحليل المسارات]
    D --> E{تخزين مؤقت للتهيئة؟}
    C --> E
    E -->|إصابة| F[تهيئة مخزنة مؤقتًا]
    E -->|فقد| G[تحميل .env + ملفات التهيئة]
    G --> H{تخزين مؤقت للبيانات؟}
    F --> H
    H -->|إصابة| I[إرجاع البيانات المخزنة مؤقتًا]
    H -->|فقد| J[استعلام DB + تخزين النتيجة مؤقتًا]
    I --> K[استجابة]
    J --> K

(2) أنواع التخزين المؤقت وحالات الاستخدام

نوع التخزين المؤقت الأمر/الطريقة حالة الاستخدام الاستمرارية
تخزين مؤقت للتهيئة config:cache تهيئة بيئة الإنتاج طويل الأمد (حتى المسح)
تخزين مؤقت للمسارات route:cache مسارات كثيرة، عمليات إغلاق قليلة طويل الأمد (حتى المسح)
تخزين مؤقت للعروض view:cache تجميع عروض Blade طويل الأمد (حتى المسح)
تخزين مؤقت للبيانات Cache::put() نتائج الاستعلام/بيانات التقارير انتهاء صلاحية TTL
تخزين مؤقت للاستعلام remember() استعلامات متكررة انتهاء صلاحية TTL
OPcache php.ini كود PHP البايتكود صالح حتى إعادة التشغيل

(1) ▶ مثال: استراتيجية التخزين المؤقت بـ Redis في ShopMetrics

PHP
// app/Services/DashboardService.php
class DashboardService
{
    public function getOverview(Tenant $tenant): array
    {
        return Cache::remember(
            "dashboard:{$tenant->id}:overview",
            now()->addMinutes(15),
            fn () => $this->calculateOverview($tenant)
        );
    }

    public function getTopProducts(Tenant $tenant, string $period = '7d'): Collection
    {
        return Cache::remember(
            "dashboard:{$tenant->id}:top-products:{$period}",
            now()->addHours(1),
            fn () => $this->calculateTopProducts($tenant, $period)
        );
    }

    public function getRevenueChart(Tenant $tenant, string $granularity = 'daily'): array
    {
        return Cache::remember(
            "dashboard:{$tenant->id}:revenue:{$granularity}",
            now()->addMinutes(30),
            fn () => $this->calculateRevenueChart($tenant, $granularity)
        );
    }

    public function invalidateTenantCache(Tenant $tenant): void
    {
        $prefix = "dashboard:{$tenant->id}";
        // KEYS في Redis بطيء مع البيانات الكبيرة؛ استخدم العلامات أو المسح بالبادئة
        Cache::getStore()->getConnection()->del(
            ...Cache::getStore()->getConnection()->keys("{$prefix}:*")
        );
    }
}

// المتحكم
class DashboardController extends Controller
{
    public function __construct(
        private DashboardService $dashboard
    ) {}

    public function index(Tenant $tenant)
    {
        return response()->json([
            'overview' => $this->dashboard->getOverview($tenant),
            'top_products' => $this->dashboard->getTopProducts($tenant),
            'revenue_chart' => $this->dashboard->getRevenueChart($tenant),
        ]);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

(3) علامات التخزين المؤقت وانتهاء الصلاحية الدفعي

PHP
// تخزين مؤقت مع علامات (مشغلات Redis/قاعدة البيانات فقط)
Cache::tags(['dashboard', "tenant:{$tenant->id}"])->put(
    'overview',
    $data,
    3600
);

// إلغاء جميع تخزين لوحة المعلومات المؤقت لمستأجر
Cache::tags(["tenant:{$tenant->id}"])->flush();

// أو استخدام اصطلاحات مفاتيح التخزين المؤقت مع المسح بالبادئة
Cache::forget("dashboard:{$tenant->id}:overview");
Cache::forget("dashboard:{$tenant->id}:top-products:7d");
Cache::forget("dashboard:{$tenant->id}:revenue:daily");

5. تحسين قاعدة البيانات

(1) استراتيجية الفهرسة

PHP
// ترحيل: إضافة فهارس لاستعلامات ShopMetrics
Schema::table('orders', function (Blueprint $table) {
    // فهرس عمود واحد
    $table->index('tenant_id', 'idx_orders_tenant');
    $table->index('created_at', 'idx_orders_date');
    $table->index('status', 'idx_orders_status');

    // فهرس مركب (الأكثر فائدة للاستعلامات متعددة الشروط)
    $table->index(['tenant_id', 'created_at'], 'idx_orders_tenant_date');
    $table->index(['tenant_id', 'status', 'created_at'], 'idx_orders_tenant_status_date');

    // فهرس فريد
    $table->unique(['tenant_id', 'external_id'], 'uniq_orders_tenant_external');
});

(2) مقارنة أنواع الفهارس

نوع الفهرس الصياغة حالات الاستخدام الوصف
Index $table->index(col) WHERE/ORDER BY B-tree قياسي
Unique $table->unique(col) قيد فريد B-tree + إزالة التكرار
Composite $table->index([c1,c2]) استعلام متعدد الشروط يتبع البادئة اليسرى
FullText $table->fullText(col) بحث نصي MySQL 8.0+
Spatial $table->spatialIndex(col) استعلام جغرافي POINT/LINESTRING

(3) EXPLAIN: تحليل الاستعلامات

SQL
-- تحليل استعلام بطيء
EXPLAIN SELECT * FROM orders 
WHERE tenant_id = 1 AND status = 'completed' 
ORDER BY created_at DESC LIMIT 20;

-- ابحث عن:
-- type: ALL (مسح كامل) → يحتاج فهرس
-- key: NULL (لم يُستخدم فهرس) → يحتاج فهرس
-- rows: رقم كبير → يحتاج تحسين
-- Extra: Using filesort → يحتاج فهرس مركب

(1) ▶ مثال: تشخيص وتحسين الاستعلامات البطيئة في ShopMetrics

SQL
-- ❌ قبل: مسح جدول كامل على 5 ملايين صف (3000مللي ثانية)
EXPLAIN SELECT * FROM orders WHERE tenant_id = 42;
-- type: ALL, rows: 5,000,000, key: NULL

-- إضافة فهرس
ALTER TABLE orders ADD INDEX idx_orders_tenant (tenant_id);

-- ✅ بعد: مسح فهرس (5مللي ثانية)
EXPLAIN SELECT * FROM orders WHERE tenant_id = 42;
-- type: ref, rows: 50,000, key: idx_orders_tenant

-- ❌ لا يزال بطيئًا: ترتيب بدون فهرس (500مللي ثانية)
EXPLAIN SELECT * FROM orders 
WHERE tenant_id = 42 ORDER BY created_at DESC LIMIT 20;
-- Extra: Using filesort

-- ✅ فهرس مركب (3مللي ثانية)
ALTER TABLE orders ADD INDEX idx_orders_tenant_date (tenant_id, created_at DESC);

الناتج:

TEXT
CREATE TABLE

(4) فصل القراءة والكتابة

PHP
// config/database.php
'mysql' => [
    'read' => [
        'host' => env('DB_READ_HOST', 'read-replica.shopmetrics.internal'),
    ],
    'write' => [
        'host' => env('DB_WRITE_HOST', 'primary.shopmetrics.internal'),
    ],
    'sticky' => true,  // القراءة من مضيف الكتابة بعد الكتابة في نفس الطلب
    'driver' => 'mysql',
    'database' => env('DB_DATABASE', 'shopmetrics'),
    // ...
],

6. تحسين OPcache و PHP-FPM

(1) تهيئة OPcache

INI
; php.ini أو php.d/opcache.ini
opcache.enable=1
opcache.memory_consumption=256        ; 256 ميغابايت لتخزين البايتكود المؤقت
opcache.interned_strings_buffer=32    ; 32 ميغابايت للسلاسل
opcache.max_accelerated_files=40000   ; Laravel لديه ~12000 ملف
opcache.validate_timestamps=0         ; إيقاف في الإنتاج (إعادة تعيين يدوية)
opcache.save_comments=1               ; مطلوب لتعليقات Laravel التوضيحية
opcache.jit=1255                      ; PHP 8.2 JIT: وضع التتبع
opcache.jit_buffer_size=128M          ; 128 ميغابايت مخزن JIT المؤقت

(2) ضبط PHP-FPM

INI
; php-fpm.d/www.conf
pm = dynamic
pm.max_children = 50          ; = (الذاكرة - نظام التشغيل) / المتوسط لكل عملية
pm.start_servers = 10         ; = min_spare + (max - min) / 2
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000        ; منع تسرب الذاكرة

(3) إعادة تعيين OPcache

BASH
# بعد النشر، أعد تعيين OPcache
php artisan optimize:clear
# أو عبر مسار
Route::get('/opcache-clear', function () {
    if (app()->environment('production')) {
        opcache_reset();
    }
    return 'OPcache cleared';
})->middleware('auth:api');

(1) ▶ مثال: نص برمجي بنقرة واحدة للتخزين المؤقت للإنتاج في ShopMetrics

BASH
#!/bin/bash
# deploy-optimize.sh — تشغيل بعد كل نشر

set -e

echo "→ مسح جميع التخزين المؤقت..."
php artisan optimize:clear

echo "→ تخزين التهيئة مؤقتًا..."
php artisan config:cache

echo "→ تخزين المسارات مؤقتًا..."
php artisan route:cache

echo "→ تخزين العروض مؤقتًا..."
php artisan view:cache

echo "→ تشغيل الترحيلات..."
php artisan migrate --force

echo "→ إعادة تشغيل عمال الطابور..."
php artisan queue:restart

echo "→ مسح OPcache..."
php artisan optimize

echo "✓ اكتمل التحسين!"

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

7. اختبار الحمل

(1) مقارنة الأدوات

الأدوات اللغة المزايا حالات الاستخدام
Apache Bench (ab) C بسيط، بدون تبعيات اختبار إجهاد سريع لنقطة نهاية API واحدة
k6 JS برمجة مرنة، متوافق مع CI سيناريوهات معقدة + CI
wrk C تزامن عالي، موارد منخفضة اختبار التزامن القصوي
JMeter Java واجهة رسومية وإضافات غنية اختبار معقد على مستوى المؤسسة

(2) نص اختبار الحمل بـ k6

JAVASCRIPT
// k6-scripts/dashboard-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 20 },   // تصاعد إلى 20 مستخدم
    { duration: '2m', target: 20 },    // البقاء عند 20
    { duration: '30s', target: 50 },   // تصاعد إلى 50
    { duration: '2m', target: 50 },    // البقاء عند 50
    { duration: '30s', target: 0 },    // انخفاض
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],  // 95% من الطلبات < 500مللي ثانية
    http_req_failed: ['rate<0.01'],    // معدل الخطأ < 1%
  },
};

const BASE_URL = 'https://api.shopmetrics.io';
const TOKEN = __ENV.API_TOKEN;

export default function () {
  const params = {
    headers: { Authorization: `Bearer ${TOKEN}` },
  };

  // اختبار API لوحة المعلومات
  const res = http.get(`${BASE_URL}/api/v1/dashboard`, params);

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'has overview data': (r) => JSON.parse(r.body).overview !== null,
  });

  sleep(1);
}

(3) أوامر اختبار الحمل السريعة

BASH
# Apache Bench: 100 مستخدم متزامن، 1000 طلب إجمالي
ab -n 1000 -c 100 -H "Authorization: Bearer $TOKEN" \
   https://api.shopmetrics.io/api/v1/dashboard

# wrk: 50 اتصال لمدة 30 ثانية
wrk -t4 -c50 -d30s -H "Authorization: Bearer $TOKEN" \
    https://api.shopmetrics.io/api/v1/dashboard

# k6: تشغيل اختبار الحمل مع فحوصات الحدود
k6 run --env API_TOKEN=$TOKEN k6-scripts/dashboard-load.js

8. مثال شامل: مقارنة قبل وبعد تحسين ShopMetrics

PHP
// ============================================
// شامل: تحسين API لوحة المعلومات
// قبل مقابل بعد مع تطبيق جميع التقنيات
// ============================================

// ❌ قبل: لوحة معلومات بطيئة (5 ثوانٍ، 200+ استعلام)
class DashboardController extends Controller
{
    public function index(Request $request)
    {
        $tenant = $request->user()->tenant;

        $orders = $tenant->orders()
            ->whereMonth('created_at', now()->month)
            ->get();  // يحمل جميع الأعمدة، بدون eager loading

        return response()->json([
            'total_revenue' => $orders->sum('total'),
            'order_count' => $orders->count(),
            'top_shops' => $orders->groupBy('shop_id')  // N+1 على shop
                ->map->sum('total')
                ->sortDesc()
                ->take(5),
            'recent_orders' => $orders->sortByDesc('created_at')
                ->take(10)
                ->map(fn ($o) => [
                    'id' => $o->id,
                    'customer' => $o->customer->name,  // N+1 على customer
                    'total' => $o->total,
                ]),
        ]);
    }
}

// ✅ بعد: لوحة معلومات سريعة (100مللي ثانية، 3 استعلامات، تخزين مؤقت)
class DashboardController extends Controller
{
    public function index(Request $request)
    {
        $tenant = $request->user()->tenant;

        $data = Cache::remember(
            "dashboard:{$tenant->id}:overview",
            now()->addMinutes(15),
            fn () => $this->buildDashboardData($tenant)
        );

        return response()->json($data);
    }

    private function buildDashboardData(Tenant $tenant): array
    {
        // استعلام محسن واحد مع تجميعات
        $stats = DB::table('orders')
            ->where('tenant_id', $tenant->id)
            ->whereMonth('created_at', now()->month)
            ->select([
                DB::raw('SUM(total) as total_revenue'),
                DB::raw('COUNT(*) as order_count'),
            ])
            ->first();

        // أفضل المتاجر: استعلام 1
        $topShops = DB::table('orders')
            ->join('shops', 'orders.shop_id', '=', 'shops.id')
            ->where('orders.tenant_id', $tenant->id)
            ->whereMonth('orders.created_at', now()->month)
            ->groupBy('shops.id', 'shops.name')
            ->orderByDesc('revenue')
            ->limit(5)
            ->select('shops.name', DB::raw('SUM(orders.total) as revenue'))
            ->get();

        // الطلبات الأخيرة مع eager loading: استعلام 1
        $recentOrders = Order::with('customer:id,name')
            ->where('tenant_id', $tenant->id)
            ->select(['id', 'customer_id', 'total', 'created_at'])
            ->latest()
            ->limit(10)
            ->get()
            ->map(fn ($o) => [
                'id' => $o->id,
                'customer' => $o->customer->name,
                'total' => $o->total,
            ]);

        return [
            'total_revenue' => (float) $stats->total_revenue,
            'order_count' => (int) $stats->order_count,
            'top_shops' => $topShops,
            'recent_orders' => $recentOrders,
        ];
    }
}
100%
flowchart LR
    subgraph "قبل: 5000مللي ثانية"
        A1[200+ استعلام SQL] --> A2[بدون تخزين مؤقت]
        A2 --> A3[بدون فهارس]
        A3 --> A4[تحميل N+1]
    end
    subgraph "بعد: 100مللي ثانية"
        B1[3 استعلامات SQL] --> B2[تخزين مؤقت Redis 15دقيقة]
        B2 --> B3[فهرس مركب]
        B3 --> B4[Eager Loading]
    end
    A4 -->|تحسين| B1

❓ أسئلة شائعة

س متى يجب استخدام التخزين المؤقت؟
ج خزّن مؤقتًا فقط البيانات التي تُقرأ بشكل متكرر لكن تُكتب نادرًا. التقارير ولوحات المعلومات والبيانات الإحصائية مناسبة للتخزين المؤقت (TTL 15-60 دقيقة)؛ البيانات الفورية لا يجب تخزينها مؤقتًا. يجب أن يتضمن التخزين المؤقت سياسة انتهاء صلاحية — يجب مسح البيانات استباقيًا عند تغييرها.
س كيف أختار بين eager loading و lazy loading؟
ج استخدم eager loading بشكل افتراضي (with())، واستخدم lazy loading فقط عندما تكون متأكدًا أنك لن تصل إلى البيانات المرتبطة. استخدم Laravel Debugbar لمراقبة عدد الاستعلامات — مشاكل N+1 ستكون واضحة فورًا.
س أيهما أفضل، chunk أم cursor؟
ج cursor() يستخدم أقل ذاكرة لكن يمكنه فقط المعالجة سطر بسطر؛ لا يمكنه تخطي الأسطر ولا يدعم أمان الكتابة المتزامنة. chunkById() أكثر استقرارًا ومناسب للمعالجة الدفعية في بيئات الإنتاج. يُستخدم chunkById() في معظم السيناريوهات.
س ماذا يجب أن أفعل إذا كان الاستعلام لا يزال بطيئًا حتى بعد إضافة فهرس؟
ج استخدم EXPLAIN للتحقق مما إذا كان الفهرس يُستخدم فعلاً (عمود key)؛ تحقق مما إذا كان ترتيب أعمدة الفهرس يتبع مبدأ البادئة اليسرى؛ تحقق مما إذا كان الفرز يحدث على مجموعة نتائج كبيرة (filesort)؛ فكر في استخدام فهرس تغطية لتقليل مسح الجدول.
س هل يجب تفعيل OPcache في بيئة التطوير؟
ج لا تفعله في بيئة التطوير، أو اضبطه على opcache.validate_timestamps=1. بخلاف ذلك، لن تسري تغييرات الكود، مما قد يسبب ارتباكًا أثناء التصحيح. تأكد من تفعيله في بيئة الإنتاج واضبطه على validate_timestamps=0.
س كم مستخدم متزامن يجب استخدامه لاختبار الحمل؟
ج ابدأ بالاختبار بـ 2 إلى 5 أضعاف الذروة المتوقعة. على سبيل المثال، إذا كان لديك 1,000 مستخدم نشط يوميًا وذروة حوالي 100 مستخدم متزامن، اختبر بـ 200-500 مستخدم متزامن. ركز على قيم زمن الانتقال p95 و p99، وليس المتوسط.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): حدد أي استعلام N+1 في ShopMetrics، وأصلحه باستخدام with()، وقارن عدد الاستعلامات قبل وبعد التحسين (مسجلة باستخدام DB::enableQueryLog()).

  2. تمرين متقدم (⭐⭐): أضف تخزينًا مؤقتًا بـ Redis (TTL 15 دقيقة) إلى API لوحة معلومات ShopMetrics ونفذ مسحًا تلقائيًا للتخزين المؤقت عند تغير البيانات. اكتب نص k6 لاختبار معدل إصابة التخزين المؤقت.

  3. تحدي (⭐⭐⭐): حسّن بالكامل API قائمة طلبات ShopMetrics — أضف فهرسًا مركبًا (tenant_id + status + created_at)، واستبدل عمليات Collection باستعلامات تجميع DB::table، ونفذ التخزين المؤقت، واستخدم EXPLAIN للتحقق من الأداء، بهدف p95 < 200 مللي ثانية.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%