Redis والتخزين المؤقت المتقدم

المعاملات والتسلسل في Redis

20 دقيقة الدرس 13 من 30

المعاملات والتسلسل في Redis

يوفر Redis ميزتين قويتين لتنفيذ أوامر متعددة بكفاءة: المعاملات للعمليات الذرية والتسلسل للتنفيذ الدفعي. فهم هذه الميزات أمر حاسم لبناء تطبيقات عالية الأداء.

معاملات Redis (MULTI/EXEC)

تسمح معاملات Redis بتنفيذ مجموعة من الأوامر بشكل ذري - جميع الأوامر تنجح أو تفشل جميعها:

معاملة أساسية:
MULTI           # بدء المعاملة
SET user:1 "John"
SET user:2 "Jane"
INCR user:count
EXEC            # تنفيذ جميع الأوامر

# يعيد مصفوفة من النتائج:
1) OK
2) OK
3) (integer) 3
الخصائص الرئيسية:
  • جميع الأوامر في قائمة انتظار ويتم تنفيذها معاً
  • تنفيذ ذري - لا يمكن لأي عميل آخر المقاطعة
  • الكل أو لا شيء: إذا كان أمر واحد يحتوي على خطأ في بناء الجملة، لا يتم تنفيذ أي منها
  • إذا فشل الأمر أثناء التشغيل (مثل نوع خاطئ)، لا تزال الأوامر الأخرى تنفذ

DISCARD - إلغاء المعاملة

إلغاء معاملة في قائمة الانتظار قبل التنفيذ:

مثال:
MULTI
SET user:1 "John"
SET user:2 "Jane"
DISCARD         # إلغاء المعاملة - لا يتم تنفيذ أي شيء

# يعيد: OK

معاملات Laravel Redis

يوفر Laravel واجهة برمجة تطبيقات نظيفة لمعاملات Redis:

استخدام طريقة transaction():
use Illuminate\Support\Facades\Redis;

// معاملة بسيطة
Redis::transaction(function ($redis) {
    $redis->set('user:1', 'John');
    $redis->set('user:2', 'Jane');
    $redis->incr('user:count');
});

// معاملة مع قيمة إرجاع
$results = Redis::transaction(function ($redis) {
    $redis->set('balance:1', 1000);
    $redis->decrby('balance:1', 100);
    return $redis->get('balance:1');
});

// $results تحتوي على مصفوفة من نتائج الأوامر
التحكم اليدوي في المعاملات:
Redis::multi();
Redis::set('key1', 'value1');
Redis::set('key2', 'value2');
Redis::incr('counter');
$results = Redis::exec();  // التنفيذ وإرجاع النتائج

// أو الإلغاء
Redis::multi();
Redis::set('key1', 'value1');
Redis::discard();  // إلغاء المعاملة

WATCH - القفل المتفائل

WATCH يراقب المفاتيح للتغييرات ويلغي المعاملة إذا تم تعديل أي مفتاح مراقب:

نمط القفل المتفائل:
// مثال Redis CLI
WATCH balance:1        # مراقبة هذا المفتاح
GET balance:1          # قراءة القيمة الحالية: 1000

# إذا عدّل عميل آخر balance:1 هنا، ستفشل المعاملة

MULTI
DECRBY balance:1 100   # خصم 100
EXEC                   # التنفيذ فقط إذا لم يتغير balance:1

# يعيد: (nil) إذا تم تعديل المفتاح المراقب
# يعيد: [OK, 900] إذا نجح
تنفيذ Laravel - تحويل بنكي:
public function transfer(int $fromAccountId, int $toAccountId, float $amount)
{
    $maxRetries = 5;
    $attempt = 0;

    while ($attempt < $maxRetries) {
        try {
            // مراقبة أرصدة الحسابين
            Redis::watch("balance:{$fromAccountId}", "balance:{$toAccountId}");

            // قراءة الأرصدة الحالية
            $fromBalance = (float) Redis::get("balance:{$fromAccountId}");
            $toBalance = (float) Redis::get("balance:{$toAccountId}");

            // التحقق
            if ($fromBalance < $amount) {
                Redis::unwatch();
                throw new \Exception('رصيد غير كافٍ');
            }

            // تنفيذ المعاملة
            $results = Redis::transaction(function ($redis) use (
                $fromAccountId, $toAccountId, $amount, $fromBalance, $toBalance
            ) {
                $redis->set("balance:{$fromAccountId}", $fromBalance - $amount);
                $redis->set("balance:{$toAccountId}", $toBalance + $amount);
                $redis->lpush('transactions', json_encode([
                    'from' => $fromAccountId,
                    'to' => $toAccountId,
                    'amount' => $amount,
                    'timestamp' => time()
                ]));
            });

            if ($results !== null) {
                return true;  // نجح
            }

            // فشلت المعاملة بسبب تغيير المفتاح المراقب - إعادة المحاولة
            $attempt++;
            usleep(50000);  // انتظر 50 ميلي ثانية قبل إعادة المحاولة

        } catch (\Exception $e) {
            Redis::unwatch();
            throw $e;
        }
    }

    throw new \Exception('فشلت المعاملة بعد الحد الأقصى من المحاولات');
}
مهم: اتصل دائماً بـ UNWATCH أو نفذ/ألغِ المعاملة لتحرير المفاتيح المراقبة. الفشل في القيام بذلك يمكن أن يسبب تسرب في الذاكرة.

التسلسل - تنفيذ الأوامر الدفعية

التسلسل يرسل أوامر متعددة إلى Redis دون انتظار الردود الفردية، مما يقلل من رحلات الشبكة:

بدون تسلسل (بطيء):
// 1000 أمر = 1000 رحلة شبكة
for ($i = 0; $i < 1000; $i++) {
    Redis::set("key:{$i}", "value:{$i}");  // انتظر الاستجابة في كل مرة
}
// الوقت: ~2 ثانية (مع 2 ميلي ثانية كمون لكل طلب)
مع التسلسل (سريع):
// تسلسل Laravel
Redis::pipeline(function ($pipe) {
    for ($i = 0; $i < 1000; $i++) {
        $pipe->set("key:{$i}", "value:{$i}");
    }
});
// الوقت: ~10 ميلي ثانية (رحلة شبكة واحدة)

// يعيد مصفوفة من جميع النتائج
تعزيز الأداء: يمكن أن يحسن التسلسل الأداء بمقدار 10-100 مرة عند تنفيذ العديد من الأوامر. إنه فعال بشكل خاص للاتصالات عالية الكمون.

التسلسل مقابل المعاملات

الاختلافات الرئيسية:
المعاملات (MULTI/EXEC):
✓ تنفيذ ذري - الكل أو لا شيء
✓ تنفذ الأوامر في عزلة
✓ يضمن الاتساق
✗ أبطأ من التسلسل
✗ إنتاجية محدودة

التسلسل:
✓ أقصى إنتاجية
✓ حد أدنى من النفقات العامة للشبكة
✓ يمكن أن يتضمن أي أوامر
✗ ليس ذرياً - يمكن تشابك الأوامر
✗ لا توجد ضمانات اتساق
✗ ممكنة الفشل الجزئي

الجمع بين التسلسل والمعاملات

استخدم كليهما للحصول على أقصى أداء مع الذرية:

معاملات متسلسلة:
// تنفيذ معاملات مستقلة متعددة في دفعة واحدة
Redis::pipeline(function ($pipe) {
    // المعاملة 1: تحديث المستخدم 1
    $pipe->multi();
    $pipe->set('user:1:name', 'John');
    $pipe->set('user:1:email', 'john@example.com');
    $pipe->exec();

    // المعاملة 2: تحديث المستخدم 2
    $pipe->multi();
    $pipe->set('user:2:name', 'Jane');
    $pipe->set('user:2:email', 'jane@example.com');
    $pipe->exec();

    // المعاملة 3: تحديث العدادات
    $pipe->multi();
    $pipe->incr('users:count');
    $pipe->incr('updates:count');
    $pipe->exec();
});

أساسيات البرمجة النصية Lua

توفر نصوص Lua عمليات ذرية حقيقية مع منطق معقد:

نص Lua بسيط:
// Redis CLI
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue

// Laravel - نص Lua لزيادة ذرية مع حد
$script = <<<LUA
local current = redis.call('GET', KEYS[1])
if not current then
    current = 0
end
if tonumber(current) < tonumber(ARGV[1]) then
    return redis.call('INCR', KEYS[1])
else
    return -1
end
LUA;

$result = Redis::eval($script, 1, 'counter', 100);
// يعيد قيمة العداد الجديدة أو -1 إذا تم الوصول إلى الحد
Laravel - محدد المعدل مع Lua:
public function checkRateLimit(string $key, int $limit, int $window): bool
{
    $script = <<<LUA
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('GET', key)

if not current then
    redis.call('SETEX', key, window, 1)
    return 1
elseif tonumber(current) < limit then
    redis.call('INCR', key)
    return tonumber(current) + 1
else
    return -1
end
LUA;

    $result = Redis::eval($script, 1, $key, $limit, $window);
    return $result !== -1;
}
مزايا Lua:
  • تنفيذ ذري مضمون
  • منطق معقد على خادم Redis (تقليل حركة مرور الشبكة)
  • أداء أفضل من القفل المتفائل المبني على WATCH
  • يتم تخزين النصوص مؤقتاً بواسطة Redis (استخدم SCRIPT LOAD للتنفيذ المتكرر)

أفضل الممارسات

متى تستخدم كل نهج:
1. استخدم المعاملات (MULTI/EXEC):
   - عمليات ذرية بسيطة
   - 2-10 أوامر ذات صلة
   - دلالات نجاح/فشل واضحة

2. استخدم WATCH + المعاملات:
   - التحديثات الشرطية بناءً على القيم الحالية
   - سيناريوهات القفل المتفائل
   - مفاتيح تنافسية منخفضة

3. استخدم التسلسل:
   - عمليات جماعية (100+ أمر)
   - عمليات للقراءة فقط
   - اتصالات عالية الكمون
   - عندما لا يكون الذرية مطلوباً

4. استخدم نصوص Lua:
   - منطق شرطي معقد
   - سيناريوهات تنافسية عالية
   - تحتاج إلى ذرية مضمونة مع منطق
   - عمليات متكررة (تخزين النص مؤقتاً)
تمرين عملي:
  1. تنفيذ سداد عربة التسوق باستخدام WATCH والمعاملات (فحص المخزون، خصم المخزون، إنشاء طلب)
  2. إنشاء وظيفة استيراد جماعي باستخدام التسلسل لإدراج 10,000 سجل
  3. بناء نص Lua لـ "pop and push" الذري بين قائمتين مع منطق شرطي
  4. تنفيذ عداد موزع مع Lua يعيد الضبط تلقائياً بعد الوصول إلى عتبة
  5. مقارنة الأداء: تشغيل 1000 أمر SET مع وبدون التسلسل، قياس فرق الوقت