Mohammad Teimori Pabandi
Mohammad Teimori Pabandi
خواندن ۷ دقیقه·۱ سال پیش

راهبردهای طراحی نرم‌افزار حسابداری با تمرکز بر «همیان» - بخش سوم: جدا کردن موجودیت حسابداری

به نام خدا

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

طراحی جداول همیان

فرض کنید الآن می‌خوایم یک دیتابیس حداقلی برای همیان طراحی کنیم. همون‌طور که قبلاً گفتیم همیان یه جاییه که آدم‌ها توش تشکیل یک سری گروه مالی (صندوق قرض‌الحسنه) می‌دن و سپرده‌ی مشترک ایجاد می‌کنن. پس می‌شه گفت این موارد رو نیاز داریم:

  • Moneypool: صندوق قرض‌الحسنه
  • User: یک کاربر در اپلیکیشن همیان
یک طرح فرضی برای صندوق و رابطه‌ی آن با اعضا
یک طرح فرضی برای صندوق و رابطه‌ی آن با اعضا

در پایگاه‌داده، جدول بالا معمولاً به این شکل ذخیره نمی‌شه و از یک جدول میانی استفاده می‌کنن که رابطه‌ی «عضویت یک عضو در صندوق» رو نشون می‌ده. این جدول در همیان اسمش هست Poolship (ترکیبی از Membership و Moneypool). هر سطر در این جدول یک رابطه‌ی عضویت رو نشون می‌ده و مثلاً برای هر کدوم از ۵ عضو صندوق همیانستان، یک سطر در این جدول قرار می‌گیره.

جدول «عضویت در صندوق» در پایگاه‌داده
جدول «عضویت در صندوق» در پایگاه‌داده

حالا غیر از نام صندوق و اعضای اون چی مهمه؟ یکی از چیزهایی که خیلی مهمه، موجودی صندوقه. در این پست می‌خوایم به این بپردازیم که «موجودی صندوق» رو در پایگاه داده بهتره کجا قرار بدیم.

موجودی صندوق رو کجا بذاریم؟

اولین جایی که برای موجودیِ صندوق به ذهن می‌رسه، اینه که روی خودِ صندوق قرارش بدیم. به هر حال «موجودی» یک ویژگی برای خود صندوقه دیگه. نه؟

موجودی به عنوان یکی از ویژگی‌های صندوق
موجودی به عنوان یکی از ویژگی‌های صندوق

در خیلی از موارد ممکنه این الگو کار کنه و مشکلی هم ایجاد نکنه. اما این طراحی محدودیت‌هایی رو ایجاد می‌کنه که می‌خوایم به یکی از مهم‌هاش اشاره کنیم:

  • پیوندِ تنگاتنگ (tight coupling) اطلاعات حسابداری با اطلاعات صندوق: در اینجا اطلاعات صندوق با اطلاعات حسابداری در هم فرو رفتن و قابل تفکیک نیستن؛ برای مثال «نام صندوق» و «اعضا» دقیقاً در جدولی نگهداری می‌شن که «موجودی» توش هست. اما این چه مشکلی ایجاد می‌کنه؟

تراکنش اتمی

دموکریت، دانشمند یونانی اولین بار واژه‌ی «اتم» رو به معنای «تجزیه‌ناپذیر» استفاده کرد. اون گفت که اگه شروع کنیم چیزها رو هی بشکونیم و جلوتر بریم، توی یک نقطه به چیزی به نام «اتم» می‌رسیم که دیگه اونو نمی‌تونیم به ذرات کوچکتر بشکونیم و اتم چیزیه که غیرقابل شکسته‌شدن به اجزای کوچکتر باشه.

بعضی‌وقت‌ها ما هم نیاز داریم با مجموعه‌ای از کارها به صورت یک «اتم» رفتار کنیم. برای مثال فرض کنید یک تراکنش انتقال پول داریم که در اون، از حساب من ۱۰۰۰ تومن کسر می‌شه و ۱۰۰۰ تومن باید به حساب مجتبی واریز بشه. این تراکنش در واقع دو بخش داره:

  1. از حساب محمد ۱۰۰۰ تومن کم کن (بالانس جدید محمد = بالانس قبلی محمد منهای ۱۰۰۰)
  2. به حساب مجتبی ۱۰۰۰ تومن اضافه کن (بالانس جدید مجتبی = بالانس قبلی مجتبی به‌علاوه‌ی ۱۰۰۰)

اما یا باید کل این تراکنش اجرا بشه و یا هیچ بخش از اون اجرا نشه و حالت وسطی نباید داشته باشیم. مثلاً فرض کنید بخش ۱ انجام می‌شه ولی قبل از انجام بخش ۲، برق سرور قطع می‌شه و انجام نمی‌شه. در این صورت «اصل پایستگی ارزش» نقض شده و پول تو سیستم غیب شده (از محمد کم شده ولی به هیچ‌جا نرفته و جابجایی صورت نگرفته، بلکه پول از بین رفته).

این امکان خوشبختانه در دیتابیس‌ها وجود داره که ما چندتا چیز رو با هم یکی کنیم و به عنوان 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

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

قفل‌کردن و موقعیت موجودی حساب

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

  1. قرار دادن موجودی حساب توی جدول «صندوق قرض‌الحسنه» مثل مثال اولیه
  2. جدا کردن «حساب صندوق قرض‌الحسنه» و قرار دادنش روی یک جدول جدید

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

خسته شدم. بقیه‌شو بعداً می‌نویسم. قرار بود از اولش همینو بگم ولی مجبور شدم کلی مفهوم دیگه رو توضیح بدم اول :|.

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