ویرگول
ورودثبت نام
حسن سعادت
حسن سعادتیه مهندس نرم افزار کنجکاو، عاشق حل مسئله و ساده کردن مفاهیم پیچیده
حسن سعادت
حسن سعادت
خواندن ۵ دقیقه·۵ ماه پیش

مفهومMVCC؛ قلب تپنده دیتابیس

MVCC یا Multi Version Concurrency Control یا کنترل همروندی چند نسخه ای یه روش برای افزایش همزمانی و کارایی در دیتابیس هایی مثل Postgres و MySQL و SQL Server استفاده میشه.

داستان از اونجایی شروع میشه که دو نفر میخواستن همزمان روی یه دیتابیس عملیات انجام بدن مثلا یکی میخواست بخونه یکی هم بنویسه. اون فردی که میخواست بنویسه بایستی دیتابیس رو قفل میکرد به یکی از سه روش Table Lock - Page Lock - Row Lock تا کارشو انجام بده و بعدش بقیه بتونن کارشونو انجام بدن.

ینی تا زمانی که نفر دوم کارش تموم نشده بود تو نمیتونستی دیتابیس یا بخشی ازش رو بخونی و این خودش باعث کاهش شدید همزمانی و کارایی میشد و باعث deadlock هم میشد.

روش MVCC اومد گفت از هر ردیف جدول چنتا نسخه نگه میداریم و هر Transaction یه snapshot از دیتابیس رو می بینه و باهاش کاراشو میکنه. ینی وقتی یه نفر داره مینویسه با اون اسنپ شاتی که اول تراکنش گرفته کاراشو میکنه و تا زمانی که commit نکرده بقیه اون تغییرات جدید رو نمیبینن. و اینطوری کسی بلاک نمیشه.

نکته جالب اینجاست که وقتی یه ردیف رو آپدیت می کنیم روی اون ردیف اعمال نمیشه و به جاش یه ردیف جدید درست میشه با شناسه اون تراکنش و ردیف قدیمی علامت میخوره تا بعدا توسط VACCUM دیلیت بشه (البته دیلیت نمیشن صرفا فضاشون برای نوشتن ردیف های جدید قابل استفاده میشه).

مثال:

توی این مثال الکس میخواد ۱۰۰ دلار بده به کریس، Postgres میاد برای هر کدوم یه row جدید درست می کنه.

عدد xmin نشون دهنده شناسه تراکنشیه که اون ردیف باهاش درست شده و xmax نشون دهنده شناسه تراکنشیه که اون ردیف رو دیلیت کرده.

نکته: اگه یه تراکنش دومی بخواد روی این دوتا ردیف کار کنه چه اتفاقی میفته؟ دیگه جا برای xmax نداریم که!

پس اون تراکنش بلاک میشه تا تکلیف تراکنش اول مشخص بشه!

نکته جالب:

دلیل کندی Count(*) هم توی Postgres همین MVCC هستش چون شمردن تعداد ردیف ها کار ساده ایه ولی وقتی MVCC استفاده میکنی بیشتر از تعدادی که باید ردیف وجود داره و باید یه for بندازی ببینی کدوماش قابل دیدنه و کدوماش قدیمیه که باید دیلیت بشه بعدا. Postgres میاد یه همچین if ای به ازای هر ردیف میزنه و counter رو یه دونه میبره بالا اگه ویزیبل بود!

این دوستمون توی ویدیوی یوتیوبش خیلی خوب این قضایا رو توضیح داده:

https://youtu.be/GtQueJe6xRQ?si=o4xQrHcQXYmsZS1q

قضیه VACCUM:

وکیوم میاد dead tuple ها رو علامت میزنه به عنوان اینکه میشه روی اون ردیف های مرده دیتای جدید رایت کرد. وکیوم فضای مصرف شده ردیف های مرده رو به دیسک برنمیگردونه! تنها دستور VACCUM FULL میتونه اینکارو کنه ولی این دستور بلاکینگ هستش و کل جدول رو لاک می کنه (EXCLUSIVE LOCK) و نمیذاره کسی روی جدول کاری بکنه ولی وکیوم خالی بلاکینگ نیست.

وکیوم به صورت دوره ای اجرا نمیشه بلکه به صورت event-based بر اساس یه سری threshold و با فرمول زیر فعال میشه که بهش میگیم auto vaccum:

num_dead_tuples > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * total_rows

مقدار autovaccum_vaccum_threshold میگه حداقل چقدر row داشته باشیم باید اتو وکیوم صورت بگیره. مثلا برای یه دونه ردیف لازم نیست انجام بشه و بیخوده.

وکیوم و وکیوم فول رو میشه به صورت دستی هم اجرا کرد.

قضیه VACCUM Freeze:

اتو وکیوم علاوه بر اینکه میاد dead tuple ها رو قابل استفاده مجدد می کنه یه کار دیگه هم میکنه و اون فریز کردن xid های خیلی قدیمیه (برای ردیف های زنده فقط) چون اون ردیف ها دیگه xmin نیاز ندارن داشته باشن صرفا یه چیزی مثل FrozenTransactionId =2 ینی xmin=2 (این رکورد برای همه تراکنش ها ویزیبله) براشون کفایت میکنه.

قضیه XID Wraparound:

عدد xid یا همون Transaction تایپش unit32 عه ینی یه عدد بدون علامت ۳۲ بیتیه که بازه اش از صفره تا دو به توان ۳۲ که میشه ۴ میلیارد و خورده ای. دیتابیس برای اینکه بدونه توی هر تراکنش کدوم row ها برای اون تراکنش قابل دیدن باشن و کدوما نباشن از یه مقایسه استفاده می کنه. اون میاد xid تراکنش فعلی رو با xmin اون row ها مقایسه می کنه اینجوری:

int32(xid1 - xid2) < 0 => xid1 is older than xid2

حالا نکته این چیه؟ حاصل مقایسه دوتا unit32 میتونه از دو به توان ۳۱ بیشتر باشه که دیگه توی int32 نمیگنجه چون بازه int32 = (-2^31, 2^31-1) هستش و از اونجا به بعد مقایسه ها اشتباه انجام میشه و دیتاهای جدید ممکنه از دست برن و دیتاهای قدیمی زنده بشن!

اینجاست که دیتابیس میاد و همیشه اختلاف xid فعلی و آخرین xid فریز نشده رو محاسبه می کنه که نزدیک به 2^31 نباشه و اگه نزدیک بود چندین بار وارنینگ میده میگه VACCUM کن! اگه گوش نکنی وقتی به عدد بحرانی(دو میلیارد و خورده ای) 2^31 - 1000000 برسه خودش دیگه xid جنریت نمیکنه و وقتی xid جدید نباشه هیچ عملیات INSERT/UPDATE/DELETE نمیتونه انجام بشه و فقط میتونی SELECT کنی و در واقع دیتابیس read-only میشه!

با وکیوم و وکیوم فریزهای منظم این اختلاف همیشه کم میمونه و خطری وجود نداره.

نکته اینجاست که در حالت نرمال(اجرای منظم وکیوم و فریز) وقتی xid به 2^32 ینی ۴ میلیارد و خورده ای میرسه دوباره صفر میشه و ادامه میده (۳ میشه در واقع چون ۰ و ۱ و ۲ رزرو شده ان!) و مشکلی هم پیش نمیاد و اتفاقا چیز ضروری هستش و هر یکبار صفر شدن این xid رو بهش میگن یه epoch که تعداد epoch هم توی یه متغیری ذخیره میشه.

نکته دیگه:

توی ورژن های قدیمی postgres مثل قبل از ورژن ۸.۱ اتو-وکیوم وجود نداشت و وکیوم و وکیوم فریز به صورت دستی باید انجام میشد و قضیه xid wraparound خیلی جدی بود و امکان data loss و shut down شدن دیتابیس وجود داشت. توی ورژن های جدید نهایتا read-only میشه.

postgrespostgresqlconcurrencysql server
۰
۰
حسن سعادت
حسن سعادت
یه مهندس نرم افزار کنجکاو، عاشق حل مسئله و ساده کردن مفاهیم پیچیده
شاید از این پست‌ها خوشتان بیاید