مستويات العزل في قواعد البيانات: متى ترفعها ومتى تقفل

قبل أن ترفع مستوى العزل، اعرف أين تبدأ: الافتراضي يختلف بين المحرّكات. PostgreSQL يبدأ من Read Committed، وMySQL/InnoDB من REPEATABLE READ، وOracle من Read Committed مع منع القراءة القذرة إطلاقاً. وSQL Server محلياً من READ COMMITTED مع READ_COMMITTED_SNAPSHOT مطفأً، بينما Azure SQL Database من READ COMMITTED SNAPSHOT: الاسم نفسه والسلوك مختلف.
عملياً، ترحيل تطبيق من MySQL إلى PostgreSQL ينزل بك من Repeatable Read إلى Read Committed بصمت: قراءتان متتاليتان داخل المعاملة الواحدة قد تريان بيانات مختلفة، بلا خطأ يظهر.
ما يسمح به كل مستوى فعلاً
| المستوى | قراءة قذرة | قراءة غير قابلة للتكرار | قراءة شبحية | انحراف كتابة |
|---|---|---|---|---|
| Read Uncommitted | مسموح بالمعيار، ممنوع في PostgreSQL | مسموح | مسموح | مسموح |
| Read Committed | ممنوع | مسموح | مسموح | مسموح |
| Repeatable Read | ممنوع | ممنوع | مسموح بالمعيار وفي SQL Server، وممنوع في PostgreSQL وفي InnoDB بأقفال المفتاح التالي | مسموح |
| Serializable | ممنوع | ممنوع | ممنوع | ممنوع |
يقبل PostgreSQL المستويات الأربعة لكنه ينفّذ ثلاثة: Read Uncommitted فيه يسلك سلوك Read Committed تماماً، فلا قراءة غير مثبَّتة أصلاً.
انحراف الكتابة: ما لا يحلّه Repeatable Read
معاملة A تحسب SUM لصفوف class = 1 فتحصل على 30، وتدرجه في صف جديد بـclass = 2. وB تفعل العكس: تحسب SUM لصفوف class = 2 فتحصل على 300، وتدرجه في صف جديد بـclass = 1. عند Repeatable Read تُثبَّت المعاملتان معاً، والنتيجة لا تطابق أي ترتيب تسلسلي ممكن. وعند Serializable المبني على Serializable Snapshot Isolation تُثبَّت واحدة وتُرجَع الأخرى بـERROR: could not serialize access due to read/write dependencies among transactions.
وهذا شكل «آخر مقعد» و«حد أقصى للحجوزات» و«رصيد كافٍ»: تقرأ مجموعاً ثم تكتب بناءً عليه.
ثمن الرفع: إعادة المعاملة كاملة
الرفع ينقل العبء إلى تطبيقك. رمز SQLSTATE 40001 اسمه serialization_failure في PostgreSQL، والجمود 40P01. وحين يصلك أحدهما، أجهض المعاملة وأعدها من بدايتها كاملة، لا العبارة الفاشلة وحدها. وبلا منطق إعادة محاولة لم تصلح فساد البيانات، بل حوّلته إلى أخطاء 500. وإذا كان للمعاملة أثر خارجي كاستدعاء بوابة دفع، فأنت تحتاج حارساً على مستوى الطلب أيضاً، وهنا يفيد مفتاح idempotency لمنع تكرار الطلب في واجهات الدفع.
في MySQL التمييز أدق: الجمود ER_LOCK_DEADLOCK رقم 1213 بـSQLSTATE 40001 يُرجع المعاملة كاملة، أما مهلة انتظار القفل ER_LOCK_WAIT_TIMEOUT رقم 1205 بـSQLSTATE HY000 فتُرجع العبارة الفاشلة وحدها، ومعالجتهما بكود واحد خطأ. ولإرجاع المعاملة كاملة عند المهلة شغّل الخادم بـinnodb-rollback-on-timeout.
والصياغة سطر واحد: BEGIN ISOLATION LEVEL SERIALIZABLE; في PostgreSQL، وSET TRANSACTION ISOLATION LEVEL READ COMMITTED; في MySQL — وهذه الأخيرة بدون GLOBAL أو SESSION تنطبق على المعاملة التالية وحدها، ولا يجوز تنفيذها داخل معاملة.
فخّ MySQL: تحذف صفوفاً لا تراها
عند REPEATABLE READ تُلتقط اللقطة من أول قراءة، وكل SELECT لاحق يقرأ اللقطة نفسها. لكنها لا تنطبق بالضرورة على عبارات التعديل: UPDATE وDELETE يقرآن أحدث نسخة مثبَّتة. والنتيجة أن SELECT COUNT(c1) FROM t1 WHERE c1 = 'xyz' يعيد 0، ثم DELETE FROM t1 WHERE c1 = 'xyz' يحذف عدة صفوف. فإن أردت أحدث حالة فانزل إلى READ COMMITTED أو اقرأ بقفل مثل SELECT ... FOR SHARE.
غالباً القفل الصريح أرخص من الرفع
ما دمت لا تستخدم Serializable، فلا سبيل إلى ضمان صحة صف وحمايته من تحديث متزامن إلا عبر SELECT FOR UPDATE أو SELECT FOR SHARE أو قفل جدول مناسب. ورفع العزل لا يلغي قيود الفرادة على مستوى المخطط، وهي أرخص دفاع ضد التكرار.
وانتبه لحدود هذا القفل: SELECT FOR UPDATE في PostgreSQL يقفل الصف فعلاً ويمنع أي UPDATE أو DELETE أو SELECT FOR UPDATE آخر عليه حتى تنتهي معاملتك؛ لكنه لا يحمي من صف جديد يُدرَج لاحقاً فيخالف الشرط نفسه — ذاك انحراف كتابة يحتاج Serializable لا قفل صف واحد. وإن اعتمدت القفل الصريح فاستخدم Read Committed، أو خذ الأقفال قبل الاستعلامات عند Repeatable Read، وإلا سبقت اللقطة تغييرات مثبَّتة.
جدول القرار
| الحالة | ماذا تختار |
|---|---|
| تحويل مالي مع فحص رصيد | Serializable مع إعادة محاولة تلقائية، أو Read Committed مع SELECT … FOR UPDATE على صف الحساب |
| حجز أو مخزون بشرط تجميعي | انحراف كتابة صريح: Serializable، أو قفل صريح على صف الأب، أو قيد فرادة يمنع الحجز المكرر |
| تقرير طويل يمسح ملايين الصفوف | SERIALIZABLE READ ONLY DEFERRABLE في PostgreSQL: بلا عبء Serializable المعتاد ولا خطر إلغائه بفشل تسلسل، لكنه قد يتوقف عند بدايته حتى يأخذ لقطة آمنة |
| لوحة تحكم قراءة غالبة | ابقَ على الافتراضي |
| عمّال خلفيون على جدول طابور | Read Committed مع SELECT … FOR UPDATE SKIP LOCKED؛ يعطي عرضاً غير متسق فلا يصلح لأغراض عامة |
إذا كنت على Aurora MySQL
نسخ القراءة تعمل افتراضياً على REPEATABLE READ وتتجاهل أي SET TRANSACTION ISOLATION LEVEL ترسله إليها. ويمكن تفعيل READ COMMITTED بإعداد الجلسة aurora_read_replica_read_committed، لكن العزل حينها أقل صرامة منه على النسخة الرئيسية ويسمح بقراءات غير قابلة للتكرار وقراءات شبحية. فهو للتقارير التحليلية الضخمة، لا للاستعلامات القصيرة التي تحتاج دقة.
اقرأ أيضاً
- UUID أم bigint: كيف تختار المفتاح الأساسي لجدولك
- مشكلة N+1 في الاستعلامات: كيف تكتشفها وتحلها في كل إطار عمل مشكلة أداء أخرى شائعة في طبقة قواعد البيانات، لكن سببها عدد الاستعلامات لا مستوى العزل.
- تجمّع الاتصالات في قاعدة البيانات: اضبط الحجم والمهلات المعاملة الطويلة التي تنتظر قفلاً تحتجز اتصالاً من التجمّع أيضاً، وهناك تجد مهلة
idle_in_transaction_session_timeoutواستعلام كشف الجلسات العالقة.