دوستم امروز تماس گرفت و گفت که تو یکی از جدولهای دیتابیسش یک مشکل ایجاد شده. دنبال راهی بود که بتونه جلوی تکرار این مشکل رو بگیره. در این پست این مشکل که به اون مسئله مجموع نادرست گفته میشه و علت ایجادش رو توضیح میدم.
ساختار دیتابیس
دیتابیس در کنار جدول کاربرها (users) لیستی از تراکنشهای مالی اونها (transactions) رو نگهداری میکنه و در جدول سوم لیست پکیجهای خریداری شده (user_package) کاربرها رو (جدول پکیجها در این مسئله موضوعیتی ندارد).
کاربر تنها از موجودی سپردهای که پیش سایت داره امکان خرید داره. هر بار که روی دکمه خرید یک پکیج کلیک میکنه مراحل زیر انجام میشه:
۱- موجودی حساب کاربر از دیتابیس کوئری گرفته میشه (که از SUM مقادیر تراکنش روی جدول transaction به دست میاد)
۲- در صورتی که کاربر موجودی داشته باشه پکیج مورد نظر به کاربر نسبت داده میشه (در جدول user_package)
۳- موجودی از حساب کاربر کم میشه (یک سطر جدید در جدول transactions)
مشکل
در اتفاقی نادر برای یک کاربر که سپرده محدودی روی سایت داشت ۲ مورد خرید از ۱ پکیج ثبت شده بود و برای هر دو مورد مبلغ پکیج از کاربر کسر و موجودی کاربر منفی شده بود. این اتفاق در صورتی پیش اومده بود که هر ۳ مرحله بالا در یک تراکنش (database transaction) انجام میشد.
اما چطور چنین چیزی اتفاق میوفته؟ این مسئله ریشه در همزمانی (concurrency) برنامه و پایگاهداده داره. زمانی که [به هر دلیلی] دو درخواست خرید از سمت کاربر به صورت همزمان در سمت دیتابیس اجرا میشود امکان وقوع چنین مسئلهای وجود دارد.
البته سطح ایزولهسازی پایگاهداده هم در این زمینه موثره. تصویر چگونگی ایجاد این مسئله رو با توجه به مراحل ذکر شده بالا نشون میده:
زمانی که تراکنش ۲ در حال محاسبه موجودی کاربر است، تراکنش ۱ هنوز موجودی کاربر را کم نکرده. حتی در صورتی که بالانس کاربر توسط تراکنش ۱ به روز شده باشد اما تراکنش اعمال (commit) نشده باشه باز هم نتیجه موجودی در تراکنش ۲ اشتباه خواهد بود.
به این مسئله، مشکل مجموع نادرست (Incorrect summary problem) یا مسئله تحلیل متناقض (Inconsistent analysis) گفته میشه.
در پست بعدی راهحلهای این مسئله رو تشریح میکنم.