ليه Transactions؟
حاجات كتير ممكن تبوظ: الداتابيز تقع في نص كتابة، الأبلكيشن يقع في نص عمليات، الشبكة تقطع، كلاينتين يكتبوا فوق بعض، حد يقرا داتا نصها متحدث… الـ transaction بتجمع كذا read/write في وحدة منطقية واحدة: يا تنجح كلها (commit) يا تفشل كلها (abort/rollback) — وساعتها تقدر تعمل retry بأمان.
معنى ACID (وإيه اللي مخبي وراه)
| الحرف | المعنى الحقيقي |
|---|---|
| A — Atomicity | مش ليها علاقة بالـ concurrency! معناها abortability: لو حصل خطأ في النص، كل اللي اتكتب يترجع. فتقدر تعمل retry بأمان من غير ما تخاف من نص كتابة. |
| C — Consistency | كلمة متحمّلة معاني كتير! هنا معناها إن الـ invariants بتاعة الأبلكيشن تفضل صح (زي: الحسابات المدينة والدائنة متوازنة). وده مسؤولية الأبلكيشن مش الداتابيز — الـ C “اتحطت عشان الاختصار يمشي”. |
| I — Isolation | الـ transactions الـ concurrent ماتدخلش في بعض. النظري: serializability (كأنهم اتنفذوا واحد ورا التاني). عمليًا داتابيزات كتير بتدي أضعف من كده عشان الأداء. |
| D — Durability | اللي اتعمله commit مايضيعش (ديسك + WAL، أو replication). بس مفيش durability مطلقة: الهاردات بتموت، SSDs بتبوظ بعد قطع الكهربا، الـ replicas ممكن تقع كلها مع بعض. |
والأنظمة اللي مابتحققش ACID بيقولوا عنها BASE (Basically Available, Soft state, Eventual consistency) — وده تعريف مبهم أكتر من ACID نفسها 😄.
Single-object vs Multi-object
- Single-object: تقريبًا كل الداتابيزات بتضمن atomicity و isolation على مستوى الـ object الواحد (log للـ recovery و lock لكل object)، وفيه عمليات زي increment و compare-and-set. دي مش transactions بالمعنى الكامل.
- Multi-object ضرورية لما: foreign keys بين جداول، denormalized data في document DBs لازم تتحدث مع بعض، secondary indexes لازم تتحدث مع الداتا.
مستويات العزل الضعيفة (Weak Isolation)
الـ serializable مكلف، فالداتابيزات بتقدم مستويات أضعف — والمستويات دي سببت فلوس ضايعة حقيقي وفساد داتا. لازم تفهم كل واحد بيمنع إيه:
Read Committed
ماتشوفش داتا transaction تانية لسه ماعملتش commit. (وإلا ممكن تشوف حاجة هتترجع، أو نص تحديث.)
ماتكتبش فوق داتا لسه ماتعملهاش commit. (مثال: اتنين بيشتروا نفس العربية — الفاتورة تروح لواحد والعربية لواحد تاني!)
التنفيذ: الـ dirty writes بـ row-level locks. الـ dirty reads: مش بالـ lock (هيبطّأ القراءة)، بل الداتابيز بتفتكر القيمة القديمة المتعملها commit وتديها للقارئين لحد الـ commit. ده الـ default في PostgreSQL و Oracle و SQL Server.
Snapshot Isolation (Repeatable Read)
الحل: كل transaction تقرا من snapshot ثابت للداتابيز من لحظة ما بدأت. “القرّاء مش بيعطلوا الكتّاب، والكتّاب مش بيعطلوا القرّاء”.
التنفيذ: MVCC (Multi-Version Concurrency Control) — الداتابيز تحتفظ بكذا version لكل صف. كل transaction لها ID متزايد، وكل صف عليه created_by و deleted_by. قاعدة الظهور: صف يبان لو اللي عمله كان commit قبل ما انت تبدأ، ومحدش مسحه (أو اللي مسحه ماعملش commit قبلك). والـ garbage collection بيشيل القديم. والـ indexes: إما تشاور على كل الـ versions، أو B-trees append-only/copy-on-write (CouchDB, LMDB).
Lost Updates
اتنين بيعملوا read-modify-write في نفس الوقت (زيادة عداد، تعديل JSON، اتنين بيعدلوا صفحة wiki). واحد بيكتب فوق التاني والتعديل يضيع.
| الحل | إزاي |
|---|---|
| Atomic write operations | UPDATE counters SET value = value + 1 WHERE key='x' — أحسن حل لو ينفع. (وخلي بالك: الـ ORMs سهل تخليك تعمل read-modify-write في الكود من غير ما تاخد بالك!) |
| Explicit locking | SELECT … FOR UPDATE — لازم تفتكر تعمله في كل مكان. |
| Automatic detection | الداتابيز تكتشف وتعمل abort (PostgreSQL repeatable read، Oracle serializable، SQL Server snapshot). MySQL InnoDB repeatable read مابتكتشفش. |
| Compare-and-set | UPDATE … SET content='new' WHERE id=1 AND content='old' — بس خلي بالك لو الـ WHERE بتقرا من snapshot قديم. |
| في الـ replication | locks و CAS مابيشتغلوش مع multi-leader/leaderless. الحل: siblings + merge، أو عمليات commutative (increment). وLWW = lost updates (وده الـ default في سيستمات كتير!). |
Write Skew و Phantoms — الأخطر
الشكل العام: (1) SELECT بيدور على صفوف بشرط، (2) الأبلكيشن بيقرر بناءً على النتيجة، (3) كتابة بتغير النتيجة اللي كانت أساس القرار. أمثلة تانية: حجز أوضة اجتماعات في نفس الوقت، لعبة multiplayer (لاعبين في نفس المربع)، username مكرر، صرف فلوس مرتين (رصيد سالب).
لو الـ SELECT بيدور على صفوف موجودة ممكن تحلها بـ FOR UPDATE. بس لو بيتأكد إن حاجة مش موجودة (مفيش حجز في الوقت ده) يبقى مفيش صف تقفل عليه! الكتابة اللي بتغير نتيجة البحث بتاعة transaction تانية اسمها Phantom.
حل يدوي: Materializing conflicts — تعمل جدول فيه كل (أوضة × ربع ساعة) مقدمًا عشان يبقى فيه صفوف تقفل عليها. صعب وقبيح — والأحسن serializable.
Serializability — أقوى ضمان
النتيجة كأن كل الـ transactions اتنفذت واحدة ورا التانية. بيمنع كل المشاكل. تلات طرق للتنفيذ:
1) Actual Serial Execution
حرفيًا transaction واحدة في المرة على thread واحد! ليه بقى ممكن مؤخرًا؟ الرام رخص (الداتا كلها في الرام)، والـ OLTP transactions قصيرة وصغيرة. مستخدم في VoltDB/H-Store و Redis و Datomic.
- Stored procedures إجباري: مينفعش transaction تستنى اليوزر أو round-trips على الشبكة — الكود كله يتبعت مرة واحدة. (السمعة الوحشة للـ stored procedures جت من لغاتها القديمة PL/SQL؛ دلوقتي فيه Java/Lua/Clojure.) وفي الـ replication: تنفذ نفس الـ procedure على كل replica (لازم deterministic).
- Partitioning عشان تستخدم كذا core: كل partition له thread. بس الـ cross-partition transactions أبطأ جدًا.
- الشروط: transactions صغيرة وسريعة، الـ active data تدخل الرام، throughput الكتابة يتحمله core واحد.
2) Two-Phase Locking (2PL)
الطريقة التقليدية لـ 30 سنة. (ماتلخبطهاش مع 2PC!) القاعدة: الكتّاب بيعطلوا القرّاء والقرّاء بيعطلوا الكتّاب — عكس snapshot isolation.
- كل object عليه lock إما shared (للقراءة — كذا واحد مع بعض) أو exclusive (للكتابة — واحد بس).
- بتمسك الـ locks لحد آخر الـ transaction (المرحلة 1: بتاخد، المرحلة 2: بتسيب الكل عند الـ commit).
- Deadlocks بتحصل كتير؛ الداتابيز بتكتشفها وتعمل abort لواحدة.
- Phantoms: predicate locks (lock على شرط بحث — حتى على صفوف لسه مش موجودة) — بس أداؤها وحش، فبيستخدموا index-range locks (next-key locking) كتقريب: تقفل حتة أكبر من اللازم بس أسرع.
- ❌ الأداء أضعف بكتير، والـ latency مش متوقع خالص (transaction بطيئة واحدة توقف الكل).
3) Serializable Snapshot Isolation (SSI)
خوارزمية جديدة (2008) — في PostgreSQL من 9.1 و FoundationDB. Optimistic: سيب الكل يشتغل على snapshot من غير blocking، ووقت الـ commit اتأكد: لو فيه تعارض اعمل abort.
“لو ممكن حاجة تبوظ، استنى.” زي mutual exclusion.
“كمّل، ولو طلع فيه مشكلة نلغي.” كويس لو الـ contention قليل؛ لو عالي هيبقى aborts كتير.
بيكتشف إمتى “الفرضية” اللي القرار اتبنى عليها بقت قديمة:
- قراءة stale MVCC: وقت الـ commit، هل فيه writes كانت لسه ماعملتش commit لما قريت واتعملها commit دلوقتي؟ → abort.
- كتابات بتأثر على قراءات سابقة: زي index-range locks بس من غير blocking — الداتابيز تسجل مين قرا إيه، ولما حد يكتب تبلّغ القرّاء إن داتاهم بقت قديمة.
✅ مفيش blocking بين القرّاء والكتّاب، latency متوقع، وبيتوزع على كذا ماكينة. ⚠️ الـ transactions الطويلة اللي بتقرا وتكتب كتير ممكن تتعمل abort كتير.
🧪 Lab
اعمل write skew بإيدك في Postgres بـ terminal-ين (psql):
CREATE TABLE doctors (name text, on_call bool);INSERT INTO doctors VALUES ('alice', true), ('bob', true);
في الاتنين: BEGIN ISOLATION LEVEL REPEATABLE READ; → SELECT count(*) FROM doctors WHERE on_call; → كل واحد يعمل UPDATE لنفسه → COMMIT. هتلاقي الاتنين نجحوا والـ count بقى صفر 😱.
كرر نفس الخطوات بـ ISOLATION LEVEL SERIALIZABLE — واحد منهم هياخد serialization error (ده الـ SSI شغال).
مين بيمنع إيه؟
| المشكلة | Read Committed | Snapshot Isolation | Serializable |
|---|---|---|---|
| Dirty reads | ✅ | ✅ | ✅ |
| Dirty writes | ✅ | ✅ | ✅ |
| Read skew | ❌ | ✅ (MVCC) | ✅ |
| Lost updates | ❌ | ⚠️ بعض التطبيقات | ✅ |
| Write skew | ❌ | ❌ | ✅ |
| Phantoms (في write skew) | ❌ | ❌ (محتاج index-range locks) | ✅ |