بنناجح
المرحلة 2: الآليات · الدرس 3 من 4 · 35 دقيقة

MVCC و Snapshot Isolation

لو كل قراية لازم تستنى كل كتابة، أي تقرير تقيل هيوقف الموقع. الـ MVCC بيحل ده بفكرة بسيطة: متعدّلش الصف، اعمل نسخة جديدة منه، وكل ترانزاكشن تشوف النسخة اللي تناسب وقتها. الدرس ده بيشرح الـ MVCC من جوه في PostgreSQL و InnoDB، وتمنه، والـ Snapshot Isolation اللي مبني عليه.

في الدرس ده هتتعلم
  • تفهم ليه في الـ MVCC القراية مش بتستنى الكتابة والعكس
  • تعرف إزاي PostgreSQL بيخزن النسخ (xmin و xmax) وإزاي InnoDB بيخزنها (Undo Log)
  • تفهم الـ VACUUM والـ Purge، وليه الترانزاكشن الطويلة خطر
  • تعرّف Snapshot Isolation و First-Updater-Wins
  • تفرّق بين سلوك PostgreSQL و MySQL في REPEATABLE READ لما اتنين يعدّلوا نفس الصف

1. الفكرة: نسخ كتير بدل Lock

في نظام Locks بس، T1 بتعدّل صف (ماسكة X)، و T2 عايزة تقراه (محتاجة S): T2 تستنى. تقرير بيقرا الجدول كله ياخد S على كل صف، وأي UPDATE يستنى التقرير يخلص.

القاعدة الذهبية للـ MVCC:

قراية كتابة
قراية مش بيستنوا مش بيستنوا (القراية بتشوف النسخة القديمة)
كتابة مش بيستنوا بيستنوا (لسه فيه Row Lock)

يعني الـ MVCC مش بيلغي الـ Locks، بيلغي الحاجة لـ Locks في القراية العادية. كتابتين على نفس الصف لسه لازم واحدة تستنى التانية.

في الـ MVCC اللي بيقرا مش بيقفل، واللي بيكتب مش بيوقف اللي بيقرا. الخناقة بس بين اتنين بيكتبوا نفس الصف.

2. MVCC في PostgreSQL: كل UPDATE صف جديد

في PostgreSQL، كل نسخة من الصف (اسمها Tuple) متخزنة كصف فعلي في الجدول، وفي الـ Header بتاعها رقمين:

  • xmin: رقم الترانزاكشن (XID) اللي عملت النسخة دي (بالـ INSERT أو UPDATE).
  • xmax: رقم الترانزاكشن اللي مسحتها (بالـ DELETE أو UPDATE). صفر لو لسه محدش مسحها.

الـ UPDATE في PostgreSQL = DELETE للنسخة القديمة + INSERT لنسخة جديدة. والنسخة القديمة مش بتتمسح فعلاً، بيتحط عليها xmax بس.

تقدر تشوفهم بنفسك
SELECT xmin, xmax, id, stock FROM products WHERE id = 7;
--  xmin | xmax | id | stock
-- ------+------+----+-------
--   900 |    0 |  7 |    10

BEGIN;                                   -- الترانزاكشن دي رقمها 950
UPDATE products SET stock = 9 WHERE id = 7;
COMMIT;

بعد الـ UPDATE، الجدول فيه نسختين فعلاً على الـ Disk:

النسخة xmin xmax stock مين يشوفها
القديمة 900 950 10 الترانزاكشنز اللي الـ Snapshot بتاعتها قبل 950
الجديدة 950 0 9 الترانزاكشنز اللي الـ Snapshot بتاعتها بعد COMMIT 950

قاعدة الرؤية (مبسطة): النسخة ظاهرة ليك لو:

  1. الـ xmin بتاعها ترانزاكشن عملت COMMIT قبل الـ Snapshot بتاعتك (أو إنت نفسك)، و
  2. الـ xmax صفر، أو ترانزاكشن عملت ABORT، أو لسه ماعملتش COMMIT وقت الـ Snapshot بتاعتك.

يعني كل ترانزاكشن، وهي بتمشي على الجدول، بتختار من كل صف النسخة الوحيدة اللي تناسب الـ Snapshot بتاعتها.

3. تمن الـ MVCC في PostgreSQL: الـ VACUUM

النسخ القديمة (Dead Tuples) مش بتتمسح لوحدها، لأن ممكن لسه فيه ترانزاكشن قديمة محتاجاها. اللي بيمسحها هو VACUUM: بيلف على الجداول، وأي نسخة محدش من الترانزاكشنز الشغالة يقدر يشوفها بيعلّم مكانها فاضي عشان يتستخدم تاني. والـ Autovacuum بيشغّله لوحده في الخلفية.

وفيه تحسين اسمه HOT (Heap-Only Tuple): لو الـ UPDATE ماغيّرش أي عمود عليه Index وفيه مكان في نفس الـ Page، النسخة الجديدة بتتحط في نفس الـ Page والـ Indexes مش بتتلمس.

4. MVCC في InnoDB: الصف واحد والتاريخ في الـ Undo Log

InnoDB عامل العكس: الصف في الـ Clustered Index فيه آخر نسخة بس، والنسخ القديمة متخزنة كفروقات في الـ Undo Log.

كل صف فيه أعمدة مخفية:

  • DB_TRX_ID (6 bytes): آخر ترانزاكشن عدّلت الصف.
  • DB_ROLL_PTR (7 bytes): مؤشر لسجل في الـ Undo Log فيه النسخة اللي قبلها، واللي قبلها فيه مؤشر للأقدم، وهكذا: سلسلة نسخ.

والتنضيف هنا اسمه Purge: Thread في الخلفية بيمسح سجلات الـ Undo اللي مفيش Read View محتاجاها. ونفس المشكلة: ترانزاكشن طويلة توقف الـ Purge، والـ Undo يكبر، وكل قراية لازم تمشي سلسلة أطول. مؤشرها في SHOW ENGINE INNODB STATUS اسمه History list length: لو بيزيد بالآلاف باستمرار، دوّر على ترانزاكشن مفتوحة.

PostgreSQL InnoDB
النسخة الجديدة فين صف جديد في الجدول نفسه الصف بيتعدّل مكانه، والقديمة في الـ Undo Log
القراية للنسخة القديمة النسخة موجودة جاهزة في الجدول بتتبني من الـ Undo (أبطأ كل ما السلسلة تطول)
التنضيف VACUUM / Autovacuum Purge thread
عرض المشكلة Bloat في الجداول والـ Indexes History list length كبير، والـ Undo tablespace بيكبر
الـ ROLLBACK سريع جداً (النسخة الجديدة بتتساب Dead) بطيء (لازم يطبّق الـ Undo)

الـ MVCC بيشتري قراية من غير استنا بنسخ قديمة لازم حد ينضفها. والترانزاكشن الطويلة بتمنع التنضيف في الداتابيز كلها، مش في جدولها بس.

5. Snapshot Isolation

SI بتمنع: Dirty Read، و Non-repeatable Read، و Phantoms في القراية، و Read Skew، و Lost Update. ومش بتمنع: Write Skew (شفناها في درس الـ Anomalies: كل واحد كتب صف مختلف، فمفيش تعارض كتابة يتشاف).

إمتى الـ Snapshot بتتاخد؟

READ COMMITTED REPEATABLE READ
PostgreSQL Snapshot جديدة مع كل جملة Snapshot واحدة من أول جملة في الترانزاكشن
MySQL (InnoDB) Read View جديدة مع كل قراية عادية Read View واحدة من أول قراية عادية (أو START TRANSACTION WITH CONSISTENT SNAPSHOT)

6. لما اتنين يعدّلوا نفس الصف: PostgreSQL ضد MySQL

هنا أهم فرق عملي بين الداتابيزين، وأكتر حاجة بتلخبط الناس. نفس السيناريو: Read-Modify-Write على المخزون، والاتنين على REPEATABLE READ.

نفس الكود على داتابيزينجرّب بنفسك
في الداتابيزstock10
T1⏱T2
  1. SELECT stock FROM products WHERE id = 7;101
  2. 2SELECT stock FROM products WHERE id = 7;10
  3. UPDATE products SET stock = 9 WHERE id = 7; COMMIT;تم3
  4. 4UPDATE products SET stock = 9 WHERE id = 7;ERROR 40001
  5. 5ROLLBACK; -- وبعدين Retry من الأولتقرا 9 وتكتب 8
▶

دوس ▶ شغّل، أو امشي خطوة خطوة بالأسهم.

والـ Current Read في InnoDB بيعمل مفاجأة غريبة كمان: جوه REPEATABLE READ، لو عملت UPDATE ... WHERE status = 'pending'، هيعدّل صفوف اتضافت بعد الـ Snapshot (لأنه بيقرا آخر نسخة). وبعد كده لو عملت SELECT عادي، هتشوف الصفوف دي لأنك إنت اللي عدّلتها، رغم إنك ماكنتش شايفها قبل الـ UPDATE. فالـ “Snapshot” في MySQL متسقة للقراية العادية بس، مش للكتابة.

PostgreSQL بيحمي الـ Read-Modify-Write في REPEATABLE READ بـ Error لازم تعمل عليه Retry. MySQL مش بيحميه خالص، وبيكتب على آخر قيمة. في الاتنين: الحسبة الذرية في الـ UPDATE أو FOR UPDATE هما الحل المضمون.

7. أسئلة الإنترفيو

فكّر في إجابتك الأول، وبعدين افتح السؤال.

01إزاي الـ MVCC بيخلّي القراية ماتستناش الكتابة؟

الكتابة مش بتمسح القيمة القديمة، بتعمل نسخة جديدة. والقراية بتشوف النسخة اللي كانت متثبتة في لحظة الـ Snapshot بتاعتها، فمش محتاجة Lock ومش بتستنى. بس كتابتين على نفس الصف لسه بيستنوا بعض بالـ Row Lock.

02الفرق بين تطبيق الـ MVCC في PostgreSQL و InnoDB؟

PostgreSQL بيعمل صف جديد في الجدول مع كل UPDATE، وكل نسخة فيها xmin و xmax، والقديمة بتتمسح بالـ VACUUM. InnoDB بيعدّل الصف مكانه ويحط النسخة القديمة كفرق في الـ Undo Log، والقراية القديمة بتبني النسخة بالمشي على DB_ROLL_PTR، والتنضيف بالـ Purge.

03ليه الترانزاكشن الطويلة خطيرة في نظام MVCC؟

لأن التنضيف (VACUUM أو Purge) مش يقدر يمسح أي نسخة ممكن أقدم Snapshot لسه محتاجاها. ترانزاكشن واحدة مفتوحة ساعات بتخلي النسخ القديمة تتراكم في كل الجداول: Bloat في PostgreSQL، و History list length كبير في InnoDB، والكويريز تبطأ.

04Snapshot Isolation بتمنع إيه ومش بتمنع إيه؟

بتمنع Dirty Read و Non-repeatable Read و Phantoms في القراية و Read Skew و Lost Update (بـ First-Updater-Wins). ومش بتمنع Write Skew، لأنها بتكشف تعارض الكتابة على نفس الصف بس.

05نفس كود Read-Modify-Write على REPEATABLE READ: إيه اللي بيحصل في PostgreSQL وإيه في MySQL؟

PostgreSQL: الترانزاكشن التانية اللي تعدّل صف اتعدّل بعد الـ Snapshot بتاعتها بتاخد Error 40001 (could not serialize access due to concurrent update)، ولازم Retry. MySQL: الـ UPDATE بيعمل Current Read على آخر نسخة ويكتب من غير Error، فالـ Lost Update بتعدّي. الحل في MySQL: FOR UPDATE أو UPDATE ذري أو version column.

علّم عليه لما تقدر تجاوب أسئلة الإنترفيو من غير ما تبص.