کاربرد Lock/قفل در برنامه نویسی وب و دیتابیس

داشتم روی پروژه کار میکردم که یهو یادم افتاد ای داد بیداد دسترسی همزمان و نیاز به قفل کردن رو فراموش کردم!
گفتم کارم درآمد الان باید کلی کد دیگه و الگوریتم پیچیده پیاده کنم؛ ولی خوشبختانه فقط یک خط به دوتا فایل اضافه کردم و بنظرم نیازی نیست خیلی درگیر جزییات بشم.

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

بهتره با یه مثال عملی توضیح بدم.
فرضا من در بخشی از برنامهء خودم وقتی کاربر یک تلاش ناموفق برای لاگین داره این کوئری رو اجرا میکنم:

select * from `failed_logins` where `username`=$_username limit 1

بعد اگر رکوردی برای تلاشهای ناموفق اون کاربر در دیتابیس وجود نداشت، در چند خط بعد با کوئری زیر یک رکورد برای ذخیرهء تلاشهای ناموفق اون نام کاربری در جدول failed_logins درج میکنم:

insert into `failed_logins` (`username`, `times`, `pos`) values($_username, $times, $pos)

خب الان این چه مشکلی داره؟
مشکل اینه که ممکنه در فاصلهء زمانی بعد از اجرای کوئری select تا قبل از اجرای کوئری insert، یک رکورد مشابه (برای همین کاربر) توسط درخواست دیگری در دیتابیس درج شده باشه. و مسلما این یک باگ است!

خب راه حل چیه؟
قبل از اجرای کوئری select این کوئری رو اجرا میکنیم:

lock tables `failed_logins` write

این کوئری باعث میشه جدول failed_logins قفل بشه و تا وقتی اجرای پردازش فعلی ما تموم نشده (یا کانکشن به دیتابیس بسته نشده) یا با کوئری unlock tables قفل رو آزاد نکردیم، کوئری های پردازشهای دیگر که با جدول failed_logins کار دارن چه از نوع خواندن (select) و چه از نوع نوشتن (update، insert، …) در صف انتظار برای اجرا باقی بمونن تا کار ما تموم بشه.

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

——————-

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

ضمنا کل جدول رو بخاطر یک رکورد قفل کردن کار جالبی نیست چون در این مدت هیچ کلاینتی نمیتونه به هیچ بخشی از جدول که تداخلی با عملیات جاری هم نداره دسترسی داشته باشه و این میتونه در شرایطی که ترافیک سایت خیلی بالا باشه سرعت سایت رو پایین بیاره و مشکل عملی ایجاد کنه، اما چون این کد فقط نمونهء اولیه و الگوریتم کلی بود و انجین MyISAM مورد استفاده در جداول پروژهء بنده هم از قفل در سطح رکورد پشتیبانی نمیکنه بنده کل جدول رو قفل کردم. در مرحلهء بعد میخوام این کد رو تغییر بدم و با استفاده از تابع GET_LOCK یک قفل روی رکورد برای هر کاربر خاص ایجاد کنم. البته این یک نوع Advisory lock خواهد بود که در سطح اپلیکیشن ایجاد میشه (چون گفتم که خود MyISAM قفل در سطح رکورد نداره). حالا اینکه Advisory lock چی هست و غیره بحث دیگریست. بنده در این تاپیک فقط میخواستم بگم در خیلی جاها و شرایط مسئلهء دسترسی همزمان وجود داره و باید براش راهکار پیاده بشه.
علت خیلی از باگهایی که کشف منشاء اونها بسیار دشوار خواهد بود همینطور چیزهاست. باگهایی که نتایج اونها گهگاه بصورت اسرارآمیزی رخ میدن.

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

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

—————————–

ضمنا یه موضوع مهم!

وقتی این دستور رو اجرا میکنید:

lock tables `table1` write

تا وقتی قفل گرفته شده آزاد نشده هیچ کلاینت/کانکشن دیگری نمیتونه در table1 بنویسه یا ازش بخونه.
چون شما دارید میگید من میخوام در این جدول بنویسم.
ضمنا کلاینت دیگری در این مدت نمیتونه هیچ قفلی رو چه read و چه write روی table1 بگیره.
به قفل write به این خاطر exclusive lock هم گفته میشه.

اما وقتی این دستور رو اجرا میکنید:

lock tables `table1` read

تا وقتی قفل گرفته شده آزاد نشده کسی نمیتونه در table1 بنویسه، کسی نمیتونه قفل از نوع write بگیره روی این جدول. اما هرکسی میتونه از table1 بخونه و حتی ممکنه قفل read خودش رو قبل از اینکه شما قفل read خودتون رو آزاد کنید روی این جدول بگیره. یعنی همزمان چند نفر میتونن روی یک جدول قفل read داشته باشن.
بخاطر همین به قفل read یک shared lock هم گفته میشه. یا شاید بهتره بگیم قفل read یک shared lock است.
تا وقتی تمام قفلهای read روی یک جدول آزاد نشن کسی نمیتونه روش قفل write بگیره.

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

——————————

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

این گفته ها رو فراموش نکنید، چون کمتر کتاب و کسی هست که این موارد پایه ای رو بصورت واضح و کامل بهتون بگه:

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

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

میشه گفت 99% برنامه نویسها از این مسائل یا آگاه نیستن یا تمهیدات لازم برای جلوگیری از عوارض اونا رو به هر دلیلی، بحد کافی و صحیح بکار نمیبرن. این مسئله ای رو عوض نمیکنه. صرفا ضعف برنامه نویسها رو میرسونه.

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

برای اینکه ذهنیت عمومی رو در این مورد روشن کنم مسئله رو بیشتر روشن میکنم.
فکر میکنم با یک مثال بشه مطلب رو راحتتر و سریعتر روشن کرد.

فرضا درخواست «الف» توسط سرور دریافت میشه.

درخواست الف شامل چنین عملیاتی است:

...
query1
...
query2
...

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

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

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

فرض کنید عملیات درخواست ب هم اینطوریه:

...
query3
...

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

خب حالا چطوری تداخل کوئری ها رخ میده؟

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

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

فرض کنید اجرای عملیات درخواست الف، شامل اجرای کوئری 1، و تا همونجاش (یعنی دقیقا تا وقتی کوئری 1 هم اجرا شد)، 0.15 ثانیه طول بکشه.
خب پس درخواست ب که یک دهم ثانیه دیرتر شروع شده الان ممکنه شروع به اجرا کرده باشه قبل از اینکه اجرای درخواست الف کامل شده باشه. بنابراین میتونیم فرض کنیم که دو درخواست الف و ب همزمان درحال اجرا هستن، با اینکه درخواست ب مدتی بعد از درخواست الف به سرور رسیده بود. در این لحظه، اجرای درخواست الف در نیمه های اونه و هنوز به اجرای کوئری 2 نرسیده، و درخواست ب تازه شروع کرده، اما درخواست ب میتونه به هزار و یک دلیل سریعتر کار کنه و قبل از اینکه درخواست الف کوئری شماره 2 خودش رو اجرا کنه، درخواست ب کوئری شماره 3 رو اجرا کنه.

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

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

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

1 دیدگاه در “کاربرد Lock/قفل در برنامه نویسی وب و دیتابیس

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

پاسخ دهید

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

*

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