به نام خدا
وقتی میخوایم برای سیستمی که حسابداری هم داره طراحی دیتابیس انجام بدیم یکی از انتخابهایی که باید بکنیم، اینه که متغیرهای حسابداری رو در یک موجودیت مستقل قرار بدیم یا روی موجودیتهای اصلی سوار کنیم. در این پست میخوایم با نگاه به وضعیت فعلی همیان بررسی کنیم که کدوم میتونه بهتر باشه.
فرض کنید الآن میخوایم یک دیتابیس حداقلی برای همیان طراحی کنیم. همونطور که قبلاً گفتیم همیان یه جاییه که آدمها توش تشکیل یک سری گروه مالی (صندوق قرضالحسنه) میدن و سپردهی مشترک ایجاد میکنن. پس میشه گفت این موارد رو نیاز داریم:
در پایگاهداده، جدول بالا معمولاً به این شکل ذخیره نمیشه و از یک جدول میانی استفاده میکنن که رابطهی «عضویت یک عضو در صندوق» رو نشون میده. این جدول در همیان اسمش هست Poolship (ترکیبی از Membership و Moneypool). هر سطر در این جدول یک رابطهی عضویت رو نشون میده و مثلاً برای هر کدوم از ۵ عضو صندوق همیانستان، یک سطر در این جدول قرار میگیره.
حالا غیر از نام صندوق و اعضای اون چی مهمه؟ یکی از چیزهایی که خیلی مهمه، موجودی صندوقه. در این پست میخوایم به این بپردازیم که «موجودی صندوق» رو در پایگاه داده بهتره کجا قرار بدیم.
اولین جایی که برای موجودیِ صندوق به ذهن میرسه، اینه که روی خودِ صندوق قرارش بدیم. به هر حال «موجودی» یک ویژگی برای خود صندوقه دیگه. نه؟
در خیلی از موارد ممکنه این الگو کار کنه و مشکلی هم ایجاد نکنه. اما این طراحی محدودیتهایی رو ایجاد میکنه که میخوایم به یکی از مهمهاش اشاره کنیم:
دموکریت، دانشمند یونانی اولین بار واژهی «اتم» رو به معنای «تجزیهناپذیر» استفاده کرد. اون گفت که اگه شروع کنیم چیزها رو هی بشکونیم و جلوتر بریم، توی یک نقطه به چیزی به نام «اتم» میرسیم که دیگه اونو نمیتونیم به ذرات کوچکتر بشکونیم و اتم چیزیه که غیرقابل شکستهشدن به اجزای کوچکتر باشه.
بعضیوقتها ما هم نیاز داریم با مجموعهای از کارها به صورت یک «اتم» رفتار کنیم. برای مثال فرض کنید یک تراکنش انتقال پول داریم که در اون، از حساب من ۱۰۰۰ تومن کسر میشه و ۱۰۰۰ تومن باید به حساب مجتبی واریز بشه. این تراکنش در واقع دو بخش داره:
اما یا باید کل این تراکنش اجرا بشه و یا هیچ بخش از اون اجرا نشه و حالت وسطی نباید داشته باشیم. مثلاً فرض کنید بخش ۱ انجام میشه ولی قبل از انجام بخش ۲، برق سرور قطع میشه و انجام نمیشه. در این صورت «اصل پایستگی ارزش» نقض شده و پول تو سیستم غیب شده (از محمد کم شده ولی به هیچجا نرفته و جابجایی صورت نگرفته، بلکه پول از بین رفته).
این امکان خوشبختانه در دیتابیسها وجود داره که ما چندتا چیز رو با هم یکی کنیم و به عنوان Atom با اونها رفتار کنیم. در این شرایط، یا کل تراکنش انجام میشه و یا کلش انجام نمیشه. در نتیجه نیازی نیست نگران باشیم که پول توی سیستممون ظاهر و یا غیب بشه. حتی اگه سرور وسط کار خاموش بشه، به محض روشن شدن عملیات رو عقبگرد (rollback) میکنه و پول به حساب محمد برگشت داده میشه.
در حالتی که تراکنشمون اتمی باشه، همهی عملیات نگه داشته میشه و کلش با یک حرکت در دیتابیس مینشینه. یعنی هیچ زمانی وجود نداره که در دیتابیس حالت زیر رخ بده:
تراکنش اتمی خیلی خوبه و تضمین میکنه که کل کارمون یا انجام بشه و یا انجام نشه. این خوبه ولی کافی نیست.
در کامپیوتر (مثلاً در درس سیستم عامل) مفهومی داریم به نام «بخش بحرانی» که توضیح ویکیپدیاش خوبه:
در برنامه نویسی همروندی، دسترسیهای هم روندی به منابع مشترک ممکن است منجر به رفتار غیر قابل پیش بینی یا خطا شود. بنابر این بخش هایی از برنامه که در آن منبع مشترک مورد دسترس قرار میگیرد باید به گونهای حفظ شود که از دسترسی همزمان پیشگیری شود. این ناحیه محافظت شده همان بخش بحرانی یا منطقه بحرانی نام دارد. این ناحیه را نمیتوان توسط بیش از یک پروسه در هر زمان اجرا کرد. به طور معمول بخش بحرانی به یک منبع مشترک نظیر یک ساختمان داده، یک ابزار محیطی، یا یک اتصال شبکه دسترسی پیدا می کند، این منبع مشترک در صورت چندین دسترسی همزمان به درستی کار نخواهد کرد.
اینا یعنی چی؟ با یک مثال بررسی کنیم: فرض کنیم محمد درخواست انتقال ۱۰۰۰ تومن پول رو به حساب مجتبی انجام میده. در این حالت سیستم اول باید ببینه آیا محمد ۱۰۰۰ تومن پول داره که این انتقال رو انجام بده؟ اگه داشت بعدش طی یک عملیات اتمی، این مبلغ رو از حساب محمد کم کنه و همینقدر به حساب مجتبی اضافه کنه. شبهکدش به شکل زیر میشه:
if mohammad account balance > 1000 tomans: atomicly do: decrease mohammad account balance by 1000 tomans increase mohammad account balance by 1000 tomans
همهچیز در شبهکد بالا به نظر خوب میرسه، تا اینکه بحث همزمانی رخ بده. اگه نرمافزار ما فقط روی یک ریسه در حال اجرا باشه مشکلی نیست. ولی در محیطهای عملیاتی از همروندی (multi-threading)ّ و چندین ریسه استفاده میشه. در این حالت نرمافزار ما داره روی چند سرور اجرا میشه و هر کدوم از اونها ممکنه مستقلاً درخواست انتقال رو دریافت بکنن. در نتیجه ممکنه دو سرور به صورت کاملاً همزمان سعی کنن تیکهکد بالا رو اجرا کنن.
حالا چه مشکلی ممکنه رخ بده؟ این مشکل:
به این باگ میگن باگ race condition که یکی از سختترین باگها برای پیداکردن و رفعه.
اتفاقی که میافته اینه که ۲۰۰۰ تومن از حساب محمد کسر میشه و ۲۰۰۰ تومن به حساب مجتبی اضافه میشه. ولی ممکنه بالانس حساب محمد کلاً ۱۰۰۰ تومن باشه و در این حالت حساب محمد منفی میشه و در واقع بیش از داراییش خرج میکنه.
این مشکل از اونجایی ریشه گرفت که هنگام عملیات حسابداری، موجودیت مورد حسابداری رو قفل نکردیم. وقتی داریم روی «موجودی حساب محمد» یک کاری انجام میدیم، این موجودیت باید قفل بشه و فقط توسط یک سرور یا ریسه قابل دسترسی باشه و بقیه باید منتظر باشن که عملیات حسابداری سرور اول با «موجودی حساب محمد» تمام بشه و بعد بتونن دسترسی به این مقدار پیدا بکنن.
اگر بخوایم کد بالا رو طوری بنویسیم که درست کار کنه، باید اینطوری تغییرش بدیم:
lock mohammad account balance if mohammad account balance > 1000 tomans: atomicly do: decrease mohammad account balance by 1000 tomans increase mohammad account balance by 1000 tomans unlock mohammad account balance
در این حالت، سرور اول که میخواد عملیات حسابداری انجام بده، موجودی حساب رو قفل میکنه و سرور دوم نمیتونه هیچکاری بکنه تا کارهای سرور اول تموم بشه. وقتی سرور اول کارش تموم شد دیگه عملیات حسابداری تموم شده و شرط رو تازه بررسی میکنه و میبینه که پول در حساب محمد نیست و در نتیجه عملیات رو انجام نمیده و ارور برمیگردونه.
حالا که گفتیم باید در حین عملیات حسابداری، موجودیتِ موردِ حسابداری باید قفل بشه، لازمه ببینیم این موجودیت رو باید جدا کنیم یا روی خود موجودیت اصلی قرار بدیم. انتخاب بین این دوتاست:
اگه بخوایم با رویکرد ۱ بریم جلو، باید کل صندوق رو در حین عملیات حسابداری قفل کنیم. این شامل اسم صندوق و اعضای اون هم میشه.
خسته شدم. بقیهشو بعداً مینویسم. قرار بود از اولش همینو بگم ولی مجبور شدم کلی مفهوم دیگه رو توضیح بدم اول :|.