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

الـ Locks و Two-Phase Locking

أقدم وأشهر طريقة للـ Concurrency Control: قبل ما تلمس داتا اقفلها، واللي عايزها بعدك يستنى. بس القفل لوحده مش كفاية، والطريقة اللي بتقفل وتفك بيها هي اللي بتفرق. الدرس ده بيشرح الـ 2PL، وليه بيضمن Serializability، وإزاي الداتابيز بتقفل صفوف لسه مش موجودة.

في الدرس ده هتتعلم
  • تفرّق بين Shared و Exclusive Locks وتقرا جدول التوافق
  • تفهم Two-Phase Locking وليه بيضمن Serializability
  • تعرف الفرق بين Basic و Strict و Rigorous 2PL
  • تفهم الـ Intention Locks والـ Gap و Next-Key Locks في InnoDB
  • تستخدم FOR UPDATE و FOR SHARE و NOWAIT و SKIP LOCKED صح

1. Shared و Exclusive

فيه نوعين أساسيين:

  • Shared Lock (S): للقراية. كذا ترانزاكشن ممكن يمسكوا S على نفس العنصر في نفس الوقت، لأن القراية مش بتأثر على حد.
  • Exclusive Lock (X): للكتابة. ترانزاكشن واحدة بس تمسكه، ومحدش تاني يقدر يقرا (بـ Lock) ولا يكتب.
حد ماسك ↓ / إنت عايز ← S X
S ✓ اتفضل ✗ استنى
X ✗ استنى ✗ استنى

ده اسمه Compatibility Matrix، وكل أنواع الـ Locks اللي جاية ليها جدول زيه.

كذا حد يقرا مع بعض، بس اللي بيكتب لازم يبقى لوحده.

2. القفل لوحده مش كفاية

خلّينا نجرب قاعدة بسيطة: “اقفل العنصر قبل ما تستخدمه، وافتحه أول ما تخلص منه”.

الوقت T1: تحويل 100 من A لـ B T2: إجمالي الأرصدة
1 X(A) · A = A - 100 · unlock(A)
2 S(A) · read A · unlock(A)
3 S(B) · read B · unlock(B)
4 X(B) · B = B + 100 · unlock(B)

كل عملية اتعملت وهي ماسكة Lock صح، ومع ذلك T2 شافت A بعد الخصم و B قبل الإضافة: الإجمالي ناقص 100. السبب إن T1 فكت A وبعدين رجعت تقفل حاجة جديدة، فT2 دخلت في النص.

المشكلة مش في إنك تقفل، المشكلة في إنك تفك وبعدين ترجع تقفل.

3. Two-Phase Locking (2PL)

اللحظة اللي الترانزاكشن خدت فيها آخر Lock اسمها Lock Point.

النظرية (Eswaran وزمايله، 1976): لو كل الترانزاكشنز ماشية على 2PL، أي Schedule هيطلع Conflict Serializable، والترتيب Serial المكافئ هو ترتيب الـ Lock Points.

ليه؟ فكرة الإثبات: لو فيه سهم T1 → T2 في الـ Precedence Graph، يبقى T2 استنت Lock كان مع T1، يعني T1 فكت قبل ما T2 تاخد، يعني الـ Lock Point بتاع T1 قبل بتاع T2. لو فيه Cycle، يبقى T1 قبل T2 قبل … قبل T1، يعني T1 قبل نفسها، وده مستحيل.

في مثال التحويل: T1 تحت 2PL لازم تاخد X(A) و X(B) الاتنين قبل ما تفك أي واحد. فT2 هتشوف يا الاتنين قبل التحويل، يا الاتنين بعده.

آخر موبايل، بس المرة دي بالـ Locksجرّب بنفسك
في الداتابيزstock1orders0
أحمد بيشتري⏱سارة بتشتري
  1. BEGIN; SELECT stock FROM products WHERE id = 7 FOR UPDATE;stock = 1 · X lock1
  2. 2BEGIN; SELECT stock FROM products WHERE id = 7 FOR UPDATE;⏳ مستنية الـ Lock
  3. UPDATE products SET stock = 0 WHERE id = 7; INSERT INTO orders ...;1 row3
  4. COMMIT;الـ Locks اتفكت4
  5. 5-- الـ SELECT بيكمل دلوقتيstock = 0
  6. 6ROLLBACK;نفذ من المخزن
▶

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

2PL = خد كل اللي محتاجه قبل ما تسيب أي حاجة. ده كفاية يضمن Serializability.

4. Strict و Rigorous 2PL

الـ 2PL الأساسي بيسمح تفك Locks قبل الـ COMMIT (في الـ Shrinking Phase). المشكلة: لو فكيت X Lock على صف كتبته، حد تاني يقرا قيمتك، وبعدين إنت تعمل ABORT، يبقى Cascading Abort (شفناه في الدرس اللي فات).

النوع القاعدة النتيجة
Basic 2PL Growing ثم Shrinking، وتفك في أي وقت بعد الـ Lock Point Serializable، بس ممكن Cascading Aborts
Strict 2PL الـ X Locks بتفضل لحد COMMIT أو ABORT Serializable + Strict (مفيش Cascading Aborts)
Rigorous 2PL (Strong Strict) كل الـ Locks (S و X) لحد COMMIT أو ABORT زي Strict + ترتيب الـ Serial = ترتيب الـ COMMIT

كل الداتابيز اللي بتستخدم Locks بتعمل Strict أو Rigorous. وده ليه إنت عمرك ما شفت أمر UNLOCK ROW في SQL: الـ Row Locks بتتفك لوحدها في الـ COMMIT أو الـ ROLLBACK.

وده سبب أهم قاعدة عملية في الموضوع كله: خلّي الترانزاكشن قصيرة. كل ثانية الترانزاكشن مفتوحة فيها، كل الـ Locks اللي خدتها ماسكة الناس مستنية.

في الداتابيز الحقيقية، Lock الكتابة بيفضل لحد الـ COMMIT. فطول الترانزاكشن = طول الاستنا عند كل اللي وراك.

5. حجم القفل والـ Intention Locks

تقفل إيه بالظبط؟ صف؟ صفحة؟ جدول؟

الحجم ميزة عيب
صف توازي عالي جداً Locks كتير جداً، وكل واحد بياخد ذاكرة ووقت
صفحة وسط صفين ملهمش علاقة ببعض في نفس الصفحة بيستنوا بعض
جدول Lock واحد بس مفيش توازي خالص

الداتابيز بتدعم أكتر من حجم مع بعض (Multiple Granularity Locking). بس فيه مشكلة: ترانزاكشن عايزة تقفل الجدول كله X. إزاي تعرف إن مفيش حد ماسك صف واحد جواه من غير ما تلف على مليون صف؟

الحل: Intention Locks. قبل ما تقفل صف، بتحط “نية” على الجدول.

ماسك ↓ / عايز ← IS IX S SIX X
IS ✓ ✓ ✓ ✓ ✗
IX ✓ ✓ ✗ ✗ ✗
S ✓ ✗ ✓ ✗ ✗
SIX ✓ ✗ ✗ ✗ ✗
X ✗ ✗ ✗ ✗ ✗

لاحظ إن IX مع IX متوافقين: ترانزاكشنين بيكتبوا صفوف مختلفة في نفس الجدول مايستنوش بعض على مستوى الجدول. والخناقة الحقيقية بتبقى على مستوى الصف.

في InnoDB: أي SELECT ... FOR SHARE بياخد IS على الجدول، وأي UPDATE أو FOR UPDATE بياخد IX، وبعدين Lock على الصف. و LOCK TABLES ... WRITE بياخد X على الجدول. وفي SQL Server فيه كمان Lock Escalation: لو ترانزاكشن خدت آلاف Row Locks، الداتابيز بتحولهم لـ Table Lock واحد. InnoDB و PostgreSQL مش بيعملوا كده.

6. Phantoms: إزاي تقفل صف مش موجود؟

SELECT * FROM employees WHERE dept = 'IT' FOR UPDATE بيقفل الـ 3 صفوف الموجودين. بس حد يعمل INSERT لموظف رابع في IT، وده صف جديد مفيش عليه Lock. فالـ Phantom لسه بيعدّي حتى مع 2PL على الصفوف.

الحل النظري: Predicate Lock، يعني تقفل الشرط نفسه dept = 'IT'، وأي INSERT يطابق الشرط يستنى. بس التأكد إن شرطين بيتقاطعوا مكلف جداً. فالداتابيز بتعمل حل عملي: تقفل المسافات في الـ Index.

InnoDB عنده 4 أنواع Row Locks:

النوع بيقفل إيه مثال في Index فيه 10 و 20 و 30
Record Lock صف واحد في الـ Index 20 بس
Gap Lock المسافة بين صفين، من غير الصفوف نفسها (10, 20): محدش يعمل INSERT لـ 15
Next-Key Lock الصف + المسافة اللي قبله (10, 20]: الـ 20 والمسافة قبلها
Insert Intention Lock نوع Gap Lock بيتاخد قبل الـ INSERT اتنين يدخلوا 12 و 17 في نفس المسافة من غير ما يستنوا بعض

في REPEATABLE READ، أي Locking Read أو UPDATE أو DELETE بشرط Range بياخد Next-Key Locks على كل اللي مسحه في الـ Index، فمحدش يقدر يحط صف جديد في النطاق ده.

محدش يضيف موظف وأنا بحسبجرّب بنفسك
في الداتابيزIT rows3
مراجعة مرتبات IT⏱تعيين موظف IT
  1. BEGIN; SELECT * FROM employees WHERE dept = 'IT' FOR UPDATE;3 صفوف · Next-Key Locks1
  2. 2INSERT INTO employees (name, dept) VALUES ('Omar', 'IT');⏳ مستنية Insert Intention
  3. UPDATE employees SET salary = salary * 1.1 WHERE dept = 'IT'; COMMIT;3 rows3
  4. 4-- الـ INSERT بيكمل1 row
▶

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

وده بيوضح نقطة مهمة: الـ Index بيحدد حجم القفل. شرط على عمود من غير Index = Full Scan = Locks على كل صف عدّى عليه. وده سبب قصة “UPDATE واحد وقّف الموقع كله” في موضوع الـ Index.

PostgreSQL بيحل الـ Phantoms بطريقة مختلفة خالص: الـ Snapshot بتخلّي القراية مش شايفة الصفوف الجديدة، وفي SERIALIZABLE بيستخدم SIREAD Locks (Predicate Locks مش بتمنع حد، بس بتتابع التعارضات). ده في درس SSI.

الداتابيز مش بتقفل الشرط، بتقفل المكان في الـ Index اللي الصفوف المطابقة للشرط ممكن تتحط فيه. علشان كده الـ Index الصح بيصغّر الـ Lock.

7. الـ Row Locks في SQL

الأمر MySQL 8 PostgreSQL المعنى
FOR UPDATE ✓ ✓ X: هعدّل، محدش يقفل ولا يعدّل
FOR SHARE ✓ (قديماً LOCK IN SHARE MODE) ✓ S: اقرا وامنع التعديل لحد ما أخلص
FOR NO KEY UPDATE ✗ ✓ زي FOR UPDATE بس بيسمح بـ FK checks على الصف
FOR KEY SHARE ✗ ✓ أضعف Lock: يمنع بس مسح الصف أو تغيير الـ Key
NOWAIT ✓ ✓ لو مقفول ارمي Error فوراً بدل الاستنا
SKIP LOCKED ✓ (8.0+) ✓ (9.5+) اعدّي الصفوف المقفولة وهات الباقي

PostgreSQL عنده 4 مستويات لأن الـ UPDATE العادي اللي مش بيغيّر الـ Primary Key بياخد FOR NO KEY UPDATE بس، والـ Foreign Key check من جدول تاني بياخد FOR KEY SHARE. والاتنين دول متوافقين. فتعديل سعر منتج مش بيوقف إضافة أوردر بيشاور على المنتج ده.

nowait-skip-locked.sql
-- حجز كرسي: لو حد ماسكه، قول للمستخدم فوراً بدل ما يستنى
SELECT * FROM seats WHERE id = 42 FOR UPDATE NOWAIT;
-- ERROR: could not obtain lock on row in relation "seats"   (PostgreSQL)
-- ERROR 3572: Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set (MySQL)

-- Worker بياخد شغل من طابور: كل Worker ياخد صفوف مختلفة من غير ما يستنوا بعض
SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY id
LIMIT 10
FOR UPDATE SKIP LOCKED;

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

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

01اشرح Two-Phase Locking. وليه بيضمن Serializability؟

كل ترانزاكشن ليها مرحلة تاخد فيها Locks بس، وبعدها مرحلة تفك فيها بس، ومفيش Lock جديد بعد أول فك. بيضمن Conflict Serializability لأن أي سهم Ti → Tj في الـ Precedence Graph معناه إن Lock Point بتاع Ti قبل Tj، فلو فيه Cycle تبقى ترانزاكشن قبل نفسها، وده مستحيل.

02الفرق بين 2PL و Strict 2PL؟

في 2PL العادي ممكن تفك Locks قبل الـ COMMIT في مرحلة الـ Shrinking، فحد يقرا كتابة لسه ماتثبتتش، وده ممكن يعمل Cascading Aborts. Strict 2PL بيمسك الـ X Locks لحد الـ COMMIT أو الـ ABORT، فمحدش يقرا حاجة مش متثبتة. وده اللي الداتابيز الحقيقية بتعمله.

03ليه محتاجين Intention Locks؟

عشان الداتابيز تقدر تقفل الجدول كله من غير ما تلف على كل صف تتأكد إن مفيش حد ماسك صف جواه. أي حد بيقفل صف بيحط IS أو IX على الجدول، فاللي عايز X على الجدول يشوف الـ IX ويستنى. و IX مع IX متوافقين، فاللي بيكتبوا صفوف مختلفة مايستنوش بعض.

04إزاي InnoDB بيمنع الـ Phantoms في Locking Reads؟

بـ Next-Key Locks: بيقفل الصفوف اللي مسحها في الـ Index والمسافات (Gaps) اللي قبلها، فأي INSERT في النطاق ده بيستنى. ومن غير Index على عمود الشرط، المسح بيبقى على كل الـ Clustered Index، فالـ Lock بيبقى على الجدول كله تقريباً.

05إمتى تستخدم SKIP LOCKED؟ وإمتى NOWAIT؟

SKIP LOCKED لما مش فارق أنهي صف تاخده، زي Workers بتسحب شغل من جدول jobs: كل Worker ياخد صفوف مختلفة من غير استنا. NOWAIT لما عايز صف معين بالذات ولو مقفول عايز ترد فوراً، زي حجز كرسي: قول للمستخدم "حد بيحجزه دلوقتي" بدل ما يستنى.

06الفرق بين Lock و Latch؟

Lock على داتا منطقية زي صف أو جدول، وبيفضل لحد آخر الترانزاكشن، والداتابيز بتكشف الـ Deadlocks بتاعته. Latch على هيكل في الذاكرة زي صفحة في الـ Buffer Pool، وبيتمسك ميكروثواني عشان threads ماتبوظش بعض، ومفيهوش Deadlock Detection.

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