بررسی امکان استفاده از ترنزکشن بجای قفل

در سلسله بحثهایی که در این تاپیک داشتیم: تاپیک
با مسئلهء تداخل کوئری های چند درخواست و مشکلی که ایجاد میکردن آشنا شدیم.
نمونه تست هم که براتون گذاشتم در این پست: پست

خب راه حل مسئله چیست؟
در اونجا گفتیم که یک راه عمومی حل این مسئله، استفاده از قفل است.
ولی یک چیز دیگری که بعضی ها قبلا هم گفته بودن و بنده هم حدس میزدم اینه که استفاده از ترنزکشن این مشکلات رو برطرف میکنه. اما بنده خودم چون در زمینهء استفاده از ترنزکشن ها مطالعه و کار عملی کافی نداشتم تاحالا و دربارهء ارتباطش با مسئلهء تداخل کوئری ها تحقیق نکرده بودم، بررسی و تایید این مطلب رو بعد موکول کردم.

خب حالا بنده یک تستی رو همین الان انجام دادم.

این کد تست همون کد تستی هست که برای نشان دادن مسئلهء تداخل کوئری ها گذاشته بودم؛ فقط نوع جدول رو از MyISAM به InnoDB تغییر دادم که بتونیم از ترنزکشن استفاده کنیم (MyISAM از ترنزکشن پشتیبانی نمیکنه)، و دستورات مربوط به ترنزکشن رو هم قبل و بعد از کوئری های read و write اضافه کردم.

نمونه کد تست ما به این شکل است:

<?php

error_reporting(E_ALL);
ini_set('display_errors', '1');

header('Content-Type: text/html; charset=utf-8');
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0");
header('Pragma: private');
header("Pragma: no-cache");

mysql_connect('localhost', 'root', '');

mysql_select_db('test');

mysql_query('START TRANSACTION');

$res=mysql_query('select `counter` from `test`');

$row=mysql_fetch_array($res);

$counter=$row[0];

echo "Read: $counter";

$counter++;

sleep(10);

mysql_query("update `test` set `counter`=$counter");

mysql_query('COMMIT');

echo "<br>Write: $counter";

?>

جالب اینکه تستی که بنده کردم نشون میده که استفاده از ترنزکشن تاثیری روی مسئلهء تداخل کوئری ها نداره و این مشکل رو حل نمیکنه. چون همچنان نتایج مشابهی با نتایج آزمایش اولیه بدست میاد.

حالا از بقیهء حضرات و افراد مطلع و متخصص در این زمینه که احتمالا بیشتر از بنده تجربهء کار با ترنزکشن ها رو هم دارن، دعوت به همکاری و کمک در این بحث میکنم تا بالاخره به نتیجه برسیم و مطمئن بشیم که جواب این سوال چیه. منظورم اینه که آیا ترنزکشن ها میتونن جلوی مشکل تداخل کوئری های چند درخواست رو بگیرن یا نه، و اگر بله، چطور دقیقا؟

دانلود برنامهء تست: دانلود

——————-

خب سرانجام مکشوف کردید که در یک ترنزکشن هم میشه بدون ایجاد صریح قفل، از تداخل جلوگیری کرد! حداقل برای مثال و سناریویی که تاحالا مطرح کردیم که میشه.

پس چرا تست قبلی ما با ترنزکشن جواب نداد؟
خب بخاطر اینکه ما بصورت صریح به MySQL نگفته بودیم که عملیاتی داریم که کوئری های دیگر نباید در بین اون عمل کنن.
MySQL نمیاد بصورت پیشفرض روی ترنزکشن ها محدودیت هایی بیش از آنچه که از نظر فنی و طرز کار داخلی خودش نیاز هست بذاره. بنابراین کوئری های مختلف از منابع مختلف حتی باوجود استفاده از ترنزکشن میتونن همزمان و لابلای همدیگر اجرا بشن و تداخل در منطق برنامه بوجود بیاد.
MySQL نمیاد در هر زمان فقط کوئری های یک ترنزکشن و یک درخواست رو اجرا کنه، چون اینطوری از نظر پرفورمنس بهینه نیست. خب طبیعی هم است که این انتخاب رو داشته باشه، چون تداخل که هرجایی مطرح نیست! خیلی از کوئری ها و عملیات میتونن بصورت موازی و لابلای همدیگر انجام بشن، و به این شکل از ظرفیت سیستم استفادهء بهینه میشه (که در خیلی موارد اصلا ضروری است).

خب حالا ما چطور میتونیم به MySQL حالی کنیم که میخوایم چکار کنیم و نباید کوئری های دیگری رو در این بین اجرا کنه؟
خیلی ساده! بوسیله افزودن عبارت FOR UPDATE به انتهای کوئری های SELECT خودمون.

<?php

error_reporting(E_ALL);
ini_set('display_errors', '1');

header('Content-Type: text/html; charset=utf-8');
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0");
header('Pragma: private');
header("Pragma: no-cache");

mysql_connect('localhost', 'root', '');

mysql_select_db('test');

mysql_query('START TRANSACTION');

$res=mysql_query('select `counter` from `test` FOR UPDATE');

$row=mysql_fetch_array($res);

$counter=$row[0];

echo "Read: $counter";

$counter++;

sleep(10);

mysql_query("update `test` set `counter`=$counter");

mysql_query('COMMIT');

echo "<br>Write: $counter";

?>

توضیحات رفرنس رسمی در این مورد:

If you use FOR UPDATE with a storage engine that uses page or row locks, rows examined by the query are write-locked until the end of the current transaction.

ترجمه: «اگر شما FOR UPDATE را با موتور ذخیره سازی ای استفاده کنید که از قفل رکورد یا صفحه استفاده میکند، رکوردهایی که بوسیلهء کوئری (select) بررسی میشوند تا پایان ترنزکشن جاری قفل write میگردند.»

خب این خیلی خوبه. چون MySQL خودش بصورت خودکار رکوردهایی رو که کوئری SELECT ما با اونا کار داره قفل میکنه. چون قفل از نوع write است باعث میشه که درخواستهای دیگر چه read و چه write بطور کلی معلق بمونن و تا زمان آزاد شدن قفل منتظر بمونن.
ضمنا دقت کنید که کل جدول قفل نمیشه، بلکه فقط رکوردهایی که در نتیجهء کوئری SELECT نقش دارن قفل میشن.

البته همونطور که در پست قبلی گفتم، اینا فقط مثال و قصد روشن کردن این قضایا هست. دلیل نمیشه که بگیم در موارد دیگر یا موارد مشابه هم استفاده از این روش بهترین روش یا اصلا شدنی باشه. خیلی وقتا یک مسئله رو میشه از چندین روش حل کرد.

دانلود برنامهء تست: دانلود

حالا بجز این FOR UPDATE یک گزینهء دیگه بنام LOCK IN SHARE MODE هم داریم.
LOCK IN SHARE MODE خاصیتش اینه:

Using LOCK IN SHARE MODE sets a shared lock that permits other transactions to read the examined rows but not to update or delete them

ترجمه: «استفاده از LOCK IN SHARE MODE یک قفل اشتراکی ایجاد میکند که به ترنزکشن های دیگر اجازهء خواندن رکوردهای مورد بررسی را میدهد اما اجازهء آپدیت یا دلیت کردن آنها را نمیدهد.»

خب اینم یه چیز دیگه هست که در سناریوهای دیگری کاربرد داره.
یعنی فرضا جایی هست که اگر درخواستهای دیگر همزمان رکورد رو بخونن مشکلی پیش نمیاد، اما اگر بتونن رکورد رو همزمان آپدیت یا حذف کنن مشکل پیش میاد.
در اینطور موارد ما میتونیم از LOCK IN SHARE MODE استفاده کنیم که طبیعتا چون محدودیت کمتری ایجاد میکنه و اجازه میده کوئری های read درخواستهای دیگر هم در این اثنا اجرا بشن، از نظر بهینگی و پرفورمنس سیستم کاراتره.

2 دیدگاه در “بررسی امکان استفاده از ترنزکشن بجای قفل

  1. بازپینگ: علم خوره

  2. این مورد برای موارد عالی زیاد جای بحث نداره مثلا واسه سایت های ساده و فله ای اصلا ارزش وقت گذاشتن نداره ولی وقتی پای حساب کتاب و گردش حساب و بعضی چیزهای حساس میاد وسط قضیه جدی میشه و مطئمن باش کسی که توی پروژه های حساس کار میکنه نمیاد وقتش رو توی انجمن صرف کنه و دنبال مطالب بگرده چون به قدری میدونه که حاضر شدهدست به پروژه های حساس میزنه و بیشتر از منابع اصلی کمک میگیره تا خاطره های بچه ها.

    مطالب شما رو توی انجمن دیدم و چندین موردش رو هم خوندم و جاهایی بوده که با کسایی دیدم داری بحث میکنی که طرف مقابل اصلا کل قضیه رو نمیدونه چیه ولی تو واقعا سنگ تموم میزاشتی از این بابت خوشحال شدم و تشکر میکنم از پشتکارت

    درباره این مطالبی که گفتی ، تراکنش هیچ دخلی توی این تداخل ها نداره اصلا قضیه تراکنش با این موارد کلا فرق میکنه یعنی دو بحث جدا از همدیگه هستن و هر کدوم شاخه خودشون رو دارن .

    بحث قفل کردن رکوردها زمانی بیشتر به اوجش میرسه که با یک رکورد سروکار نداشته باشی یعنی برای محاسبه مبلغ موجودی باید چندین رکورد رو جمع بزنی تا موجودی رو بهت بده خب ؟ حالا مشکل اینجاست آیا باید برای همه رکوردهای مربوط به یه کاربر قفل گذاشت ؟ یا نه فقط برای نام کاربری قفل گذاشت ؟ قفل گذاری جدول رو کلا در نظر نگیر بحث سر قفل کردن یک کاربر یا همون مشتری هستش بهترین گزینه همینه .

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

*

شما می‌توانید از این دستورات HTML استفاده کنید: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>