قسمت اول - Lost Update Problem
تجربه تا حد زیادی ثابت کرده که وقتی دارید یک API وب طراحی میکنین متدهایی که resource های موجود رو تغییر میدهن و دست کاری میکنن میتونن چالش های طراحی خیلی زیادی داشته باشن. دو تا از مهم ترین چالش های طراحی idempotency و concurrency control ( کنترل همروندی) هستن.
تو این پست من سعی میکنم با توجه به تجربه ای که تو طراحی API برای چند تا سرویس به دست آوردم و مطالعه ای که داشتم (که شرح اش رو توی لینک های داخل پست میدم) مختصری در مورد یکی از مشکلات concurrency control توضیح بدم.
تداخل concurrency که ما قصد داریم بررسی کنیم مربوط به زمانی هست که یک کاربر سعی داره resource ای رو تغییر بده که از زمانی که کاربر اطلاعات اون resource رو دریافت کرده تغییر کرده ( احتمالا توسط یک کاربر دیگه). مثلا فرض کنیم که علی و سارا به طور همزمان یک resource رو GET کنند که اطلاعات یک پست وبلاگ رو به ما برمیگردونه هم علی و هم سارا پست وبلاگ ما رو میبینن علی توی پست یک غلط املایی پیدا میکنه اون رو اصلاح میکنه و یک درخواست PUT برای اصلاح کردن دیتا به سرور ارسال میکنه. سارا هم در همون زمانها در حال خوندن پست هست و تصمیم میگیره که بخش هایی از پست وبلاگ رو آپدیت کنه و شروع میکنه به اضافه کردن یک متن به پست و بعد درخواست PUT ای رو به سرور میفرسته و متن بلاگ رو به روزرسانی میکنه در این جا سارا از تغییراتی که علی روی پست داده خبری نداره و بعد از انجام درخواست سارا تغییراتی که علی داده گم میشه و دیگه در دسترس نیست. به این مشکل Lost Update Problem گفته میشه. در این جا حتی اگر سارا میخواست مثلا فیلد دیگری مثل عنوان پست را هم عوض کنه به دلیل یکسان بودن endpoint این resource دچار همین تداخل میشه.
کم کردن احتمال ایجاد عواقب ای که انتظارش رو نداریم در زمانی که تداخلهای همروندی (concurrency conflicts) رخ میده روش ساده اما بعضی وقتا موثر برای کنترل شرایط همروندی هست میتونیم با یک سری کارها احتمال پیش آمدن این عواقب رو کم کنیم.مثلا در عمل میتونیم فقط فیلد هایی رو بهروزرسانی کنیم که کاربرنهایی قصد بهروزرسانی کردن آن ها را داره. تو مثال بالا اگر سارا فقط عنوان پست رو عوض میکرد ما میتونستیم از ایجاد عواقب ای که منتظرش نبودیم ( گم شدن تغییرات علی) جلوگیری کنیم. این روش احتمال ایجاد این تداخل ها رو کم میکنه. این روش رو میشه با استفاده از endpoint های متفاوت برای فیلد های مختلف یا استفاده از PATCH به جای PUT و ارسال فقط فیلد هایی که نیاز به بهروزرسانی دارن انجام داد.
اگه با توجه به طراحی امکان این وجود داشته باشه که به طور کلی تغییردادن و بهروزرسانی روی resource انجام نشه و بدون تغییر بمونه. تداخلهای هم روندی میتونن به طور کلی از API حذف بشوند.
اگه بخوایم یک استراتژی برای کنترل همروندی داشته باشیم optimistic locking میتونه بهترین انتخاب ما باشه. optimistic locking به این صورت هست که یک کار رو انجام بدی و بعد ببینی اگر resource از زمانی که آخرین بار دریافت شده تغییری داشته تغییرات رو رد و باطل کنی در غیر این صورت تغییرات بدون مشکل هستن. optimistic locking (قفلگذاری خوشبینانه) در برابر pessimistic locking (قفلگذاری بدبینانه) که توی اون یک resource قبل از اون که بهروزسانی بشه در برابر تغییر کردن قفلگذاری میشه و بعد از بهروزرسانی آزاد میشود قرار داره. با توجه به این که pessimistic locking نیاز به نگه داشتن state در سمت سرور دارد خیلی مناسب RESTful API ها نیست که در اون بهتر هست که سرور به صورت stateless نگه داشته شود.
روش کلی قفلگذاری خوشبینانه به این صورته که توی payload دیتا یا header ها از سمت سرور یک مقدار ارسال بشه که نشان دهنده نسخه ای از resource باشه که کاربر تغییر میدهه. اگر نسخه resource تغییر پیدا کرده باشه تغییراتی که کاربر ارسال کرده باید باطل بشه. به عنوان مثال نسخه میتونه یک عدد باشه یا hash مربوط به resource تغییر نیافته و باید با تغییر کردن resource این مقدار هم تغییر بکند.
میتونیم نسخه رو در زمان ارسال درخواست برای تغییردادن resource به عنوان بخشی از payload استفاده کنیم یا از If-Match در header درخواست استفاده کنیم که در مشخصات HTTP/1.1 معرفی شده به این صورت در زمان وجود تداخل status جواب برابر مقدار 412 (Precondition failed) خواهد بود.
همون طور که دیدین به عنوان مکانیسم کنترل همروندی قفلگذاری خوشبینانه یک روش ساده و قدرتمند هست و پیاده سازی اون سرراست و ساده است.
برای پیاده سازی این روش میتونیم از header Etag به عنوان شناسه نسخه تو گرفتن اطلاعات resource و header با کلید If-Match در ارسال درخواست استفاده کنیم تا به سرور این امکان رو بدیم که تصمیم بگیره (با استفاده از روشی که در بالا شرح داده شد)که resource باید بهروزرسانی بشه یا نه.
با این کار سرور میتونه به کاربرنهایی اطلاع بده که resource از زمانی که کاربر اطلاعات اون رو دریافت کرده تغییر کرده. سرور این کار را با ارسال Precondition Failed انجام میدهد که باعث میشه از ایجاد مشکل Lost Update Problem جلوگیری بشه و کلاینت میتونه با اطلاع دادن به کاربر نهایی (یا هر روش دیگری) با reload کردن مقادیر امکان دریافت تغییرات جدید رو به کاربر بده تا تغییرات لازم رو بده.
با بررسی کردن تعدادی از معروفترین API های عمومی مثل Github، Spotify و etcd میتونیم ببینیم که روش هایی که به کار میره بسیار مختلف هستند از روشهای سختگیرانه تا به کار نگرفتن هیچ روشی برای کنترل همروندی ( آخرین درخواست برنده میشه و تغییرات اون جایگزین میشن) رو میشه دید. روشی که بیشتر مرسوم هست استفاده از کنترل همروندی انتخابی (Opt-in) هست به عنوان مثال در API های Spotify میتونیم یک فیلد اختیاری snapshot_id رو ارسال کنیم در درخواست های تغییر تا سرور بتونه براساس اون تصمیم بگیره که تغییرات رو قبول یا رد بکنه. اما در صورتی که این فیلد رو ارسال نکنیم درخواست آخر برنده میشه و تغییراتش اعمال میشه. این نوع کنترل همروندی که اجباری نیست این امکان رو به استفاده کننده های API میده تا براساس نیازی که دارند از این امکان استفاده بکنند یا تصمیم بگیرند که درخواست آخر برنده باشد.
پ.ن:منابع این نوشتار برای مطالعه بیشتر (+++)