در دیتابیسهای رابطه ای جهت اجرای چندین دستور اعم از ایجاد، بهروزرسانی و حذف، از تراکنش یا Transaction ها استفاده میشود. هر تراکنش باید قوانین ACID را رعایت کند، در غیر اینصورت قابلاجرا نمیباشد. این قوانین با استفاده از نوعی سیستم قفل گذاری پیاده سازی شدهاند.
قوانین ACID شامل موارد ذیل است که باید در تراکنش دیتابیسهای رابطه ای رعایت گردد:
• یکپارچگی (Atomicity) : این خاصیت که به خاصیت همه یا هیچ معروف است، بیانگر این است که یک تراکنش یا باید به طور کامل اجرا شود یا اصلا اجرا نشود. برای مثال اگر در یک تراکنش سه عملیات مختلف انجام گیرد، یا هر سه این عملیات باید تا پایان تراکنش به صورت کامل انجام شوند یا هیچ کدام از آنها انجام نشوند. دستورات باید به ترتیب و سریالی انجام گیرند و در صورتی که مشکلی در سیستم به وجود آمد (مثلا برق قطع شد)، سیستم باید بعد از اتصال دوباره، عملیات ها را Rollback نماید، به این ترتیب که گویا هیچ کدام از دستورات صورت نگرفته است.
• سازگاری (Consistency): برای درک درست مفهوم سازگاری، مثال تراکنش های بانک را در نظر بگیرید. در یک سیستم حسابداری بانک، مجموعه پول های انتقالی بین بانک باید ثابت باشد. در نظر بگیرید که مثلا اگر ۵۰ هزار تومان از حساب کاربری A به حساب کاربری B منتقل شد، مجموعه پول های موجود در بانک تغییر نخواهد کرد. این همان مفهوم سازگاری است. یعنی یک تراکنش، دیتابیس را از یک حالت سازگار به یک حالت سازگار دیگر انتقال میدهد.
• انزوا (Isolation): فرض کنید در یک دیتابیس، چندین تراکنش با یکدیگر در حال اجرا هستند. هر تراکنش نباید از اجرای دیگر تراکنش ها مطلع شود. یعنی این تراکنش ها باید طوری اجرا شوند که انگار، فقط همین یک تراکنش در حال اجرا در کل دیتابیس است.
• ماندگاری (Durability) : به این معناست که یک تراکنش بعد از اتمام، باید در حافظه باقی بماند. یعنی اگر یک تراکنش با موفقیت به اتمام رسید، نتایج کار، با قطع برق یا اتفاق های دیگر از بین نرود.
تراکنش در Redis
Redis از قوانین ACID فقط دو قانون یعنی انزوا و یکپارچگی را رعایت میکند و قوانین دیگر را بر عهده برنامهنویس میگذارد.
Redis با استفاده از دستورات زیر که در دو بخش است ، تراکنش ها را مدیریت و اجرا میکند.
1. MULTI, EXEC, DISCARD – دستوراتی جهت ورود به تراکنش و خروج از آن.
2. WATCH, UNWATCH – مشاهده یک کلید خاص جهت تغییرات آن توسط client های دیگر.
نحوه استفاده از تراکنش در Redis
تراکنش با دستور MULTI شروع میشود، این دستور همیشه پاسخ "OK" را برگشت میدهد. سپس دستورات وارد شده بعد از آن صف بندی میشوند و با دستور EXEC دستورات موجود در صف به ترتیب اجرا میشوند و تراکنش پایان مییابد
در صورت استفاده از DISCARD به جای EXEC کل دستورات موجود در صف حذف شده و تراکنش پایان مییابد.
همانطور که در شکل زیر قابلمشاهده است پاسخ EXEC یک آرایه است و هر کدام از مقادیر آرایه نتیجه اجرای هر کدام از دستورات است و چون دستورات به صورت ترتیبی انجام میگیرد ترتیب مقادیر آرایه نیز با ترتیب اجرای دستورات هماهنگ است.
نمونهای از کد تراکنش در ردیس را مشاهده کنید :
$ret = $redtis->multi( )
->set('key1', ‘val1')
->get('keyl1' )
->set('key2', ‘val2')
->get('key2' )
->exec( );
/*
$ret == Array(
0 => TRUE,
1 => 'val1',
2 => TRUE,
3 => ‘val2'
);
*/
در صورتی که یک client با سرور ردیس ارتباط داشته باشد استفاده از دستورات بالا صحیح میباشد ولی اگر چند client با سرور در ارتباط باشند احتمال خرابی داده موجود در سرور وجود دارد. جهت جلوگیری از خرابی داده میباید از WATCH , UNWATCH استفاده کرد.
دستور WATCH جهت مشاهده تغییرات کلید خاص توسط client های دیگر استفاده میشود و دستور UNWATCH برای پاک کردن مشاهدات client موجود استفاده میگردد.
$redis->watch( 'x');
$ret = $redis->multi( )
->incr('x')
->exec();
/*
$ret = FALSE if x has been modifted between
the call to WATCH and the call to EXEC.
*/
در شکل بالا اگر کلید "x" توسط client دیگری watch شده بود و در حال استفاده بود، نتیجه False برمی گرداند و تراکنش مورد نظر می بایست در زمان دیگری اجرا گردد تا اگر client مورد نظر کلید "x" را رها (UNWATCH) کرده بود تراکنش اجرا گردد.
خطاها و مدیریت آنها
در طول یک تراکنش امکان رخداد دو نوع خطا وجود دارد که در ادامه به آنها خواهیم پرداخت.
• ممکن است یک دستور در صف قرار نگیرد ، بنابراین ممکن است قبل از فراخوانی دستور EXEC خطایی رخ دهد. به عنوان مثال دستور وارد شده اشتباه باشد (تعداد آرگومان اشتباه ، نام فرمان اشتباه و ...) یا ممکن است شرایط بحرانی مانند پر شدن حافظه وجود داشته باشد (اگر تنظیم سرور برای maxmemory تنظیم شده باشد). که در این صورت داده مورد نظر خراب نشده و تا اینجا Consistency رعایت میشود.
• یک خطا ممکن است پس از فراخوانی EXEC اتفاق بیافتد. به عنوان مثال از آنجا که ما عملیاتی را بر روی یک کلید با مقدار اشتباه انجام دادیم (مانند فراخوانی یک عملیات لیست در برابر یک مقدار رشته). در شکل زیر این خطا را ملاحظه می فرمایید.
> MULTI
OK
> set x "abc"
QUEUED
> lpop a
QUEUED
> EXEC
+0K
-ERR Operation against a key holding the wrong kind of value
دستور EXEC پاسخ دو رشته عناصر را بر می گرداند که یکی "OK" و پاسخ دیگری ERR است که وظیفه ی برنامهنویس یافتن راه حلی منطقی برای کنترل خطا و انتقال آن به کاربر است.
توجه به این نکته مهم است که حتی هنگامی که یک فرمان دچار مشکل میشود، تمام دستورات دیگر در صف پردازش قرار میگیرند و Redis مانع پردازش دستورات نمیشود. که این موجب عدم رعایت Consistency یا سازگاری در اطلاعات میگردد.
صرف نظر از صف دستورات
از دستور DISCARD به منظور متوقف کردن تراکنش استفاده میشود. در این حالت، هیچ فرمانی اجرا نمیشود و وضعیت اتصال به ردیس به حالت عادی بازگردانده می شود :
> set x 1
OK
> multi
OK
> incr x
QUEUED
> discard
OK
> get x
"1"
عدم پشتیبانی Redis از Rollback
در صورتی که تجربه کار با دیتابیسهای رابطه ای را دارید، این که دستورات Redis میتوانند در طول یک تراکنش دچار مشکل شوند، اما Redis به جای Rollback کردن آنها، مابقی دستورات را اجرا میکند، ممکن است برای شما عجیب به نظر برسد.
با این وجود دلایل خوبی برای این رفتار وجود دارد:
• دستورات Redis فقط درصورتی میتوانند موجب خطا شوند که با یک ساختار و Syntax اشتباه فراخوانی شوند یا دستورات فراخوانی شده برای دیتاتایپ کلید مورد نظر تعریف شده نباشد، مطابق آنچه که در خطاهای نوع دوم گفتیم. این مشکلات در حین افزودن دستور به صف قابل تشخیص نیست چون دستور اجرا نمیشود بلکه صف بندی میشود. بلکه پس از فراخوانی دستور EXEC ،وقتی نوبت اجرای دستور مشکل دار میرسد، خطا بر می گرداند.
• Redis از نظر ساختار سادهتر و از لحاظ دسترسی سریع تر است به همین دلیل نیازی به Rollback ندارد.
لذا با توجه به این دلایل، کنترل و مدیریت این خطاها بر عهده ی برنامهنویس گذاشته شده است.
تراکنش Redis در لاراول
همانطور که در بخش آموزش Redis با استفاده از یک پروژه ی کاربردی گفتیم جهت ارتباط راحت تر لاراول با ردیس از پکیج Phpredis استفاده میکنیم. برای بررسی موراد بیان شده یک مثال را بررسی میکنیم.
فرض کنید میخواهیم مبلغ 20 دلار از حساب کاربر1 به حساب کاربر2 انتقال دهیم. عملیات برداشت مبلغ از حساب کاربر1 و عملیات پرداخت آن به حساب کاربر2 میباید طی یک تراکنش انجام گیرد چون در غیر اینصورت ممکن است موجب خرابی اطلاعات گردد.
[
"id" => "1"
"name" => "foo"
"username" => "foo@foo.com"
"balance" => "50"
]
[
"id" => "2"
"name" => "bar"
"username" => "bar@bar.com"
"balance" => "80"
]
اطلاعات حساب هر دو نفر را مشاهده میکنید. حال ما به سراغ اجرای تراکنش انتقال مبلغ مطابق کد زیر میرویم که در نهایت پس از اجرای تراکنش میبایدی موجودی حساب کاربر 1 برابر 30 دلار و موجودی حساب کاربر 2 برابر 100 دلار شود.
public function account()
{
$sender_key = 'users:1'; // Sender key on redis
$receiver_key = 'users:2'; // Receiver key on redis
$money = 20;
$end = Carbon::now()->addSecond(5);
while (Carbon::now() < $end) {
try{
Redis::watch([$sender_key, $receiver_key]);
$sender_balance = (int)Redis::hget($sender_key,'balance');
if ($sender_balance < $money) {
Redis::unwatch();
return null;
}
Redis::multi();
Redis::hincrby($sender_key, "balance", -1*$money);
Redis::hincrby($receiver_key, "balance", $money);
Redis::exec();
return true;
}catch (RedisException $exp) {
continue;
}
}
return false;
}
مطابق کد بالا، ابتدا کلید کاربر پرداخت کننده، کلید کاربر گیرنده، میزان مبلغ ارسالی و میزان زمان مورد نیاز جهت تلاش برای انتقال مبلغ که در اینجا 5 ثانیه است را در نظر میگیریم. حال به مدت 5 ثانیه client تلاش میکند کلید دو کاربر را watch کند. در صورتی که در این 5 ثانیه client نتواند کلید کاربران را watch کند بدین معنی است که توسط client های دیگر watch شده است و نتیجه false بر می گرداند. در صورتی که client بتواند کلید دو کاربر را watch کند ولی موجودی حساب کاربر1 (انتقال دهنده) کافی نباشد کلیدهای watch شده، unwatch میشوند و نتیجه Null برگشت داده میشود تا بقیه client ها بتواند با آن کلیدها کار کنند. اگر موجودی کاربر1 برای انتقال مبلغ کافی باشد ابتدا با استفاده از دستور multiشروع به ایجاد صف دستورات میکنیم همانطور که مشاهده میکنید ابتدا دستور برداشت مبلغ از حساب کاربر 1 و سپس افزودن مبلغ به حساب کاربر شماره 2 صف بندی میشود و سپس با دستور exec شروع به اجرای دستورات صف بندی شده میکند و در نهایت مقدار true را بر می گرداند.
همانطور که انتظار میرفت موجودی حساب کاربر1 به 30 دلار و موجودی حساب کاربر به 100 دلار تغییر یافت.
منابع
· https://redis.io/topics/transactions
· https://github.com/phpredis/phpredis
. Redis in Action. Josiah Carlson. June 2013 ISBN 9781617290855