مدونة عمار الخوالده

خصائص ACID: ضمانات قواعد البيانات


خصائص ACID: ضمانات قواعد البيانات

مقدمة

تقدم قواعد البيانات ضماناتٍ وأدواتٍ تساعد على تقليل الأخطاء أو منعها والتي تعرف باختصار ACID:

  • Atomicity

  • Consistency

  • Isolation

  • Durability

وهي ضماناتٌ تطبقها قاعدة البيانات في الـ Transactions، وللاستفادة منها لا بد من فهم كل واحدة والمشاكل التي تحلها.

TIP

أفتَرِضُ في هذا المقال أنك تعاملت مع الـ Transaction في قواعد البيانات. كتبت قصاصة قصيرة عن الـ Transaction إن أردت تجديد ذاكرتك: ما هو الـ Transaction في قواعد البيانات؟ - مدونة عمار الخوالده 1

هذا المقال يشرح الخصائص الأربعة، مع ترك خاصية Consistency (الخاصية الثانية) إلى نهاية المقال لأن ما قبلها يتعلق بها.

Atomicity - الكل أو العدم!

قد تقع أخطاء كثيرة أثناء تنفيذ الـ Queries، سواء في الـ Queries نفسها أم بسبب مشاكل من خارج قاعدة البيانات كمشاكل الشبكةِ أو نظام التشغيلِ أو حتى انقطاع الكهرباء.

تخيل أنك تعمل على نظام لإرسال الحوالات المالية، ولديك عملية “تحويل الرصيد” في النظام، العملية تتكون من أكثر من Query على الشكل الآتي:

  1. قراءة الرصيد الموجود في الحساب الأول للتحقق من وجود رصيد كافٍ قبل التحويل منه.
  2. خصم مبلغ الحوالة من الحساب الأول.
  3. إضافة مبلغ الحوالة إلى الحساب الثاني. 🔴 وقع خطأ ولم تكتمل العملية!
  4. إضافة سجل في جدول لتتبع الحوالات تفيد بتحويل مبلغ محدد من الحساب الأول إلى الحساب الثاني.

لو لم تنفذ هذه الـ Queries ضمن Transaction لن يُضاف المبلغ إلى الحساب الثاني، وفي الغالب لن يُضافَ سجل التتبع (لأن الخطأ سيؤدي إلى انهيار في الـ Application، إلا في حالة تجاهل الأخطاء الناتجة عن الـ Queries السابقة)، ومع ذلك يفقد الحساب الأول مبلغ التحويل، تحل قاعدة البيانات هذه المشاكل بضمان الـ Atomicity.

الـ Atomicity (الذريّة، مشتقة من الذرّة) هي خاصية يضمنها الـ Transactions تقتضي تنفيذ الـ Queries كلها بنجاح أو التراجع عنها جميعا.

NOTE

الـ Atomicity مصطلح يُستخدم أحيانًا عند الحديث عن الـ Concurrency لمنع تداخل التعليمات التي ينفذها أكثر من Thread، وسنناقش مشاكل الـ Concurrency لاحقًا عند الحديث عن الـ Isolation، أما في سياق قواعد البيانات فتعني: الكل أو لا شيء.

في مثال الحوالات، وقوع خطأ في أي خطوة قد يؤدي إلى كارثة، لكن إن نُفِّذت هذه الـ Queries ضمن Transaction ووقع خطأ ستُنفّذ عملية ROLLBACK.

هذا في حال تعدد الـ Queries، ماذا عن تنفيذ Query واحد؟

الـ Query الواحد الذي يُنفذ دون استخدام جملتي BEGIN و COMMIT تتعامل معه قاعدة البيانات على أنه Transaction ضمني (Implicit Transaction)2، فلو أخذنا هذا الــ Query كمثال:

1UPDATE products SET discount=5 WHERE category="laptops";

فهو يعدل أكثر من Record، وقوع خطأٍ أثناء التعديل يؤدي إلى تنفيذ عملية Rollback لجميع التعديلات على الـ Records، فلن يتم إضافة خصم على بعض المنتجات وترك بعضها الآخر بلا تعديل، أما ان تنجح كلها أو تفشل جميعا.

ولا يقتصر هذا على الـ Queries التي تعدل في أكثر من Record، حتى لو فرضنا أننا ننفِّذ Query يكتب بيانات كبيرة بشكل بطيء (وليكن مثلا نصّاً طويلا جدا)، فإن وقع أي خطأ قبل نهاية التنفيذ لن تُكتَب هذه البيانات جزئيًا بل تُلغى كلها.

IMPORTANT

ملاحظة: تحقق دائما من هذه المعلومات حسب الـ Database Engine الذي تستخدمه، فالتطبيق في الواقع (Implementation) قد يختلف عن المعلومات النظرية، أو قد تجد استثناءات كبعض الـ DDL Statements التي قد تُنفّذ بشكل جزئي ولا تُعّد Atomic.

الـ Isolation - مشاكل الـ Concurrency وعزل الـ Transactions عن بعضها

لعلك درست أو قرأت عن مشاكل الـ Concurrency في نظم التشغيل، حيث يتنافس أكثر من thread على تعديل وقراءة نفس المتغير مما يسبب بعض المشاكل (Race Conditions).

قواعد البيانات ليست مختلفة، معظم قواعد البيانات تدعم اتصال أكثر من مستخدم في وقت واحد، أو وجود أكثر من session لنفس المستخدم، عند اتصال اكثر من مستخدم بقاعدة البيانات حيث ينفذ كل واحد منهم Queries على نفس الجداول أو البيانات، تظهر عندنا مشاكل الـ Concurrency التي تعرف بالـ Race Conditions.

لمشاكل قواعد البيانات المتعلقة بالـ Concurrency أنواع كثيرة، تسمى Anomalis (انحرافات بالمعنى الحرفي)، وهي تنقسم إلى:

  • Read Phenomenas: المشاكل المتعلقة بقراءة البيانات.

  • Write Phenomenas: المشاكل المتعلقة بكتابة البيانات.

نناقش الآن بعضها مع الحلول التي توفرها قاعدة البيانات لكل واحدة.

⚠️ الـ Dirty Reads - قراءة قيم غير مثبّتة

صورة توضح مشكلة الـ Dirty Read التي تحصل بسبب قراءة قيمة غير مثبتة بعملية COMMIT

في المثال السابق، حدّث الـ Transaction الأول الرصيد ثم تراجع عن التحديث بعملية ROLLBACK (قد يحصل واقعيا إذا وقع خطأ في النظام، أو بسبب عدم تحقق شرط معين)، قرأ الـ Transaction الثاني القيمة التي كتبها الـ Transaction الأول، ونفذ عملية شراء لمبلغ لم يمتلكه المستخدم.

هذا النوع من الأخطاء يسمى Dirty Reads، لأن الـ Transaction يقرأ قيمة غير مثبتة بعملية COMMIT (أي قراءة قيمة Uncommitted/Dirty)، بالتالي يمكن التراجع عنها في أي وقت.

قواعد البيانات تضمن للـ Transaction خاصية الـ Isolation، وهي تعني أن كل Transaction معزول عن غيره فلا يؤثر به، ولهذا العزل أنواع مختلفة كل منها يحل المشكلة بطريقة مختلفة.

✅ الـ Serializable Isolation Level - حل مثالي؟

الـ Isolation الكامل الذي يتجنب جميع مشاكل الـ Race Condition هو الـ Serializable، وهو يعني أن الـ Transactions المتداخلة يتم تنفيذها بشكل متسلسل واحدة بعد الأخرى (Serially) ولا يتم تنفيذها في وقت واحد أبدا، بمعنى آخر، هي إلغاءٌ مؤقت للـ Concurrency.

توضيح للـ Serializable Isolation Level

الـ Serializable ليست إلغاءً كاملا للـ Concurrency، فلا زال ممكنا تنفيذ أكثر من Transaction في وقت واحد، لكن في المثال السابق، عند تنفيذ Query لقراءة الرصيد في Transaction #2، يبقى هذا الـ Query معلقا ينتظر زوال “القفل” الذي وضعه الـ Transaction الأول. بمجرد تنفيذ ROLLBACK (أو COMMIT) في الـ Transaction الأول يتم تنفيذ الـ Query المعلق مباشرة.

مشكلة الـ Serializable Isolation هي زيادة عدد الـ Queries المعلّقة (Blocked) التي تنتظر، وهذا التأخير يؤدي إلى مشاكل بالـ Memory إضافة إلى البطء الشديد في الـ Queries.

لهذا توجد أنواع أخرى من الـ Isolation Levels غير الـ Serializable، فهو يعطي الضمان الأكبر لعدم حدوث Race Conditions لكنه الأسوأ من حيث الأداء، توجد مستويات أخرى لكن عزل الـ Transactions عن بعضها أقل..

✅ Read Committed - كيف نحل مشكلة الـ Dirty Read دون تضحية كبيرة بالأداء؟

ماذا لو تجنبنا الـ Dirty Reads بإجبار الـ Transaction على قراءة البيانات إذا كانت Committed فقط، أما إذا لم تكن كذلك فلن يقرأها. أصبح بالتالي لكل Transaction إمكانية العبث بالبيانات دون القلق من قراءتها قبل اكتمال الـ Transaction.

الـ Read Committed Isolation Level لحل الـ Dirty Read دون تضحية بالـ Performance

لاحظ أننا حللنا مشكلة الـ Dirty Reads دون التضحية بالـ Concurrency ودون إبقاء الـ Query منتظِراً فكسبنا الـ Isolation والأداء السريع معا.

TIP

هذا المستوى هو المستوى الافتراضي في PostgreSQL.3

صحيح أن هذا المستوى قد تجنب مشكلة الـ Dirty Reads، لكنه لا يحل محل الـ Serializable، فالـ Dirty Read ليست المشكلة الوحيدة!

⚠️ قراءتان مختلفتان لقيمة واحدة - Non-Repeatable Read

الـ Non-Repeatable Read هي مشكلة تحصل عند قراءة نفس البيانات مرتين كما في الصورة التالية:

الـ Non-Repeatable Read Issue التي تتسبب بقراتين مختلفتين لنفس القيمة

الـ Transaction الأول قرأ بيانات المنتج مرتين، مرة في بداية الـ Transaction وأخرى في نهايته، نُفِّذ Transaction آخر بين عمليتي القراءة مع تنفيذ COMMIT،

في حالة الـ Dirty Read كانت البيانات غير مثبتة بعملية COMMIT، بينما في هذه الحالة تم تنفيذها قبل نهاية الـ Transaction الأول، مما يعني قراءته البيانات حتى لو فعَّلنا الـ Read Committed isolation.

مشكلة الـ Non-Repeatable Read ليست متكررة بكثرة كغيرها من المشاكل، خاصة أن كتابة الكود بشكل جيد عن طريق قراءة أسعار المنتجات مرة واحدة في بداية الـ Transaction يكفي، ليس ثمة داعٍ غالبا لإعادة قراءة نفس القيمة مرتين.

لذلك افترضت قراءة نفس المنتج مرة بجملة SELECT مباشرةً، ومرة أخرى عن طريق عمل JOIN للمنتجات مع بيانات سلة المشتريات كمثال أكثر واقعية، ففي هذه الحالة ستحصل على سعرين مختلفين لنفس المنتج ضمن Transaction واحد.

ففي المرة الأولى قرأنا سعر المنتج على أنه 100 بينما في المرة الثانية قرأناه على أنه 50.

✅ تثبيت القيمة لأكثر من قراءة - Repeatable Read

مستوى Repeatable Read Isolation، يحل مشكلة الـ Non-Repeatable Read حيث يضمن أن قراءتك للقيمة عدة مرات ضمن نفس الـ Transaction يعطي دائما نفس النتيجة، لنلق أولا نظرة على تعريف الـ Repeatable Read:

Repeatable Read: allows only committed data to be read and further requires that, between two reads of a data item by a transaction, no other transaction is allowed to update it 4.

تسمح بقراءة البيانات المثبّتة فقط (Committed)، ولا تسمح لأي Transaction آخر بتعديل هذه البيانات بين قراءتين لها.

هذا ما يبدو عليه الأمر:

الـ Repeatable Read Isolation Level

ليس هذا ما يحصل في كثير من قواعد البيانات مثل MySQL و PostgreSQL، فإن منع تعديل القيم بهذه الطريقة سيؤدي إلى تراجعٍ كبير في الأداء عند استخدام هذا المستوى من الـ Isolation.

بعض قواعد البيانات توفر مستوى عزل باسم Repeatable Read، لكنه في واقع الأمر مستوى آخر يعمل بطريقة مختلفة ويسمى Snapshot Isolation.

Snapshot Isolation

قد أُتبِعُ هذا المقال بمقالات أخرى أشرح فيها أكثر عن طريقة عمل الـ Isolation Levels وكيف تم بناؤها أصلا، لكن يكفي حاليا أن تعلم أن الـ Isolation Levels عادة ما تُبنى بالأقفال (Locks).

NOTE

موضوع الـ Locks مهم جدا وله علاقة وثيقة بالـ Isolation، لكني لن أشرحها بالتفصيل في هذا المقال للتركيز أكثر على نقاط محددة، أضفت في الهوامش مقالاً بالعربية يتكلم عنها5

بعض قواعد البيانات كـ PostgreSQL و MySQL تحاول التقليل من استخدام الـ Locks بهدف تقليل الوقت الذي تبقى فيه الـ Queries منتظرة للسماح لها بقراءة بيانات معينة أو الكتابة عليها، فتستخدم تقنية MVCC (Multi-Version Concurrency Control)، والتي تقوم بانشاء “Snapshot” أو صورة مثبتة لشكل البيانات الحالي.6

الـ Snapshot هي اشبه بنسخة من البيانات لكل Transaction، فما يراه الـ Transaction الأول يختلف عمّا يراه الـ Transaction الثاني.

في حالة الـ Repeatable Read Level، يتم الاعتماد على الـ Snapshot في توفير العزل بين الـ Transactions.

الـ SnapShot Isolation Level

كل Transaction يملك نسخة خاصة به من البيانات، الـ Transaction الثاني سيكتب في نهاية المطاف على النسخة الأصلية، لكن الـ Transaction الأول لن يتأثر لأنه يقرأ من الـ Snapshot الخاصة به.

TIP

هذا المستوى من الـ Isolation هو المستوى الافتراضي في (InnoDB) MySQL.7

⚠️⚠️⚠️ Lost Updates - ضياع البيانات وعجز مستويات الـ Isolation!

الـ Lost Update هي من أهم المشاكل إن لم تكن أهمها من وجهة نظري، فهي في العادة تحتاج إلى تدخل يدوي لحلها، ولا يحلها مجرد تغيير للـ Isolation Level (إلا إن استخدمت الـ Serializable).

مشكلة الـ Lost Updates التي تتسبب بضياع البيانات ولا تحلها مستويات الـ Isolation الضعيفة

أفترضُ في هذا المثال أن عندنا منتجٌ تتوفر منه ثلاثة قطع فقط، قام مستخدمان بمحاولة شرائه في وقت واحد.

يتحقق كل Transaction من توفر المنتج، فيجدان أن عدد القطع المتوفرة هي ثلاثة، نفّذَ الأول Query لشراء قطعتين، أما الثاني نفّذَ Query لشراء ثلاثة قطع.

الصورة توضح انتهاء الـ Query الثاني قبل الأول، بمجرد انتهاء الـ Query تصبح الكمية المتوفرة 0، ثم بانتهاء الـ Query الأول يتم إنقاص قطعتين ما يجعل الكمية المتوفرة -2.

هذا يسبب لنا عدة مشاكل، أولها تنفيذ عمليتي شراء على كمية لا تكفي من المنتج، والأخرى وجود قيم خاطئة في قاعدة البيانات.

اكتشاف هذه الحالة قد يكون سهلا لأن الرقم أصبح تحت الصفر، لكن اكتشاف المشكلة قد يكون أصعب في أحيان أخرى.

⚠️⚠️⚠️ لماذا لا يحل الـ Repeatable Read مشكلة الـ Lost Updates؟

لو بحثت في بعض المراجع والمقالات، ستجد كثيرا من الصور أو الجداول التي توضح أنواع مشاكل قواعد البيانات وأنواع الـ Isolation لحل كل مشكلة، قد تجد أحيانا من يكتب أن مشكلة Lost Update يحلها الـ Repeatable Read، وهذا قد يكون صحيحا أحيانا لكنه غير دقيق دائما.

تعتمد هذه المراجع غالبا على الجدول الموجود في الورقة البحثية المعروفة “A Critique of ANSI SQL Isolation Levels”8، وهذا صحيح على افتراض الـ Implementation باستخدام الـ Locking-Based Concurrency Control، لكن كثيرا من قواعد البيانات (ومنها PostgreSQL و MySQL) تستخدم MVCC بدلا من الـ Locks لتحقيق الـ Repeatable Read Isolation Level.

وهنا تختلف صحة الكلام قليلا، في هذه الحالة الـ Repeatable Read وحده لن يحل مشكلة الـ Lost Updates وإن كان يحل مشكلة الـ Non-Repeatable Read، لأن ما يحل مشكلة الـ Lost Updates هي الـ Locks التي افترضت الورقة العلمية أن الـ Repeatable Read تستخدمها.

هل الـ Repeatable Read في PostgreSQL و MySQL يحل مشكلة الـ Lost Updates؟

قاعدة PostgreSQL تمنع الـ Lost Updates على مستوى الـ Repeatable Read لأنها تتحقق من وجود تعارض اثناء عملية الكتابة (Write-Write Conflict)، فهي تتحقق من عدم تغير القيمة قبل عمل COMMIT، الـ Transaction الأخير في هذه الحالة يفشل ويرجع الخطأ التالي:

[40001] ERROR: could not serialize access due to concurrent update

بينما في MySQL فلا يوجد تحقق من وجود تعارضات قبل الكتابة، مما يؤدي إلى ظهور المشكلة.

✅ كيف نحل مشكلة الـ Lost Updates؟

للـ Lost Updates حلول بسيطة، حتى لو استخدمت PostgreSQL فلا داعٍ لاستخدام Repeatable Read (تذكر أنه ليس الافتراضي فيها بل الافتراضي هو Read Committed)، يمكن حل المشكلة بطريقتين:

Atomic Operations

مشكلة الـ Lost Updates تحصل بسبب ما يعرف بـ Read-Modify-Write Cycle، أي أنك تقوم بقراءة البيانات (قراءة الكمية المتوفرة في مثالنا السابق)، ثم تعدل البيانات، ثم تعيد كتابتها، الخطأ يحصل خلال هذه العملية حيث لا يعلم الـ Transaction أنه قرأ قيمة قد غيرها Transaction آخر.

قواعد البيانات توفر Atomic Operations، وهي عمليات معزولة تماما لا تتأثر بمشاكل الـ Race-Conditions إضافة لكونها Atomic.

فبدلا من تنفيذ:

1select quantity from products where id = 1;

ثم القيام بعمليات معالجة ثم تنفيذ عملية update:

1update products set quantity = 2 where id = 1;

يمكنك القيام بعملية واحدة مباشرة:

1update products set quantity = quantity - 1;

الـ Lost Updates لا تحصل مع هذا النوع من الـ Queries.

ليست كل العمليات بهذه البساطة ويمكن تنفيذها ضمن Query واحد، لذلك يمكن اللجوء إلى حل آخر للعمليات الأكثر تعقيدا.

WARNING

حتى مع استخدام الـ Atomic Queries، قد تحصل الـ Lost Update إذا كنت تعتمد على القراءة بـ SELECT أولا (لم يتم كسر سلسلة Read-Modify-Write)، في حال احتياجك لـ SELECT استخدم الحل الثاني.

Exclusive Locks - Select For Update

قبل القيام بأي تعديل، يمكنك وضع Exclusive Lock في بداية الـ Transaction على الـ Records التي تقرأها وتحتاج لتعديلها دون تداخل من Transactions أخرى.

1select * from products where id = 1 for update;

جملة for update الموجودة في نهاية الـ Query تمنع باقي الـ Transactions من تعديل نفس الـ Record عن طريق وضع Lock عليه.

طبعا هذه العملية تعلّقُ عمل باقي الـ Transactions (أي Blocking)، كل الـ Transactions الأخرى ستنتظر انتهاء هذا الـ Transaction حتى تتمكن من إكمال تعديلاتها على الـ Record، لذلك يستحسن تنفيذها في Transactions قصيرة فقط.

مشاكل أخرى

توجد مشاكل أخرى لم أتطرق لها حتى لا أجعل المقال أطول مما هو عليه، قد أكتب عنها مستقبلا في قسم القصاصات وأحدث هذا المقال لإضافة روابطها، كان الهدف الأساسي من توضيح المشاكل السابقة هو توثيق وفهم أفكار محددة في بعض الـ Isolation Levels مثل الـ Lost Update وأنها غير محلولة دائمة بـ Repeatable Read Level.

أما باقي المشاكل مثل:

  • Dirty Writes

  • Read Skew

  • Phantom Reads

  • Write Skew

فبإمكانك الآن بسهولة البحث عنها ومعرفة المستوى الذي يحل كل واحدة منها، وقد أكتب عن بعضها لاحقا.

Durability - الحفاظ على البيانات من التلف

الـ Durability هي ضمان قاعدة البيانات للحفاظ على ما تم تخزينه، بمجرد عمل COMMIT للبيانات فلا يجب أن تُفقد وإن تعطلت قاعدة البيانات أو حتى قُطِعَت الكهرباء.

للـ Durability مستويات (أو إعدادات وخصائص)، قواعد البيانات المُستخدمة في الـ Transactions والعمليات (OLTP) تحاول غالبا ضمان مستوى عالٍ من الـ Durabilty بتفعيل جميع أو معظم خصائصه، على عكس الـ (OLAP) التي قد تخفف من الـ Durability قليلا لزيادة معدل كتابة البيانات (Throughput) لكن ذلك قد يؤدي إلى خسارة بعض البيانات حتى بعد انتهاء عملية Commit.

لا يُستحسَن التخفيف من مستوى الـ Durability إلا لو كنت تخزن بيانات غير حساسة كبعض الـ Logs أو الـ Audits التي لا ينبني عليها شيء مهم.

لن نفصل في المستويات (أو الإعدادات بالأصح) لأنها قد تختلف من قاعدة بيانات لأخرى، لكن سنشرح بشكل عام كيف تضمن قاعدة البيانات الـ Durability.

ما هي البيانات التي تُخزَّنُ نتيجة للـ Queries المُنَفَّذة؟

الـ Queries تخزن عددا من البيانات، عند تنفيذك لـ Query كهذا مثلا:

1insert into products (name, price) values ("Keyboard", 30.5);

ما يتبادر إلى ذهنك مباشرة هو تخزين الـ record على الـ Disk، لكن الواقع أعقد قليلا، فتوجد عدة أنواع من البيانات التي يتم تخزينها:

  1. يتم إضافة هذه البيانات إلى إحدى الـ Pages في الـ Disk، وهذا ما تستخدمه قاعدة البيانات لاحقا لقراءة الـ Record.

  2. تخزين بعض السجلات (Logs) التي سنتطرق إلى ذكرها بعد قليل.

  3. تخزين الـ Indexes التي تساعد على البحث في البيانات بشكل أسرع.

  4. بعض المعلومات الأخرى التي قد تحتاجها قاعدة البيانات لتنفيذ معمارية MVCC وغيرها.

لا يتم تخزين هذه البيانات كلها دفعة واحدة، بعضها يتم تخزينه مؤقتا في الـ Memory ثم يتم لاحقا كتابته على الـ Disk، وظيفة الـ Durability هنا هي تأكيد التخزين على الـ Disk بمجرد عمل COMMIT لضمان عدم فقدان البيانات.

Write-Ahead Logging (WAL)

مفهوم WAL هو من أهم المفاهيم في قاعدة البيانات، وهو نوع من الـ Logs الذي يدعم الإضافة فقط (Append-only)، كل Query يتم تنفيذه في الـ Transaction يكتب على الـ WAL في الـ Memory، وهنا قد يحصل نوعان من الأخطاء:

  1. خطأ يحصل قبل تنفيذ COMMIT

  2. خطأ يحصل بعد تنفيذ COMMIT

يتم التراجع عن كل التغييرات قبل تنفيذ COMMIT، وهذه ضمن مسؤولية الـ Atomicity، ما يهمنا هنا هو الخطأ الذي قد يحصل بعد الـ COMMIT، فالـ Durability تضمن الحفاظ على البيانات بعد الـ Commit.

قاعدة البيانات لا تُنفّذُ COMMIT وتؤكد للمستخدم اكتمال تنفيذها إلا إذا ثبتت الـ Logs الموجودة في الـ WAL وكتبتها على الـ Disk، لكنها لا تضمن أن الـ Indexes أو الـ Pages قد كُتبت على الـ Disk.

ماذا لو حصل خطأ؟ ألا يعني ذلك فقدان الـ Pages والـ Indexes؟

لا، لأن الكتابة في الـ WAL تكفي لتتبع التغييرات التي حصلت وإعادة كتابتها من جديد بعد إعادة تشغيل قاعدة البيانات (Redo & Recovery).

قد تحصل بالطبع مشكلة أخرى، وهي أن تنفيذ أمر الكتابة على الـ Disk هو إعطاء أمر لنظام التشغيل بالكتابة، لكن نظام التشغيل قد لا يَكتُب مباشرة، بل يفعلُ في ذاكرةٍ مؤقتة لتسريع عملية الكتابة ثم ينقُل الذاكرة المؤقتة إلى الـ Disk، لذلك تَستخدم قواعد البيانات الأمر fsync حتى تضمن الكتابة على الـ Disk قبل إرجاع نتيجة الـ COMMIT للمستخدم.

NOTE

الـ Durabilty هنا تضمن تثبيت بيانات الـ Memory على الـ Disk لأن الـ Memory ذاكرة متطايرة على خلاف الـ Disk، ليست من مسؤوليتها الحفاظ على البيانات لو تعطل الـ Disk، فحماية الـ Disk يتم بتقنيات مثل الـ Replication أو بعمل نسخ احتياطية بشكل دوري (Backups)

Consistency - صحة واتساق البيانات

الـ Consistency هي ضمان صحة البيانات والتزامها بقواعد محددة.

رغم وجود الـ Consistency في الترتيب الثاني في اختصار ACID، لكنني فضّلت تأجيلها لأنني لا أراها بأهمية باقي الخصائص لسببين:

  • الـ Atomicity والـ Isolation والـ Durability كلها تساهم في الـ Consistency بشكل أو بآخر.

  • الـ Consistency ليست ضمانا حقيقيا تقدمه قاعدة البيانات كما سأفصل أكثر لاحقا.

كيف تساهم باقي الخصائص في الـ Consistency

الـ Atomicity تضمن تنفيذ كل الـ Queries او التراجع عنها جميعا، فتمنع التعديل الجزئي (Partial Updates) حيث يضيف بعض البيانات في جدول ويفشل في إضافة بيانات متعلقة بها في جدول آخر، وهذا يضمن الـ Consistency.

تأثير الـ Durability مشابه، فالحفاظ على جميع البيانات المحفوظة هو حفاظ على الـ Consistency.

أما الـ Isolation فهي تمنع الـ Race Conditions التي قد تؤدي إلى إفساد البيانات أو فقدان بعضها.

كل هذه الخصائص تساهم في صحة البيانات واتساقها مع سياق النظام بشكل عام.

مشكلة الـ Consistency التي تجعلها مختلفة عن باقي خصائص ACID أنها تضمن “صحة” البيانات، لكن تعريف “الصحة” يعتمد بشكل أساسي على متطلبات النظام الذي يستخدم تلك البيانات.

لماذا لا تعد الـ Consistency ضمانا تقدمه قاعدة البيانات؟

مثال ذلك: وجود جدول للمستخدمين، وجدول للتقييمات على المنتجات، حيث يمكن للمستخدم إضافة تقييم مع تعليق على منتج معين.

لنفترض حذف أحد المستخدمين لحسابه في الموقع، ثمة طرق عدة للتعامل مع التقييمات والتعليقات التي نشرها:

  • حذف جميع تعليقات المستخدم المحذوف.

  • إبقاء التعليقات مع إبقاء اسم المستخدم عليها وإن كان حسابه محذوفا.

  • إبقاء التعليقات مع تغيير الاسم المعروض إلى “مستخدم محذوف”.

لا توجد طريقة واحدة صحيحة، فتعريف صحّة البيانات كما ترى يخضع لمتطلبات النظام وتعريف المبرمج لمعنى “صحة البيانات”، فحذف التعليقات في أحد الأنظمة قد يكون هو المطلوب بينما إبقاؤها هو المطلوب في نظام آخر.

أدوات أخرى توفرها قواعد البيانات للمساهمة في الـ Consistency

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

أول هذه الأدوات هو اختيار الـ Datatypes الصحيحة، فاستخدام INTEGER مثلا يمنع إدخال نصوص أو أرقام عشرية، واستخدام DATE للتواريخ يضمن صيغة صحيحة.

الـ Database Constraints من جهة أخرى، توفر طبقة حماية إضافية: الـ Foreign Key يمنع الإشارة إلى سجل غير موجود، والـ CHECK يتحقق من شروط معينة مثل balance >= 0، والـ UNIQUE INDEX يضمن عدم تكرار البيانات، والـ NOT NULL يمنع القيم الفارغة في الحقول المهمة.

وكل ذلك يعتمد على تعريف النظام أو المبرمج لصحة البيانات المطلوبة.