PDA

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



eshpilen
دوشنبه 20 آبان 1392, 09:56 صبح
در سلسله بحثهایی که در این تاپیک داشتیم: تاپیک (http://barnamenevis.org/showthread.php?426345-%D8%A8%D9%87%D8%AA%D8%B1%DB%8C%D9%86-%D8%B1%D9%88%D8%B4-%D8%A7%D8%AA%D8%B5%D8%A7%D9%84-%D8%A8%DB%8C%D9%86-%D9%84%D9%88%DA%A9%D8%A7%D9%84-%D9%88-%D8%A7%DB%8C%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%A8%D8%AF%D9%88%D9%86-%D8%A7%D8%B3%D8%AA%D9%81%D8%A7%D8%AF%D9%87-%D8%A7%D8%B2-%D9%88%D8%A8-%D8%B3%D8%B1%D9%88%DB%8C%D8%B3-%D8%9F)
با مسئلهء تداخل کوئری های چند درخواست و مشکلی که ایجاد میکردن آشنا شدیم.
نمونه تست هم که براتون گذاشتم در این پست: پست (http://barnamenevis.org/showthread.php?426345-%D8%A8%D9%87%D8%AA%D8%B1%DB%8C%D9%86-%D8%B1%D9%88%D8%B4-%D8%A7%D8%AA%D8%B5%D8%A7%D9%84-%D8%A8%DB%8C%D9%86-%D9%84%D9%88%DA%A9%D8%A7%D9%84-%D9%88-%D8%A7%DB%8C%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%A8%D8%AF%D9%88%D9%86-%D8%A7%D8%B3%D8%AA%D9%81%D8%A7%D8%AF%D9%87-%D8%A7%D8%B2-%D9%88%D8%A8-%D8%B3%D8%B1%D9%88%DB%8C%D8%B3-%D8%9F&p=1909838&viewfull=1#post1909838)

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

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

این کد تست همون کد تستی هست که برای نشان دادن مسئلهء تداخل کوئری ها گذاشته بودم؛ فقط نوع جدول رو از 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";

?>

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

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

MMSHFE
دوشنبه 20 آبان 1392, 10:01 صبح
میگم بهتر نیست sleep رو هم با کوئری بفرستین تا وقفه توی اسکریپت نباشه بلکه توی MySQL اتفاق بیفته؟ یکم به نتیجه این تست مشکوکم چون بنظرم وقفه در PHP هست نه سمت MySQL

eshpilen
دوشنبه 20 آبان 1392, 10:06 صبح
ببین هیچ اهمیتی نداره وقفه کجا باشه.
بهرحال در یک برنامهء واقعی این وقفه ممکنه هرجایی اتفاق بیفته، منجمله در کد.
ضمنا اصلا نیازی هم نیست که این وقفه اینقدر زیاد باشه. ما زیاد گذاشتیم بخاطر اینکه تسته و میخوایم تداخل 100% رخ بده.
درسته؟
و بهرحال تداخل کوئری ها امکان اتفاق افتادن داره. حالا فرقی نمیکنه چطوری. فرقی نمیکنه تقصیر PHP بندازیم یا آپاچی یا سیستم عامل یا MySQL یا کاربر یا هکر یا برنامهء اسکنر امنیتی و غیره.
بهرحال این واقعیته که تداخل ممکنه پیش بیاد، و ما بعنوان برنامه نویس باید این رو بدونیم و جلوش رو بگیریم، چون مسئولیت هیچکس و هیچ چیز دیگری نیست و کس دیگری جز ما نمیتونه این کار رو انجام بده.
پس بهتره بیخود بحث رو گسترش ندیم و نبریم توی وادی های نامربوط تا پراکندگی و گیج شدن و ابهام بی مورد پیش نیاد.

eshpilen
دوشنبه 20 آبان 1392, 10:13 صبح
میگم بهتر نیست sleep رو هم با کوئری بفرستین تا وقفه توی اسکریپت نباشه بلکه توی MySQL اتفاق بیفته؟ یکم به نتیجه این تست مشکوکم چون بنظرم وقفه در PHP هست نه سمت MySQL
اصلا شما فرض کن وقتی sleep رو با MySQL اجرا میکنیم، این مشکل پیش نمیاد.
خب ما به جایی که مشکل وجود نداره که کاری نداریم.
ما دنبال جایی هستیم که این مشکل وجود داره و حالا میخوایم ببینیم با چه روشهایی میشه حلش کرد.
البته بحث این تاپیک فقط درمورد امکان کاربرد ترنزکشن در این موارد هست، پس لطفا بحث قفل و راه حلهای دیگر رو حتی الامکان مطرح نکنید تا بحث بیخودی شلوغ و منحرف نشه.

MMSHFE
دوشنبه 20 آبان 1392, 10:26 صبح
خوب ببینید، این کوئریها (SELECT و UPDATE) به نحوی نوشته شدن که کاملاً مستقل از هم هستن. بنابراین بدون Transaction هم درست کار میکنن. Transaction در اصل برای وقتی کاربرد داره که اگه یکی از کوئریها Fail شد، قبلیها هم Roll Back بشن. برای مثال، فرض کنید از یک جدول، یک فیلد رو در کوئری 1 میخونیم و توی کوئری دوم، اون رو در جدول دیگری درج میکنیم که اون فیلد Unique هست. مثلاً سناریوی زیر رو برای سیستم قرعه کشی درنظر بگیرین:


1- یک کاربر تصادفی انتخاب میشه (فیلد id از جدول users بصورت تصادفی) - SELECT
2- فیلد won از جدول user برای اون کاربر مقدار 1 میگیره - UPDATE
3- اولین جایزه موجود انتخاب میشه (فیلد id از جدول products بصورت ترتیبی) - SELECT
4- فیلد assigned از جدول products برای اون محصول مقدار 1 میگیره - UPDATE
5- این دو تا (فیلدهای id از جدول users و products) رو توی جدول قرعه کشی درج میکنیم (فیلدهای uid و pid در جدول lottery) درحالی که توی جدول قرعه کشی، فیلد pid بصورت Unique هست - INSERT
اینجا اگه دو نفر همزمان صفحه رو درخواست کنن و درخواستها همزمان بیاد و تا مرحله 4 با هم جلو برن و مرحله 5 با تفاوت زمانی (به هر دلیل) رخ بده، وقتی اولی عمل Insert رو انجام بده، دومی نمیتونه Insert کنه درحالی که قبلش Update روی جدول users برای اون کاربر و products برای اون محصول انجام شده. خوب اگه 5 مرحله با Transaction انجام شده باشه، درصورتی که مرحله 5 برای دومی اتفاق نیفته، 4 مرحله قبلی رو میشه Roll Back کرد.
فکر میکنم واضح گفته باشم و تفاوت این روش با سیستم قفل مشخصه. توی قفل، اصلاً دسترسی نمیده ولی اینجا دسترسی میده ولی اگه نتونه کاری که مشخص شده رو انجام بده، مراحل قبلی رو برگشت میزنه.

Veteran
دوشنبه 20 آبان 1392, 10:51 صبح
وقتی دو صفحه رو اجرا کنیم بعد در نتیجه مشاهده میکنیم که هردو یک مقدار رو خوندن و یکی بهش اضافه کردن، و در نتیجه به شمارنده فقط یک واحد اضافه شده (میتونید با phpmyadmin هم مستقیما روی جدول صحت این قضیه رو بررسی کنید)، درحالیکه باید دوتا اضافه میشد چون دو درخواست جداگانه اجرا شدن و هدف ما شمردن این درخواستها بوده.خب اینکه طبیعی هستش :متفکر:

کاربر صفحه تست1 رو باز میکنه و مقدار فیلد کانتر رو از جدول خونده و در متغیر کانتر ذخیره میشه
که 0 هستش
بعد متغیر کانتر یک واحد افزایش پیدا میکنه میشه 1
و بعد از اون 10 ثانیه متوقف میشه اجرای کد ها (جدول اپدیت نمیشه)
در بین همین متوقف شدن کد ها
متغیر کانتر هنوز 1 هست اما جدول اپدیت نشده چون متوقف شده
بعدش
کاربر صفحه تست2 روز باز میکنه و مقدار فیلد کانتر رو میخونه که هنوز 0 هست
چرا ؟ چون صفحه اول کارشو انجام نداده و متوقف شده و اپدیت نشده جدول ما یعنی فیلد کانتر به 1 اپدیت نشده!
دوباره مقدار رو میخونیم میاریم میزاریم توی متغیر کانتر که
0 هست و مقدار متغیر یک واحد افزایش پیدا میکنه میشه 1
بعدش صفحه اول(تست1) ادامه کدهارو اجرا میکنه و چدول اپدیت میشه به 1
صفحه دوم (تست 2 ) هم اپدیت میشه به 1
خب که چی ؟ مشکلی نداره که ؟ داره ؟ من دارم اشتباه میگم ؟

eshpilen
دوشنبه 20 آبان 1392, 10:56 صبح
خوب ببینید، این کوئریها (SELECT و UPDATE) به نحوی نوشته شدن که کاملاً مستقل از هم هستن. بنابراین بدون Transaction هم درست کار میکنن. Transaction در اصل برای وقتی کاربرد داره که اگه یکی از کوئریها Fail شد، قبلیها هم Roll Back بشن. برای مثال، فرض کنید از یک جدول، یک فیلد رو در کوئری 1 میخونیم و توی کوئری دوم، اون رو در جدول دیگری درج میکنیم که اون فیلد Unique هست. مثلاً سناریوی زیر رو برای سیستم قرعه کشی درنظر بگیرین:
اینجا اگه دو نفر همزمان صفحه رو درخواست کنن و درخواستها همزمان بیاد و تا مرحله 4 با هم جلو برن و مرحله 5 با تفاوت زمانی (به هر دلیل) رخ بده، وقتی اولی عمل Insert رو انجام بده، دومی نمیتونه Insert کنه درحالی که قبلش Update روی جدول users برای اون کاربر و products برای اون محصول انجام شده. خوب اگه 5 مرحله با Transaction انجام شده باشه، درصورتی که مرحله 5 برای دومی اتفاق نیفته، 4 مرحله قبلی رو میشه Roll Back کرد.
فکر میکنم واضح گفته باشم و تفاوت این روش با سیستم قفل مشخصه. توی قفل، اصلاً دسترسی نمیده ولی اینجا دسترسی میده ولی اگه نتونه کاری که مشخص شده رو انجام بده، مراحل قبلی رو برگشت میزنه.
مثال خوبی بود.
درسته به این شکل میشه استفاده کرد.
ولی نه در هر موردی.
در خیلی موارد هم نمیشه یا صرف نمیکنه که از ترنزکشن برای جلوگیری از تداخل کوئری ها استفاده کنیم.
در این مثال شما بهرحال در نهایت جلوی این تداخل یجوری گرفته و قابل تشخیص میشه توسط برنامه نویس، و عملیات برگردان میشه، اما خیلی موارد اینطور نیستن یا شایدم در بعضی موارد بشه سعی در تشخیص بوجود آمدن تداخل کرد، ولی احتمالا الگوریتم و کد و پیچیدگی و کاهش پرفورمنسی که ایجاد میکنه باعث میشه همون روشهای ساده و سرراست مثل قفل به صرفه تر باشن.

چیزی هم که من میخواستم بدونم و مطمئن بشم همین بود که آیا ترنزکشن این خاصیت رو داره که بطور کلی و عمومی و خودکار از تداخل کوئری های درخواستهای دیگر در بین کوئری های خودش جلوگیری کنه یا نه، که تا اینجا مشخص شد جواب «نه» است!
یعنی من شخصا فکر میکردم احتمالا هر ترنزکشن بصورت انحصاری اجرا میشه و کوئری دیگری ممکن نیست بین کوئری های یک ترنزکشن تاثیرگذار باشن.

eshpilen
دوشنبه 20 آبان 1392, 11:01 صبح
خب اینکه طبیعی هستش :متفکر:

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

eshpilen
دوشنبه 20 آبان 1392, 11:09 صبح
آقای MMSHFE راهی بنظر شما میاد که با استفاده از ترنزکشن مشکل اون الگوریتم شمارنده رو برطرف کنیم؟

Veteran
دوشنبه 20 آبان 1392, 11:09 صبح
عزیزم خب خودمون داریم اپدیت صفحه اول رو به تاخیر میندازیم
صفحه دوم هم میاد دوباره مقداره اولیه فیلد که 0 هست رو میخونه
در هردو صفحه یک مقدار افزایش پیدا میکنه و بعدش فیلد کانتر در هردو به 1 اپدیت میشه
چراکه خودمون نذاشتیم به موقع اپدیت فیلد کانتر در صفحه اول به انجام بشه که در صفحه دوم مقداره جدید رو بخونیم
به همین دلیله !
حالا اینکه شما میگی نباید اینطور باشه رو من نمیفهمم یعنی چی !
بزنی ماشینو خراب کنی بعد بگی نباید ماشین خراب میشد !

eshpilen
دوشنبه 20 آبان 1392, 11:27 صبح
حالا اینکه شما میگی نباید اینطور باشه رو من نمیفهمم یعنی چی !
بزنی ماشینو خراب کنی بعد بگی نباید ماشین خراب میشد !
ببین فرض کن ما اون sleep ها رو حذف کنیم، و بقیهء کد رو بعنوان شمارندهء سایتمون استفاده کنیم.
بنظرت مشکلی نداره؟
فرض کن دو درخواست میاد از دو کاربر جداگانه، فرضا با فاصلهء 0.1 ثانیه.
بعد فکر میکنی امکان نداره قبل از اینکه هیچکدام از این دو درخواست جدول رو آپدیت کرده باشن (write)، هر دو مقدار یکسانی رو از جدول خونده باشن (read)؟
اگر میگی نه امکان نداره، چرا؟ دلیل و سندت برای این ادعا چیه؟


چراکه خودمون نذاشتیم به موقع اپدیت فیلد کانتر در صفحه اول به انجام بشه که در صفحه دوم مقداره جدید رو بخونیمموضوع همینه که این «به موقع» از دید شما برنامه نویسه که وجود داره. سیستم عامل، MySQL، PHP و هیچ بخش دیگری که کد شما رو اجرا میکنه از این منطق و انتظار خبر نداره و هیچ تضمینی در این زمینه به شما نمیده. یعنی تضمینی وجود نداره که کدها و کوئری هایی که شما گذاشتی بقول خودت به موقع اجرا بشن. ممکنه کوئری اول هر دو درخواست قبل از کوئری های دوم اونا اجرا بشه. و این باعث میشه با اینکه دو درخواست داشتیم اما فقط یک واحد به فیلد شمارنده اضافه بشه.

eshpilen
دوشنبه 20 آبان 1392, 11:43 صبح
فراموش نکن که درخواستها بصورت موازی پردازش میشن. یعنی اینطور نیست که لزوما درخواست یا کوئری B صبر میکنه تا پردازش درخواست یا کوئری A تموم بشه.
در دیتابیس هم کوئری های read میتونن بصورت موازی و همزمان اجرا بشن.
بنابراین مراحل اجرای این دو درخواست میتونه بصورت موازی هم پیش بره.
ضمنا برای بروز این تداخل نیازی نیست که درخواستها و مراحل پردازش اونا دقیقا همزمان باشن، بلکه فقط این مهمه که زمانی که هنوز یکی جدول رو آپدیت نکرده، دیگری read بکنه. مثلا ممکنه در این زمان کوئری اول چند خط کدش بیشتر اجرا شده باشه، ولی هنوز کوئری آپدیت رو اجرا نکرده.

eshpilen
دوشنبه 20 آبان 1392, 11:47 صبح
ضمنا بنده تعجب میکنم واقعا از اینکه ظاهرا اکثریت افراد این مسائل پایه ای رو هنوز نمیدونن!
پردازش موازی شاید 20 سال پیش هنوز اینقدر فراگیر و شناخته شده نبود، ولی امروزه یک مسئلهء عادیه.
حتی روی PC های امروزی هم CPU ها چند هسته ای هستن و بنابراین میتونن بصورت موازی پردازش کنن.
تازه جالب اینکه برای رخداد خیلی از این تداخل ها، به پردازش موازی واقعی هم نیازی نیست.
مثلا وقتی شما چند برنامه رو همزمان روی یک PC پنتیوم 3 اجرا میکنید، پردازش موازی واقعی وجود نداره، اما بصورت مجازی وجود داره. یعنی CPU با سرعت زیاد بین چند پراسس سویچ میکنه. مثلا یه مقدار از درخواست A رو پردازش میکنه، بعد پردازش درخواست A رو متوقف میکنه و میره یه مقدار از درخواست B رو پردازش میکنه، بعد دوباره B رو متوقف میکنه در همونجایی که هست و میره پردازش بقیهء A رو از سر میگیره، و خلاصه همینطور در هر ثانیه بارها و بارها این کار رو انجام میده و بنظر ما میاد که درخواستها دارن بصورت همزمان/موازی پردازش میشن. البته این به خود برنامه (در اینجا آپاچی و PHP) و تنظیماتش هم بستگی داره که از این قابلیت استفاده بکنه یا نه و در هر زمان خاص حداکثر چند درخواست /ترد رو برای پردازش همزمان ایجاد و پردازش کنه.

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

MMSHFE
دوشنبه 20 آبان 1392, 13:44 عصر
آقای MMSHFE راهی بنظر شما میاد که با استفاده از ترنزکشن مشکل اون الگوریتم شمارنده رو برطرف کنیم؟
با Transaction به تنهایی فکر نمیکنم بشه چون Transaction روی جدول قفل نمیگذاره. راه منطقی همون قفله. Transaction فقط توی چند کوئری که اجراشون وابسته به همه، این تضمین و اختیار رو میده که یا همه با هم اجرا بشن، یا هیچکدوم اجرا نشن. تضمین نمیده که جدول تو این فاصله تغییر نکنه.

MMSHFE
دوشنبه 20 آبان 1392, 13:48 عصر
veteran@ :
ببینید، مسئله اینه که CPU وقت با ارزششو تلف نمیکنه. مثلاً شما Sleep گذاشتی توی اسکریپتت، یا حتی نگذاشتی ولی اسکریپتت منتظر جواب از منبعی هست که زمان میبره (مثل سوکت و وب سرویس و...). خوب توی این فاصله، CPU بجای علاف بودن، میره و یک اسکریپت دیگه رو که همزمان (یا با تأخیر زمانی کم و قبل از زمانی که اسکریپت شما تو کما رفته) رسیده، پردازش میکنه. در اینجا احتمال اینکه دیتابیس قبل از اینکه دوباره به سراغ برنامه شما بیاد تغییر کنه، خیلی زیاده. حالا این وظیفه برنامه شماست که اگه لازم داره جدول تغییر نکنه، روش قفل بگذاره. مسئله خیلی روشنه نه؟!

shahriyar3
دوشنبه 20 آبان 1392, 14:52 عصر
مثلا وقتی شما چند برنامه رو همزمان روی یک PC پنتیوم 3 اجرا میکنید، پردازش موازی واقعی وجود نداره، اما بصورت مجازی وجود داره. یعنی CPU با سرعت زیاد بین چند پراسس سویچ میکنه. مثلا یه مقدار از درخواست A رو پردازش میکنه، بعد پردازش درخواست A رو متوقف میکنه و میره یه مقدار از درخواست B رو پردازش میکنه، بعد دوباره B رو متوقف میکنه در همونجایی که هست و میره پردازش بقیهء A رو از سر میگیره، و خلاصه همینطور در هر ثانیه بارها و بارها این کار رو انجام میده و بنظر ما میاد که درخواستها دارن بصورت همزمان/موازی پردازش میشن. البته این به خود برنامه (در اینجا آپاچی و PHP) و تنظیماتش هم بستگی داره که از این قابلیت استفاده بکنه یا نه و در هر زمان خاص حداکثر چند درخواست /ترد رو برای پردازش همزمان ایجاد و پردازش کنه.

به نظرم این الگوریتم زمان بندی صف قدیمی باشه .
خیلی سالهای پیش راجب اینا خوندم . صف های الویت دار به ترتیب اولیت(اجرای برنامه) اجرا میشن. اولیتش هم "اهمیت اجرا" هست البته پارامتر های دیگه ای مثل زمان هم هست ولی در هر اولویت فقط یک process انجام میشه
اگر پردازش گر قدرت پردازش موازی داشته باشه به ترتیب صف الویت اجرا برنامه ها رو به صورت موازی اجرا میکنه و در صورت بروز وقفه در برنامه از حالت process در میاد میره به صف و بر اساس آخرین اولویت دوباره شروع به پردازش میکنه. خود پردازش گر قاعدتا تاخیری تو اجرای برنامه ها به وجود نمیاره
اگر اسکریپت شما وقفه داشته باشه از اولویت های اول خارج میشه و تو صف تغییراتی بوجود میاد .
بحث صف cpu خیلی پیچیده و طولانی هست و احتیاج به تحقیق زیاد داره .

MMSHFE
دوشنبه 20 آبان 1392, 17:12 عصر
ببینید، اینجا اصلاً بحثهای تخصصی CPU و الگوریتمهای زمانبندی معماری و... مطرح نیست. بحث خیلی ساده است: چند درخواست داره همزمان میرسه به MySQL و اگه یکی از اونها میخواد بقیه توی جدول تغییر ایجاد نکنن، باید با قفل، جدول رو در اختیار بگیره و بعد از انجام کار، قفل رو آزاد کنه تا بقیه به کارشون برسن. موضوع خیلی ساده است. نمیدونم چرا اینقدر داره پیچیده میشه توی اذهان! بحثهای وقفه و... ارتباطی به این سیستم و این مشکل نداره. اصلاً وظیفه نرم افزار هم دونستن اینها نیست. بخصوص در برنامه وب که اصولاً اینقدر بزرگ نیست که بخواد با وقفه های CPU خودش رو درگیر کنه. حتی توی هاست های اشتراکی چنین اجازه ای هم داده نمیشه.

abolfazl-z
دوشنبه 20 آبان 1392, 18:10 عصر
ببینید، اینجا اصلاً بحثهای تخصصی CPU و الگوریتمهای زمانبندی معماری و... مطرح نیست. بحث خیلی ساده است: چند درخواست داره همزمان میرسه به MySQL و اگه یکی از اونها میخواد بقیه توی جدول تغییر ایجاد نکنن، باید با قفل، جدول رو در اختیار بگیره و بعد از انجام کار، قفل رو آزاد کنه تا بقیه به کارشون برسن. موضوع خیلی ساده است. نمیدونم چرا اینقدر داره پیچیده میشه توی اذهان! بحثهای وقفه و... ارتباطی به این سیستم و این مشکل نداره. اصلاً وظیفه نرم افزار هم دونستن اینها نیست. بخصوص در برنامه وب که اصولاً اینقدر بزرگ نیست که بخواد با وقفه های CPU خودش رو درگیر کنه. حتی توی هاست های اشتراکی چنین اجازه ای هم داده نمیشه.

کاملا درسته.

اگر قرار باشه mysql هر دستور را در صف قرار دهد ما باید برای باز کردن گوگل تا 1 ماه صبر کنیم شاید نوبتمان شد.

abolfazl-z
دوشنبه 20 آبان 1392, 18:12 عصر
ولی یک سول ؟

زمانی که جدول قفل بشود و یک کوئری همان لحظه درخواست دهد آیا کوئری خطا بر میگرداند و یا نه صبر می کند تا جدول آزاد بشود؟

eshpilen
دوشنبه 20 آبان 1392, 18:13 عصر
خوب توضیح دادی مهندس.
از این ساده تر و روشن تر دیگه نمیشه.
خوبه حالا شما گفتی، وگرنه من میگفتم شاید باور نمیکردن!
حالا هم احتمالا باز نفهمیدن یا شک دارن بعضیا.

نمیدونم چرا اینقدر داره پیچیده میشه توی اذهان!
دمار از روزگارم درآوردن اینا مهندس :لبخند:
معلم های اینا چی کشیدن از دستشون :متعجب:

eshpilen
دوشنبه 20 آبان 1392, 18:18 عصر
ولی یک سول ؟

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

eshpilen
دوشنبه 20 آبان 1392, 18:39 عصر
بابا درخواست ها میتونن بطور موازی اجرا بشن.
فرض کنید سرور اصلا بیکاره.
دو تا درخواست میرسه، سرور هر دو رو همزمان پردازش میکنه.
نیازی هم نیست دقیقا همزمان برسن. ممکنه چند دهم ثانیه اختلاف داشته باشن. بیشتر، کمتر. زیاد مهم نیست.
شما نمیتونید به هیچ وجه بدونید که دستورات کدوم درخواست چه زمانی اجرا میشن کدوم درخواست زودتر کدوم دیرتر اجرا میشه.
ممکنه الان داره خط چهارم یکی توسط مفسر PHP اجرا میشه اون یکی داره خط ششمش اجرا میشه، بعد تازه ممکنه به هزار و یک دلیل، این فاصله تغییر کنه، مثلا اولی برسه خط ششم دومی خط هفتم باشه (فاصلهء دستورات کمتر شد)، و حتی ممکنه اونی که اول پیش بوده بعدا پس بیفته. ممکنه این پس و پیش افتادن بارها در جریان اجرا تکرار و معکوس بشه. خط به خط، کاراکتر به کاراکتر، دستور به دستور، کوئری به کوئری. مثل مسابقه دهنده هایی که تا پایان مسیر مشخص نمیشه بالاخره چه کسی زودتر چه کسی دیرتر میرسه و شاید هم چند نفر همزمان از خط پایان عبور کنن، و در مسیر مدام وضعیت اینا نسبت به هم تغییر میکنه.

ممکنه یه درخواست الان میاد دومی 3 ثانیه بعد میرسه به سرور، منتها سرور همون موقع که درخواست اول رسیده هم اینقدر سرش شلوغ و منابعش اشغال بوده که درخواست اول رو از اون موقع تاحالا توی اتاق انتظار نگه داشته، و بعد از اینکه سرش بقدر کافی خلوت میشه، یعنی مثلا پردازش یک درخواست سنگین دیگه که مدت زیادی درحال اجرا بوده تموم میشه، این دوتا درخواست رو یکجا برمیداره از اتاق انتظار و شروع به اجرای همزمان اونا میکنه.
یعنی میخوام بگم حتی روی این حساب نکنید بگید چون یک درخواست 3 ثانیه زودتر رسیده پس با اون یکی درخواست امکان تداخل نداره.
شما به هیچ وجه نباید روی زمانبندی و ترتیب اجرای درخواستها و کدهای درخواستهای مختلف نسبت به هم حساب کنید، چون نمیشه هم 100% حساب کرد. هیچ تضمینی وجود نداره. در هیچ رفرنسی تضمینی ندادن چیزی نگفتن. قفل و اینا رو هم بخاطر مدیریت همین شرایط گذاشتن و اینکه بتونید کاری کنید که از عدم بروز تداخل مطمئن باشید.

یه درخواست داره اجرا میشه، ممکنه همون موقع یه پردازش با اولویت بالا یا ریل تایم میاد توی سیستم و اجرای درخواست جاری بخاطر آزاد شدن CPU موقتا متوقف میشه، ممکنه این تعلیق موقت هر زمانی طول بکشه (به ندرت خیلی طولانی میشه، ولی کشیده شدن حتی تا چند ثانیه چیز عجیبی نیست).

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

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

H:Shojaei
دوشنبه 20 آبان 1392, 20:12 عصر
جناب eshpilen باز هم من غذر خواهي ميكنم ولي من هنوز هم رو اين مساله ي مثال شما مشكل دارم كه البته از تاپيك هاي اول همينجا هم گرفتم دوستان ديگه هم باهاش مشكل دارن :D .
يه مثال واسه تداخل درست كردم ديگه اين كاملا تداخل كوئري ها رو نشون ميده و نشون ميده كه بعضي از درخواستها به دليل تداخل و هم زمان بودن رو يه فيلد يه نتيجه ي مثل هم دارن كه نبايد داشته باشن (همون چيزي كه شما ميفرماييد ولي با سند كه مقادير ديتابيسه).
تو اين مثال دو تا برنامه داريم مثلا دوتا كاربر كه هر دو ميخوان يه فيلد (counter كه مثلا اول 1 يا 0 هست) رو همزمان بخونن يكي بهش اضافه كنن و همونو به عنوان يه سطر جديد درج كنن كه اين عمليات تو هر كدوم از برنامه ها 100 بار تكرار ميشه كه جمعا اگر همه ي درخواست ها اجرا بشن بايد 201 ركورد مجزا در جدول داشته باشيم. حالا مشكلي كه پيش مياد اينه كه اگر شما هر دو رو همزمان اجرا كنيد (جهت يادآوري و اهميت موضوع ميگم جسارت نشه:ctrl رو نگه داريد رو تبها كليك كنيد تا سلكت بشن بعد راست كليك و Reload رو بزنيد كه كاملا همزمان اجرا بشن) بعضي از درخواستهاي اين دو برنامه يا كاربر با هم تداخل ايجاد ميكنن و داده هايي كه گاهي اوقات خونده ميشن يه مقداره كه نبايد باشه مثلا من ميانگيني كه گرفتم تقريبا 70 تا درخواست از 200 درخواست تداخل دارن.
تو هر پيج بعد از اجراي هر دو يه سري مقادير درج ميشن يه نگاه به اونها بندازيد و مقاديري كه واكشي شدن و درج نمايش داده شدن تو هر پيج رو ببينيد جالبه.
مثال (دقيقا همين كد رو تو دو تا برنامه اجرا كنيد) :


<?php
mysql_connect('localhost', 'root', '');
mysql_select_db('test');
for($i=0;$i<100;$i++)
{
$res=mysql_query("select counter from `test` order by id desc limit 1");
$row=mysql_fetch_array($res);
$count=$row['counter']+1;
mysql_query("insert into `test` set `counter`=$count");
echo $row['counter']."--".$i."<br />";
}
///////////////
/*

ALTER TABLE `test` DROP `id`;
ALTER TABLE `test` ADD `id` INT( 10 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ;
INSERT INTO `test`.`test` (`counter`, `id`) VALUES ('1', NULL);

*/
?>


اينم كد ايجاد تيبل تو ديتابيس test (حوصله آپلودش نبود :D ) :

-- phpMyAdmin SQL Dump
-- version 3.3.8
-- http://www.phpmyadmin.net
--
-- Host: 127.0.0.1
-- Generation Time: Nov 12, 2013 at 08:02 AM
-- Server version: 5.1.52
-- PHP Version: 5.3.3

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- Database: `test`
--

-- --------------------------------------------------------

--
-- Table structure for table `test`
--

CREATE TABLE IF NOT EXISTS `test` (
`counter` tinyint(3) unsigned NOT NULL,
`id` int(10) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;

--
-- Dumping data for table `test`
--

INSERT INTO `test` (`counter`, `id`) VALUES
(1, 1);



خب حالا با اين تفاصيل كه امكان داره بعضي درخواستها از كاربران كه به ديتابيس ميرسه به هم برسن اين ميتونه واسه دقيقا قفل گذاري ها هم ايجاد بشه يعني دو كاربر همزمان درخواست قفل يه تيبل رو بدن و باز همين مشكلات پيش مياد...
البته اينو تست نكردم با قفل ولي درخواست درخواسته ديگه فرقي فك نميكنم داشته باشه (البته گفتم فقط فكر ميكنم :لبخند:).

abolfazl-z
دوشنبه 20 آبان 1392, 20:14 عصر
بابا درخواست ها میتونن بطور موازی اجرا بشن.
فرض کنید سرور اصلا بیکاره.
دو تا درخواست میرسه، سرور هر دو رو همزمان پردازش میکنه.
نیازی هم نیست دقیقا همزمان برسن...

خوب اینجا میان گفته هایتون یک سوال پیش میاد !

بر فرض میگیریم دو تا کوئری همزمان ارسال بشوند.

و هر دو روی یک جدول کار می کنند.

و هر دو روی همان جدولی که کار میکنند قفل می گذارند.

خوب اینجا میگیم دیگه باید خود mysql تشخیص بده که چکار کنه ؟ یا نه ؟


ببین هیچ اهمیتی نداره وقفه کجا باشه.

چرا دیگه !

فکر کنم این وقفه PHP رو، دیگه mysql حساب نمیکنه (به php میگه e زرنگی:لبخند:)
اون زمانی هست که واقعا این رویداد رخ دهد.(البته از روی حدث میگمااا)

و فکر کنم همین ترنزکشن عمل کند.

http://stackoverflow.com/questions/4980801/how-simultaneous-queries-are-handled-in-a-mysql-database

H:Shojaei
دوشنبه 20 آبان 1392, 20:21 عصر
حالا اگر بشه رو اين كدها كاري كرد كه وقتي داده‌ها دارن select ميشن تيبل توسط هر برنامه‌ جداگانه قفل بشه و بعد از درج مقدار جديد قفل باز بشه مشكل حل ميشه ولي سرعت عمل فكر ميكنم خيلي پايين بياد البته رو مقادير زياد.
البته اين باز احتمالا يه مشكل داشته باشه كه بسته به كاريه كه تو خود ديتابيس انجام ميشه و واكنشش به اين مشكل كه متمايز از ديگر درخواستها باشه يا نباشه يعني اگر باز همين دستورات قفل تيبل ها با هم اجرا بشن چي آيا كاري توسط خود ديتابيس انجام شده كه اين درخواسها تفكيك بشن و هر كدوم كاملا جداگانه اجرا بشن يا نه...

shahriyar3
دوشنبه 20 آبان 1392, 21:22 عصر
استارتر یک قسمتی از صحبت هاش راجب صف های اجرای برنامه cpu بود منم همون قسمت و فقط نقل قول کردم و جواب دادم .
یکم بیشتر به پست دقت میکردید مشخص بود جواب چی و دادم!!


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

abolfazl-z
دوشنبه 20 آبان 1392, 22:00 عصر
استارتر یک قسمتی از صحبت هاش راجب صف های اجرای برنامه cpu بود منم همون قسمت و فقط نقل قول کردم و جواب دادم .
یکم بیشتر به پست دقت میکردید مشخص بود جواب چی و دادم!!
در جواب این صحبت ها هم فقط میشه خندید به بیسوادی

اول سلام
دوم من اصلا پست شما را نخواندم ! و در جواب شما نقل و قول نکردم.

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

Tarragon
سه شنبه 21 آبان 1392, 04:27 صبح
سلام
اقای شهرکی این راه حل بهتری نیست؟
mysql_query("insert into `test` set `counter`=`counter` + 1");

eshpilen
سه شنبه 21 آبان 1392, 08:23 صبح
يه مثال واسه تداخل درست كردم ديگه اين كاملا تداخل كوئري ها رو نشون ميده و نشون ميده كه بعضي از درخواستها به دليل تداخل و هم زمان بودن رو يه فيلد يه نتيجه ي مثل هم دارن كه نبايد داشته باشن (همون چيزي كه شما ميفرماييد ولي با سند كه مقادير ديتابيسه).
واسه من مگه بدون سند بود؟


جهت يادآوري و اهميت موضوع ميگم جسارت نشه:ctrl رو نگه داريد رو تبها كليك كنيد تا سلكت بشن بعد راست كليك و Reload رو بزنيد كه كاملا همزمان اجرا بشن
واسه من اینطوری کار نمیکنه.
از چه مرورگر و چه نسخه ای استفاده میکنی؟


بعضي از درخواستهاي اين دو برنامه يا كاربر با هم تداخل ايجاد ميكنن و داده هايي كه گاهي اوقات خونده ميشن يه مقداره كه نبايد باشه مثلا من ميانگيني كه گرفتم تقريبا 70 تا درخواست از 200 درخواست تداخل دارن.
تو هر پيج بعد از اجراي هر دو يه سري مقادير درج ميشن يه نگاه به اونها بندازيد و مقاديري كه واكشي شدن و درج نمايش داده شدن تو هر پيج رو ببينيد جالبه.
الحمدالله پس اصل قضیهء امکان بروز تداخل درخواستها با هم پذیرفته شد!؟
قصد منم معرفی همین مسئله بود که خیلی ها نسبت بهش آگاه نیستن.
بعدش میرسیم سر راه حل های ممکن.


مثال (دقيقا همين كد رو تو دو تا برنامه اجرا كنيد) :

...

اينم كد ايجاد تيبل تو ديتابيس test (حوصله آپلودش نبود :D ) :

...


خب منم تست کردم.
حالا از اینا میخواستی به چه نتیجه ای برسی؟ اینکه امکان تداخل وجود داره؟ منکه از اول همینو میگفتم!



خب حالا با اين تفاصيل كه امكان داره بعضي درخواستها از كاربران كه به ديتابيس ميرسه به هم برسن اين ميتونه واسه دقيقا قفل گذاري ها هم ايجاد بشه يعني دو كاربر همزمان درخواست قفل يه تيبل رو بدن و باز همين مشكلات پيش مياد...
البته اينو تست نكردم با قفل ولي درخواست درخواسته ديگه فرقي فك نميكنم داشته باشه (البته گفتم فقط فكر ميكنم :لبخند:).
د نه دیگه!
قفل رو طوری درست کردن که ممکن نیست چند درخواست بتونن همزمان یک قفل رو بگیرن.
این تضمین شده است.
البته منظورم قفلهای انحصاری است (exclusive lock). چون قفلهای غیرانحصاری (shared lock) هم داریم که در بعضی سناریوها کاربرد دارن.
اصلا قفل اگر چنین خاصیتی نداشته باشه که خودشون رو مسخره کردن اصلا واسه چی گذاشتن که ما بخوایم مشکلاتی رو باهاش حل کنیم که خودش هم گرفتارشه!!
حالا اینکه الگوریتم و روشش چطوریه که جلوی این قضیه رو میگیره خب دیگه ربطی به این بحث نداره و داستان خودش رو داره (که بنده هم تاحالا مطالعه نکردم راجع بهش).

eshpilen
سه شنبه 21 آبان 1392, 08:33 صبح
خوب اینجا میان گفته هایتون یک سوال پیش میاد !

بر فرض میگیریم دو تا کوئری همزمان ارسال بشوند.

و هر دو روی یک جدول کار می کنند.

و هر دو روی همان جدولی که کار میکنند قفل می گذارند.

خوب اینجا میگیم دیگه باید خود mysql تشخیص بده که چکار کنه ؟ یا نه ؟


چرا دیگه !

فکر کنم این وقفه PHP رو، دیگه mysql حساب نمیکنه (به php میگه e زرنگی:لبخند:)
اون زمانی هست که واقعا این رویداد رخ دهد.(البته از روی حدث میگمااا)

و فکر کنم همین ترنزکشن عمل کند.

http://stackoverflow.com/questions/4980801/how-simultaneous-queries-are-handled-in-a-mysql-database
درمورد قفل که در پست قبلی گفتم قفل انحصاری رو همیشه فقط یک درخواست میتونه در اختیار بگیره، ولو دو یا چند درخواست بصورت کاملا همزمان اجرا بشن.
این رو خود MySQL مدیریت میکنه. حالا شاید MySQL هم خودش از کتابخانه ها و امکانات سیستم عامل برای این کار استفاده میکنه؛ نمیدونم.

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


و فکر کنم همین ترنزکشن عمل کند.
منظورت چیه دقیقا؟
میگی ترنزکشن از تداخل و دسترسی همزمان جلوگیری میکنه؟

eshpilen
سه شنبه 21 آبان 1392, 08:41 صبح
حالا اگر بشه رو اين كدها كاري كرد كه وقتي داده‌ها دارن select ميشن تيبل توسط هر برنامه‌ جداگانه قفل بشه و بعد از درج مقدار جديد قفل باز بشه مشكل حل ميشه ولي سرعت عمل فكر ميكنم خيلي پايين بياد البته رو مقادير زياد.
بله میشه این کار رو کرد.
سرعت هم بطور معمول بطور محسوس یا مشکل سازی پایین نمیاد مگر اینکه ترافیک اونقدری زیاد باشه که درخواستهای همزمان یا با فاصلهء زمانی کم اونقدری زیاد داشته باشیم که تعداد قابل توجهی از درخواستها مجبور بشن توی نوبت بمونن.
در کل هم بهرحال توان پردازش موازی سرور نامحدود نیست. مثلا CPU سرور ممکنه 8 تا هسته داشته باشه، بنابراین تا 8 درخواست همزمان رو میتونه بصورت موازی پردازش کنه و بیشتر از اون باشه پرفورمنس پایین تر میاد، چون برای بیشتر از 8 درخواست پردازش موازی واقعی نداره و مجازی میشه.
یعنی منظورم اینه در ترافیک اونقدر بالا احتمالا خود سرور هم بطور کلی کند میشه و تاثیر قفل ما فقط بخشی از کاهش پرفورمنس رو باعث میشه، نه همش رو.
ضمنا خیلی وقتا لازم نیست کل جدول رو قفل کنیم، بلکه میشه فقط رکورد مورد نظر رو قفل کرد، و به این شکل کوئری های دیگری که با رکوردهای دیگر کار دارن میتونن بصورت موازی اجرا بشن.


البته اين باز احتمالا يه مشكل داشته باشه كه بسته به كاريه كه تو خود ديتابيس انجام ميشه و واكنشش به اين مشكل كه متمايز از ديگر درخواستها باشه يا نباشه يعني اگر باز همين دستورات قفل تيبل ها با هم اجرا بشن چي آيا كاري توسط خود ديتابيس انجام شده كه اين درخواسها تفكيك بشن و هر كدوم كاملا جداگانه اجرا بشن يا نه... بله دیگه گفتیم که اصولا گرفتن همزمان قفل انحصاری توسط بیش از یک درخواست غیرممکنه.
حتی اگر 1000 تا درخواست برای گرفتن قفل بصورت کاملا همزمان به دیتابیس برسن، فقط یکی از اونا موفق به گرفتن قفل میشه. بعد که اون قفل رو آزاد کرد، دوباره قفل فقط به یک درخواست داده میشه، و همینطور به نوبت تا اینکه آخرین درخواست هم قفل رو بگیره.

abolfazl-z
سه شنبه 21 آبان 1392, 08:50 صبح
منظورت چیه دقیقا؟
میگی ترنزکشن از تداخل و دسترسی همزمان جلوگیری میکنه؟

آره فکر کنم :متفکر:

ولی همانطور که شما میگین قفل اینطور کار کند دیگه لازم به هیچ کاری نیست. (یک نمونه مثال بزنید از همین قفل)

eshpilen
سه شنبه 21 آبان 1392, 09:22 صبح
آره فکر کنم :متفکر:
منم فکر میکردم میتونه. البته شک داشتم.
بعد تست کردم دیدم ظاهرا نمیتونه!
ولی الان داشتم اون منابع و سرنخ ها رو دنبال میکردم به یک مطلب و سینتاکسی برخورد کردم که ظاهرا واسه همین کاره و میتونیم باهاش ترنزکشن هایی ایجاد کنیم که از تداخل هم جلوگیری کنن.
البته درواقع در نهایت همین قفل ها هستن که دارن جایی کار رو انجام میدن. یعنی حتی ترنزکشن هم در نهایت داره از قفل استفاده میکنه برای عدم ایجاد تداخل. فقط سینتاکس و امکاناتش یخورده تفاوت میکنه. برنامه نویس میتونه خودش صریحا قفل بگیره، و میتونه منظورش رو در کوئری بیان کنه. البته روش دوم میتونه بهتر باشه، چون سطح بالاتره و ممکنه یکسری بهینه سازیهایی رو دیتابیس بتونه خودش بصورت خودکار برای افزایش پرفورمنس انجام بده که در حالت اول نمیتونه. بنابراین بخصوص جاهایی که بهرصورت به دلایل دیگری از ترنزکشن استفاده میکنیم، بهتره از این روش استفاده کنیم. ولی قفل هم بعضی جاها میتونه بهتر یا لازم باشه. خلاصه بستگی داره. نمیشه نظر کلی و مطلق داد. از مورد به مورد میتونه تفاوت کنه با توجه به منطق و جزییاتش.


یک نمونه مثال بزنید از همین قفلمثلا در مثال شمارنده، ما میتونیم کل جدول رو با دستور LOCK TABLES `test` WRITE قفل کنیم.
کلمهء WRITE در اینجا مهمه، چون به MySQL میفهمونه که ما میخوایم در جدول بنویسیم و بنابراین به یک قفل از نوع انحصاری نیاز داریم.


<?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('LOCK TABLES `test` WRITE');

$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('UNLOCK TABLES');

echo "<br>Write: $counter";

?>

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

بعضی جاها اصلا میشه با تغییر دادن کوئری ها و کد یا کلا انتخاب یک الگوریتم جایگزین دیگر، مشکل تداخل رو از بین برد بدون اینکه نیازی به قفل باشه.
قفل ها هم انواع دارن از جهات مختلف. انحصاری، اشتراکی، Advisory، در سطح جدول، در سطح رکورد، ...، و هر قفل هم ممکنه آپشن های متفاوتی داشته باشه (الان همین LOCK TABLES (http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html) یک آپشن جالبی داره که داشتم میدیدم).
باید اینا رو آدم بدونه که هرکدام چه خاصیتی دارن از چه جهتی متفاوت هستن و بتونه بفهمه هرجا از کدامش باید استفاده کنه هم از نظر نیاز منطق برنامه و هم از نظر بهینه تر بودن.

اینجاست که بقول جناب شهرکی مردم کم کم متوجه میشن که Develop با Design خیلی فرق داره.
یعنی اینکه برنامه نویسی با PHP این نیست که صرفا باهاش خروجی HTML تولید کنیم برای کلاینت و دوتا جدول خروجی بدیم و در دیتابیس ذخیره و بازیابی کنیم. بلکه خیلی بیشتر نکات و علم و اصول خودش رو داره.
یک برنامه نویس واقعی باید دانش و بینش گسترده و قوی ای داشته باشه و تمام این موارد رو مطالعه و درک کنه تا جایی که نیازه بتونه تشخیص بده و ازشون استفاده کنه. سوالات و پارامترهای زیادی وجود دارن. هر مورد بستگی به خیلی جزییات و منطق و هدف برنامه نویس و شرایط محیط اجرای برنامه داره که چه روشی انتخاب بشه. مثلا لزوما این نیست که فکر کنید هر برنامه ای رو همیشه باید یا باید از همون اول تا حداکثر ممکن بهینه کرد. نه خود این روش ممکنه از نظرهای دیگر بهینه نباشه (مثلا از نظر افزایش پیچیدگی برنامه، وقت و هزینهء برنامه نویسی، هیچگاه فایده نداشتن در عمل، غیرقابل پیشبینی بودن کامل از نظر عملی و بهینه سازیهایی که بی اثر میشن یا حتی اثر معکوس میذارن).

eshpilen
سه شنبه 21 آبان 1392, 11:03 صبح
خب سرانجام مکشوف کردید که در یک ترنزکشن هم میشه بدون ایجاد صریح قفل، از تداخل جلوگیری کرد! حداقل برای مثال و سناریویی که تاحالا مطرح کردیم که میشه.

پس چرا تست قبلی ما با ترنزکشن جواب نداد؟
خب بخاطر اینکه ما بصورت صریح به 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";

?>

توضیحات رفرنس رسمی در این مورد (http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47902338553504):


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 نقش دارن قفل میشن.

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

eshpilen
سه شنبه 21 آبان 1392, 11:18 صبح
حالا بجز این 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 درخواستهای دیگر هم در این اثنا اجرا بشن، از نظر بهینگی و پرفورمنس سیستم کاراتره.

abolfazl-z
سه شنبه 21 آبان 1392, 12:21 عصر
واقعا جای تشکر دارد.

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

همین امکان خیلی عالی هست. (تاثیر زیادی روی پرفرمنس داره)

abolfazl-z
سه شنبه 21 آبان 1392, 12:27 عصر
یک سوال دیگر ؟

COMMIT برای چی هست ؟(I travel)

abolfazl-z
سه شنبه 21 آبان 1392, 12:34 عصر
راستی کلا دو نوع قفل وجود دارد :

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html

اینم مرجع کامل مدل ترنزکشن و قفل :

http://dev.mysql.com/doc/refman/5.0/en/innodb-transaction-model.html

eshpilen
سه شنبه 21 آبان 1392, 12:35 عصر
COMMIT مال بحث ترنزکشن هست دیگه.
معلومه ترنزکشن کار نکردی تاحالا.
کوئری هایی که بین یک START TRANSACTION و COMMIT قرار میگیرن، بعنوان یک مجموعه هستن، که میشه در هرجایی که لازم باشه اونا رو rollback کنیم.
حالا جزییات بیشتر و ایناش رو حضور ذهن ندارم و خودم هم تاحالا کار نکرده بودم که بخوام توضیح بیشتر بدم.
دقت کنید این بحث FOR UPDATE که گفتیم وقتی هست که کوئریش داخل یک ترنزکشن باشه.
کوئری های read و write ما داخل یک ترنزکشن قرار دارن.

abolfazl-z
یک شنبه 16 فروردین 1394, 13:55 عصر
پس از چند وقت برای یاد آوری به این تاپیک سر زدم تا دوباره یک مروری کنم اما ناگهان متوجه یک اشتباه (شاید هم من اشتباه کنم) از سوی مطالب شدم.


خب این خیلی خوبه. چون MySQL خودش بصورت خودکار رکوردهایی رو که کوئری SELECT ما با اونا کار داره قفل میکنه.

فکر کنم حالا نمیدونم ولی تست من اینطور جواب داد که transaction بر روی رکورد هایی که ما کار می کنیم تاثیر ندارد بلکه بر روی جدولی که در اولین کوئری select می شود تاثیر دارد در نهایت کل جدول قفل می شود.

بطور مثال :


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");

$con = mysqli_connect('localhost', 'root', '');

mysqli_select_db($con,'test');

mysqli_query($con,'START TRANSACTION');



#mysqli_query($con,'select 1 from `test` FOR UPDATE');
$res=mysqli_query($con,'select `counter` from `test` WHERE `id` = 1 for update');
$res2=mysqli_query($con,'select `counter` from `test` WHERE `id` = 2');

$row=mysqli_fetch_array($res);
$row2=mysqli_fetch_array($res2);

$counter=$row[0];
$counter2=$row2[0];

echo "Read: $counter<br>";
echo "Read: $counter2<br>";

$counter++;
$counter2++;

sleep(5);

mysqli_query($con,"update `test` set `counter`=$counter WHERE `id` = 1");
mysqli_query($con,"update `test` set `counter`=$counter2 WHERE `id` = 2");

mysqli_query($con,'COMMIT');

echo "Write: $counter<br>";
echo "Write: $counter2<br>";


یا


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");

$con = mysqli_connect('localhost', 'root', '');

mysqli_select_db($con,'test');

mysqli_query($con,'START TRANSACTION');



mysqli_query($con,'select 1 from `test` FOR UPDATE');
$res=mysqli_query($con,'select `counter` from `test` WHERE `id` = 1');
$res2=mysqli_query($con,'select `counter` from `test` WHERE `id` = 2');

$row=mysqli_fetch_array($res);
$row2=mysqli_fetch_array($res2);

$counter=$row[0];
$counter2=$row2[0];

echo "Read: $counter<br>";
echo "Read: $counter2<br>";

$counter++;
$counter2++;

sleep(5);

mysqli_query($con,"update `test` set `counter`=$counter WHERE `id` = 1");
mysqli_query($con,"update `test` set `counter`=$counter2 WHERE `id` = 2");

mysqli_query($con,'COMMIT');

echo "Write: $counter<br>";
echo "Write: $counter2<br>";


دیتابیس مورد نظر

CREATE TABLE IF NOT EXISTS `test` (
`counter` int(11) NOT NULL,
`counter2` int(11) NOT NULL,
`id` int(11) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;

INSERT INTO `test` (`counter`, `counter2`, `id`) VALUES
(5, 0, 1),
(5, 0, 2);


ALTER TABLE `test`
ADD PRIMARY KEY (`id`);


ALTER TABLE `test`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=3;

freeman99
یک شنبه 16 فروردین 1394, 18:25 عصر
منکه نفهمیدم قصدت چی بوده و بنظرت چه جوابی باید میگرفتی.
دوتا فیلد رو خوندی و یدونه اضافه کردی و دوباره خوندی دیگه.
چطور حالا مشکلش کجاست؟ چرا چه نتیجه دیگری باید میگرفتی؟

abolfazl-z
یک شنبه 16 فروردین 1394, 19:52 عصر
اون تست هایی که گذاشتم خروجی هاشون درست هست.(+ تست های بالاتر)

یا اینکه من بد متوجه شدم یا اشتباهی رخ داده


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

مشکل اینجاست که این جمله با تست هایی که کردم درست از آب در نمیاد.

نگاه کنید من با خواندن این جمله فکر کردم (شاید بقیه هم اینطوری فکر کردن) که باید رکورد هایی رو select کنیم تا به mysql بفهمونیم که با این رکورد ها کار داریم و قفلشون کند، که این اشتباه است در واقع استفاده از FOR UPDATE در اولین کوئری SELECT بر روی جدولی که در SELECT انتخاب شده تاثیر می گذارد یعنی کل اون جدول.

همچنین خودتون هم کل داده های جدول را درمثالی که زدین SELECT کردین.(که برای داده های زیاد بهینه نیست اصلا)

حالا من مثال های زیر را زدم تا ثابت کنم که اشتباهی رخ داده

در مثال دوم ما فقط مقدار یک را SELECT کردیم در صورتی که هیچ رکوردی را انتخاب نکردیم


mysqli_query($con,'select 1 from `test` FOR UPDATE');

به مثال اولی توجه کنید :


$res=mysqli_query($con,'select `counter` from `test` WHERE `id` = 1 for update');
$res2=mysqli_query($con,'select `counter` from `test` WHERE `id` = 2');

پس اگر قرار باشد بر روی رکورد هایی که SELECT کردیم قفل اعمال شود چرا روی کوئری دوم قفل اعمال می شود ؟

freeman99
دوشنبه 17 فروردین 1394, 11:57 صبح
کر کردم (شاید بقیه هم اینطوری فکر کردن) که باید رکورد هایی رو select کنیم تا به mysql بفهمونیم که با این رکورد ها کار داریم و قفلشون کند

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


همچنین خودتون هم کل داده های جدول را درمثالی که زدین SELECT کردین.(که برای داده های زیاد بهینه نیست اصلا)
اون فقط یه مثال بود. شما میتونید select رو بر اساس رکوردهایی که نیاز دارید و میخواید روشون کار و آپدیت کنید بنویسید.


در مثال دوم ما فقط مقدار یک را SELECT کردیم در صورتی که هیچ رکوردی را انتخاب نکردیم
عملا چون تعداد 1 هایی که کوئری برمیگردونه بر اساس تعداد رکوردهای جدوله، پس این کوئری باعث میشه تمام رکوردهای جدول قفل بشن. MySQL فقط به این کار داره که چه رکوردهایی در نتایج کوئری به هر شکل ممکن دخیل هستن، چه در خود نتایج و چه در تعدادشون.


پس اگر قرار باشد بر روی رکورد هایی که SELECT کردیم قفل اعمال شود چرا روی کوئری دوم قفل اعمال می شود ؟
نمیدونم دقیقا چی میگی اما من این مسئله رو با کد شما هم تست کردم مشکلی نداشت و طبق انتظار و گفته های قبلی عمل کرد.

ضمنا نکتهء مهم اینکه توجه کن که قفل روی کانکشن MySQL جاری تاثیر نمیذاره، بلکه روی کانکشن های دیگر، یعنی اونایی که درخواستهای همزمان دیگر ایجاد میکنن تاثیر میذاره. بنابراین شما در کدی که در یک صفحه در مرورگر اجرا میکنی نمیتونی قفل رو تست کنی، بلکه باید یک صفحهء دیگر هم باز کنی در اون مدتی که صفحهء اول رو باز کردی و قفل رو گرفته و پشت sleep مونده.

abolfazl-z
دوشنبه 17 فروردین 1394, 17:07 عصر
نمیدونم دقیقا چی میگی اما من این مسئله رو با کد شما هم تست کردم مشکلی نداشت و طبق انتظار و گفته های قبلی عمل کرد.

نه نگاه کنید کد زیر یکم من رو گیج کرده.


<?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");

$con = mysqli_connect('localhost', 'root', '');

mysqli_select_db($con,'test');

mysqli_query($con,'START TRANSACTION');

mysqli_autocommit($con,false);

# mysqli_query($con,'select `counter` from `test` WHERE `id` = 1 for update');

$res = mysqli_query($con,'select `counter` from `test` WHERE `id` = 2');

$row = mysqli_fetch_array($res);

$counter = $row[0];

echo "Read : $counter<br>";

$counter++;

sleep(5);

mysqli_query($con,"update `test` set `counter`=$counter WHERE `id` = 2");

mysqli_query($con,'COMMIT');

echo "Write : $counter";
?>

شما اگر کد بالا را در دو تب جدا اجرا کنید هر دو اجرا یک مقدار را می خوانند و یک مقدار رو هم می نویسند که کاملا درست است.(بدون قفل)

حال اگر خط زیر را از حالت comment خارج کنید خروجی اون چیزی که فکر می کردم نیست


# mysqli_query($con,'select `counter` from `test` WHERE `id` = 1 for update');

نگاه کنید ما در کوئری بالا فقط رکوردی که شناسه اش 1 است را قفل کردیم ولی بقیه سطر ها نباید قفل شوند.

پس چرا با اجرای کوئری اول قفل بر روی کوئری دوم نیز اعمال می شود ؟ در صورتی که در کوئری دوم دستور for update استفاده نشده است.

خوب در کل با کمی تست و ... تونستم ابهامی که داشتم را رفع کنم

اجرای دستور SELECT ... FOR UPDATE در یک transaction بصورت کلی(حتی بدون استفاده از FOR UPDATE در SELECT های بعدی) اون رکورد هایی که باهاشون کار داریم را قفل می کند. یعی در واقع اولین SELECT ... FOR UPDATE قفل را start میزنه و هر رکوردی که ما با هاش کار داریم تا زمانی که commit نشده قفل میکنه.

nazanin_asadi_1
دوشنبه 17 فروردین 1394, 18:54 عصر
منکه نفهمیدم قصدت چی بوده و بنظرت چه جوابی باید میگرفتی.
دوتا فیلد رو خوندی و یدونه اضافه کردی و دوباره خوندی دیگه.
چطور حالا مشکلش کجاست؟ چرا چه نتیجه دیگری باید میگرفتی؟

دوست عزیز مخاطب حرفم تنها شما نیستی

در حال کلی عرض میکنم

این که دوستمون (eshpilen) اومدن با مثالی خیلی ساده مطرح کردن خیلی خوب بود و راحت می شد عمق فاجعه رو به رخ کشید
شما دوتا رکورد رو هیچ وقت در نظر نگیر

عمق فاجعه زمانی هستش که شما سروکارت با قسمت مالی که از همه چیز حساس هستش باشه

یه مثال میزنم به این صورت که یه برنامه حسابداری رو در نظر بگیرین (همین بانک های خودمون )
موجودی حساب یک شخص مثلا 10 میلیون ریال هستش
این شخص دو فقره چک برای یک روز صادر میکنه به مبلغ های 7 میلیون ریال و یک چک 9 میلیون ریال (تا اینجای کار هیچ مشکلی پیش نمیاد)

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

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

حالا هر دو متصدی میان همزمان درخواست کسر از حساب بابت پاس شدن چک رو صادر میکنن
اینجا اگه برنامه نویسهای محترم (همچین برنامه هایی کار یک نفر نمی تونه باشه به هیچ وج ) فکری واسه این مورد نکرده باشن چی میشه ؟ بله بانک یکی از مبلغ رو از دست میده

فکر میکنید بانک یا موسسه های رسمی از همچین مشکلاتی به همین سادگی میگذرن ؟


پروسه کسر از حساب مشتری هم به هیچ وجه اینقدر ساده نمی تونه باشه
مثلا برای همین چک و وصول شدنش ممکنه پروسه هایی چون
1- تائید کد چک تائید مبلغ چک (در صورتی که مشتری توی سیستم بانک مبلغ چک رو ثبت کرده باشه مثله بانک ملت)
2- تایید موجودی حساب برای مبلغ چک
3- کسر مبلغ چک از حساب
4- وصول چک و ثبت نهایی

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

هر دو همزمان چک رو تائید میکنن
همزمان موجودی تائید میشه
هر دو همزمان از حساب کسر میکنه
هر دو وصول چک رو ثبت میکنه

حالا بانک یکی از مبلغ های 9 یا 7 میلیون ریال رو ضرر کرده

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

nazanin_asadi_1
دوشنبه 17 فروردین 1394, 19:42 عصر
پس از چند وقت برای یاد آوری به این تاپیک سر زدم تا دوباره یک مروری کنم اما ناگهان متوجه یک اشتباه (شاید هم من اشتباه کنم) از سوی مطالب شدم.

فکر کنم حالا نمیدونم ولی تست من اینطور جواب داد که transaction بر روی رکورد هایی که ما کار می کنیم تاثیر ندارد بلکه بر روی جدولی که در اولین کوئری select می شود تاثیر دارد در نهایت کل جدول قفل می شود.


در جواب این دوستمون هم می تونم اینجوری بگم که وقتی از FOR UPDATE استفاده می کنی رکوردهای توی اون سلکت قفل گذاری میشن

اینم یه تست که خودتون ببینید

این ساختار دیتابیس :


CREATE TABLE IF NOT EXISTS `table1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fint` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=3 ;

INSERT INTO `table1` (`id`, `fint`) VALUES
(1, 1),
(2, 2);


حالا دوتا فایل ایجاد میکنیم با نامهای مختلف (هر چند با نام یکسان هم نمی تونید ایجاد کنید ) مثلا نام یک فایل رو index1 و دیگری رو index2 قرار میدیم

محتوای فایل index1 :


$r=mysqli_query('SELECT * from table1 where id=1 FOR UPDATE');
$a=mysql_fetch_assoc($r);

echo '<h3>'.$a['fint'].' - '.time().'</h3>';

//ایجاد تاخیر برای تست
sleep(10);

//افزایش یک واحد
$a['fint']=$a['fint']+1;

mysqli_query('UPDATE table1 SET fint='.$a['fint'].' where id=1');

echo '<h3>'.$a['fint'].' - '.time().'</h3>';



حالا محتوای فایل index2 :

$r=mysqli_query('SELECT * from table1 where id=2 FOR UPDATE');
$a=mysql_fetch_assoc($r);

echo '<h3>'.$a['fint'].' - '.time().'</h3>';

//ایجاد تاخیر برای تست
sleep(10);

//افزایش یک واحد
$a['fint']=$a['fint']+1;

mysqli_query('UPDATE table1 SET fint='.$a['fint'].' where id=2');

echo '<h3>'.$a['fint'].' - '.time().'</h3>';



خب حالا توی مرورگر یه تا صفحه باز میکنیم و توی دوتای اولی فایل index1 رو تایپ میکنیم و توی سومی آدرس index2 رو تایپ میکنیم و همزمان هر سه تا رو میزنیم که اجرا بشه (هر چند هیچ جوری نمیشه همزمان اجراشون کرد :خجالت: ولی تاخیر یک تا دو ثانیه ای هم ایجاد بشه مهم نیست مهم اینه که هر سه صفحه اجرا بشه )

خب نتیجه رو اگه توی خود مرورگر هم نگاه کنید واضح می بینید
Index2 با یکی از index1 ها با اختلاف کمتری خاتمه پیدا میکنه ولی index1 دومی با اختلاف 10 ثانیه ای اجرا میشه

و نتیجه به صورت زیر توی صفحه مرورگر ها دیده میشه (نتیجه تست من )

index1:
0 - 14283344831 - 1428334493

index2:
0 - 14283344831 - 1428334494
index1:
1 - 14283344942 - 1428334504

با اولین نگاه متوجه میشین که توی Index1 رکورد قفل شده و index2 هم همزمان با Index1 اجرا شده ولی index1 دومی بعد از اتمام کار index1 اولی شروع شده

اون 0 و 1 و 2 هم مقدارها هستن که نشون میدن مقدارها درست آبدیت شدن


لازم به ذکر هستش که این تست چه با تراکنش چه بدون تراکنش نتیجه مشابهی رو میده

nazanin_asadi_1
دوشنبه 17 فروردین 1394, 20:01 عصر
اینم اضافه کنم که قضیه تراکنش با قفل گذاری کلا جدا هستش

تراکنش یعنی یا همه یا هیچ کدوم
وقتی میخوایم یه سری کارها که به هم وصل هستن رو انجام بدیم به این صورت که یا همشون باید انجام بشن یا هیچ کدوم نباید انجام بشه میاییم از تراکنش استفاده میکنیم

مثلا همین چک و بانک رو در نظر بگیرین
وقتی چک پاس میشه باید از موجودی هم کسر بشه
معنی نداره که چک پاس بشه ولی از حساب کسر نشه

ممکنه براتون سوال پیش بیاد که چه وقت ممکنه همچین اتفاقی بی افته که چک رو پاس کنیم ولی از حساب کسر نشه
خب این برمیگرده به این که بریم از نزدیک مسئله رو نگاه کنیم (خیلی نزدیک دیگه نه یه خرده فاصله بگیرین :قهقهه:)

پروسه پاس کردن چک رو اینجوری در نظر بگیرین
1- آبدیت وضعیت چک به پاس شد
2- کسر مبلغ از حساب مشتری

حالا برنامه میاد 1 رو اجرا میکنه تا بخواد 2 رو اجرا کنه برق میره (قهر میکنه یا اتصال میکنه یا هنگ میکنه خلاصه یه اتفاقی می افته دیگه )

حالا سیستم دوباره بالا می یاد و به کار خودش ادامه میده و یک چک در وضعیت پاس شد داریم ولی از حساب مشتری کسر نشده قضیه چیه ؟ کی ضرر کرده ؟

اینجاست که میگن یا همه یا هیچ کدوم یعنی یا هر دو باید اجرا بشه یا هیچ کدوم نباید اجرا بشه
میاییم اینجا از تراکنش (دست سازنده و طراحش درد نکنه ) استفاده میکنیم

به زبان ساده میگیم

START TRANSACTION
1- آبدیت وضعیت چک به پاس شد
2- کسر مبلغ از حساب مشتری
COMMIT

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

یعنی درسته از تراکنش استفاده کردیم ولی هنوز اون مشکل برداشت همزمان از حسابی که موجودیش کمتر از جمع دو مبلغ هستش رو داریم

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

حالا وقتی مشتری با اولین درخواست قفل شده درخواست های دیگه ای که برای این مشتری ممکنه رخ بده عقب می افته و منتظر آزاد شدن قفل مشتری میشه


اینجا دیگه این مشکل هم خیلی راحت حل میشه و ما یه برنامه نویس موفق متظاهر میشیم :خجالت:



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

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

freeman99
دوشنبه 17 فروردین 1394, 20:44 عصر
حال اگر خط زیر را از حالت comment خارج کنید خروجی اون چیزی که فکر می کردم نیست


# mysqli_query($con,'select `counter` from `test` WHERE `id` = 1 for update');

نگاه کنید ما در کوئری بالا فقط رکوردی که شناسه اش 1 است را قفل کردیم ولی بقیه سطر ها نباید قفل شوند.

پس چرا با اجرای کوئری اول قفل بر روی کوئری دوم نیز اعمال می شود ؟ در صورتی که در کوئری دوم دستور for update استفاده نشده است.

خب کد درخواست اول میاد و باعث میشه یک قفل write روی رکورد مورد نظر گرفته بشه، بعد هنوز قفل آزاد نشده شما میای و یک درخواست دیگه ارسال میکنی (یک صفحه جدید با همون آدرس باز میکنی)، خب اون هم میاد اول از همه دستور select `counter` from `test` WHERE `id` = 1 for update رو اجرا بکنه، ولی چون اجرای این دستور مستلزم گرفتن یک قفل روی رکورد مورد نظر هست و درخواست قبلی از قبل یک قفل روی اون رکورد داره، بنابراین کوئری درخواست دوم معلق/منتظر آزاد شدن قفل رکورد مورد نظر میمونه و پشت این دستور گیر میکنه، چون دو درخواست نمیتونن همزمان روی یک رکورد یکسان قفل write داشته باشن.

البته اینو بگم که درخواست دوم حتی اگر for update هم نداشته باشه بازم پشت کوئری منتظر میمونه، چون قفل write اجازه نمیده درخواستهای دیگر حتی بتونن بخونن و حتی select بدون قفل بزنن.

حالا شما میتونی کوئری مربوط به رکوردی رو که در درخواست اول قفل میشه از درخواست دوم حذف کنی؛ به این شکل میبینی که درخواست دوم دیگه بلاک نمیشه و منتظر تموم شدن درخواست اول نمیمونه. یا اینکه میتونی کوئری های مربوط به رکورد قفل شده رو ببری بعنوان آخرین کوئری ها و دستورات برنامه بذاری، که به این شکل تا قبل از اون هر دستور و کوئری دیگری باشه که به رکورد قفل شده مربوط نشن اجرا میشن.

بهرحال PHP دستورات رو ترتیبی اجرا میکنه. تا دستور اول (کوئری اول) اجرا نشده که نمیره سراغ بقیهء خطوط برنامه! اون کوئری هم معلق و منتظر آزاد شدن قفل میمونه و اجرای کدهای PHP تا وقتی اجرای تابع mysqli_query تموم نشه متوقف میمونه طبیعتا.

abolfazl-z
چهارشنبه 20 خرداد 1394, 12:07 عصر
Note

Locking of rows for update using SELECT FOR UPDATE only applies when autocommit is disabled (either by beginning transaction with START TRANSACTION or by setting autocommit to 0. If autocommit is enabled, the rows matching the specification are not locked.

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html