زندگی یه ردیف تو MVCC دیتابیس Turso
خلاصهٔ کاملتر
پره دیاز بو از تیم Turso تو این مقاله کل عمر یه ردیف رو تو حالت MVCC دنبال میکنه. MVCC یعنی نگه داشتن چند نسخه از هر ردیف تا تراکنشها بتونن همزمان کار کنن. این بخش تو Turso بر پایهی طراحی Hekaton ساخته شده. به گفتهی نویسنده، مقالهی Hekaton فقط ساختارهای داخل حافظه رو توضیح میده و دربارهی لاگ، بازیابی و checkpoint چیزی نمیگه. پس تیم مجبور شد این بخشها رو خودش طراحی کنه، طوری که فایل روی دیسک با SQLite سازگار بمونه.
ماجرا از بایتکد شروع میشه. SQLite و Turso هر کوئری رو به بایتکدی تبدیل میکنن که روی یه ماشین مجازی به اسم VDBE اجرا میشه. برای یه INSERT ساده، دستور Transaction تراکنش رو شروع میکنه، OpenWrite یه cursor (یعنی چیزی شبیه iterator روی یه جدول) باز میکنه، MakeRecord داده رو بستهبندی میکنه، Insert ردیف رو مینویسه و Halt تراکنش رو commit یا rollback میکنه.
تو حالت MVCC، دستور Insert ردیف رو تو یه SkipMap داخل حافظه میذاره. SkipMap یه ساختار مرتب و بدون قفل (lock-free) هست که هر rowid رو به زنجیرهای از نسخههای اون ردیف وصل میکنه. هر نسخه یه زمان شروع، یه زمان پایان و خود داده رو داره. تا وقتی تراکنش commit نشده، به جای timestamp شناسهی تراکنش (TxId) نوشته میشه تا بقیه نتونن اونو بخونن:
RowVersion {
begin: TxId(tx.id),
end: None,
row: record
}commit چند مرحله داره. اول یه timestamp گرفته میشه و تراکنش میره تو حالت Preparing. بعد چک میشه که تراکنش دیگهای بعد از شروع ما روی همون ردیف یا کلیدهای UNIQUE و PRIMARY KEY ننوشته باشه، چون هر کی زودتر commit کنه برندهست. بعدش یه رکورد تو logical log نوشته میشه و با fsync واقعاً روی دیسک میره. از این لحظه اگه برنامه کرش کنه، با replay کردن لاگ (یعنی دوباره اجرا کردن commitها به ترتیب) حافظه از نو ساخته میشه.
لاگ نمیتونه بینهایت بزرگ شه. وقتی به حدود ۴ مگابایت برسه (مقدار پیشفرض PRAGMA mvcc_checkpoint_threshold)، یه checkpoint از نوع Truncate اجرا میشه که ردیفها رو اول به فایل WAL و بعد به فایل اصلی B-tree منتقل میکنه. حالت Truncate جلوی تراکنشهای جدید رو میگیره، ولی یه حالت آزمایشی Passive هم هست که میذاره چند نویسنده همزمان جلو برن. نویسنده قبول داره این کار write amplification داره، ولی میگه در عوض سازگاری با SQLite و ایمنی در برابر کرش مجانی به دست میاد.
حذف هم شبیه اضافه کردنه. موقع DELETE فیلد end نسخه با TxId پر میشه و این کار زیر قفل زنجیره انجام میشه. پس اگه تراکنش دیگهای همزمان بخواد همون ردیف رو عوض کنه، خطای WriteWriteConflict میگیره و rollback میشه. به گفتهی نویسنده، دلیل اصلی این کار اینه که حالت WAL تو SQLite فقط یه نویسنده رو همزمان قبول میکنه، در حالی که agentها خیلی وقتا با هم روی یه دیتابیس مینویسن. برای امتحانش کافیه PRAGMA journal_mode = mvcc; رو اجرا کنی.
نکات کلیدی:
- MVCC تو Turso بر پایهی Hekaton ساخته شده، ولی لاگ و checkpoint رو خود تیم طراحی کرده.
- ردیفها تو یه SkipMap بدون قفل نگه داشته میشن و هر نسخه فیلدهای begin و end داره.
- تو commit هر کی زودتر commit کنه برندهست و چک روی rowid و کلیدهای UNIQUE و PRIMARY KEY انجام میشه.
- حد پیشفرض checkpoint حدود ۴ مگابایته، یعنی اندازهی ۱۰۰۰ فریم WAL.
- حالت Passive هنوز آزمایشیه و با فلگ --experimental-mvcc-passive-checkpoint فعال میشه.
- با PRAGMA journal_mode = mvcc; میشه MVCC رو روشن کرد.




