SQLite / المتقدم: التزامن والاسترداد وحدود التطبيق / الدرس 13
التزامن وWAL وتعارض لقطات القراءة
افهم تسلسل الكتابة ولقطات القراءة في SQLite، وفسّر تعارضاً محدوداً على ملفات مؤقتة خاصة، واختر سياسات WAL والإعادة وفق الإصدار الفعلي.
في هذه الصفحة
أهداف الدرس
- تتبع لقطة قارئ أثناء اعتماد تغيير عبر اتصال آخر.
- تمييز سياقات BUSY وLOCKED واللقطة المتقادمة.
- مراجعة ملفات WAL ونقاط الفحص ومتطلبات التصحيح.
تغيّر WAL تفاعل القارئ ولا تلغي قاعدة الكاتب الواحد
تدعم SQLite قراءً متعددين، لكن كاتباً نشطاً واحداً فقط لكل قاعدة في اللحظة نفسها. تتيح WAL للقارئ الاحتفاظ بلقطة أقدم بينما يعتمد الكاتب محتوى جديداً؛ ولا تنشئ كتّاباً متزامنين بلا حد أو تزيل كل حالات الانشغال. تؤثر مدة المعاملة وعدد الاتصالات، لكن إضافة مخزون اتصالات لا تصنع سعة كتابة تتجاوز نموذج القفل.
تختلف تفاصيل التنسيق بين نمطي سجل التراجع وWAL. وثّق النمط الفعلي وتسلسل المعاملات وإعدادات واجهة المضيف. لا تتحول قواعد :memory: إلى قواعد ملفات WAL معتادة بمجرد طلب pragma. لذلك تستخدم الأمثلة ملفات مؤقتة خاصة جديدة، وتغلق كل اتصال قبل حذف مجلدها؛ ولا تفتح قاعدة المشروع أو خدمة مشتركة.
ملاحظة قارئ ثابت ثم رؤية لاحقة
يقرأ برنامج Python الأول القيمة 1 داخل معاملة قراءة مفتوحة، ثم يعتمد الكاتب تغييرها إلى 2. تبقى قراءة الاتصال نفسه 1، وبعد إنهاء المعاملة ترى قراءته التالية 2. هذا ترتيب متتابع مضبوط باتصالين، لا اختبار حمل متعدد الخيوط ولا قياس إنتاجية.
يتحقق المثال من تفعيل WAL ولا يفترضه. يدخل دعم نظام الملفات وواجهة VFS ضمن البيئة. تتطلب WAL عادة تنسيق ذاكرة مشتركة على المضيف نفسه، وليست حلاً عاماً لأنظمة الملفات الشبكية. يجب أن تغطي الأذونات سلوك القاعدة والسجل والذاكرة المشتركة، لا قابلية قراءة الملف الرئيسي ظاهرياً فقط.
import sqlite3
from pathlib import Path
from tempfile import TemporaryDirectory
from contextlib import closing
with TemporaryDirectory(prefix="sqlite-wal-training-") as temporary:
path = Path(temporary) / "training.db"
with closing(sqlite3.connect(path, isolation_level=None)) as writer, \
closing(sqlite3.connect(path, isolation_level=None)) as reader:
assert writer.execute("PRAGMA journal_mode=WAL").fetchone()[0] == "wal"
writer.executescript("CREATE TABLE item(value INTEGER); INSERT INTO item VALUES(1);")
reader.execute("BEGIN")
first = reader.execute("SELECT value FROM item").fetchone()[0]
writer.execute("UPDATE item SET value=2")
same = reader.execute("SELECT value FROM item").fetchone()[0]
reader.execute("COMMIT")
later = reader.execute("SELECT value FROM item").fetchone()[0]
print("snapshot", first, same, later)
snapshot 1 1 2
يحفظ القارئ المفتوح 1 عبر اعتماد الكاتب، وترى معاملة قراءة جديدة 2. لا يختبر المثال سباق كتابة ونقطة فحص متزامنين.
معالجة التعارض عند حد يراعي النتيجة
يمسك البرنامج الثاني معاملة كتابة ويمنح اتصالاً ثانياً مهلة انتظار صفراً، فتظهر SQLITE_BUSY المعروفة. بعد أن يحرر التراجع الكاتب الأول، يستطيع الثاني البدء والاعتماد. يميز ذلك توفر دور الكاتب من أمان إعادة طلب عمل بعينه. لا ينشئ البرنامج خيوطاً ولا يكرر حملاً.
قد تنتظر مهلة الانشغال تعارضاً عابراً، لكنها لا تصلح لقطة قراءة متقادمة. تتطلب SQLITE_BUSY_SNAPSHOT ترك اللقطة القديمة وإعادة العملية المؤهلة كاملة بقراءات جديدة. وتشير SQLITE_LOCKED غالباً إلى تفاعل مختلف لاتصال أو ذاكرة تخزين مشتركة أو عبارة. احفظ معلومات الخطأ الممتد وسياقه بدلاً من توحيد كل رسالة قفل.
import sqlite3
from pathlib import Path
from tempfile import TemporaryDirectory
from contextlib import closing
with TemporaryDirectory(prefix="sqlite-busy-training-") as temporary:
path = Path(temporary) / "training.db"
with closing(sqlite3.connect(path, isolation_level=None, timeout=0)) as first, \
closing(sqlite3.connect(path, isolation_level=None, timeout=0)) as second:
first.executescript("CREATE TABLE item(value INTEGER); INSERT INTO item VALUES(1);")
first.execute("BEGIN IMMEDIATE")
try:
second.execute("BEGIN IMMEDIATE")
except sqlite3.OperationalError as error:
assert error.sqlite_errorcode == sqlite3.SQLITE_BUSY
print("contender", error.sqlite_errorname)
else:
raise AssertionError("expected bounded writer contention")
finally:
first.execute("ROLLBACK")
second.execute("BEGIN IMMEDIATE")
second.execute("UPDATE item SET value=3")
second.execute("COMMIT")
print("after_release", second.execute("SELECT value FROM item").fetchone()[0])
contender SQLITE_BUSY
after_release 3
يرفض الكاتب الثاني ما دام الأول يملك المعاملة، ثم ينجح بعد تراجع صريح.
إيضاح ملكية نقاط الفحص والملفات المصاحبة
تنقل نقاط الفحص محتوى WAL دورياً إلى القاعدة. قد تمنع قراءة طويلة اكتمال نقطة الفحص أو إعادة استخدام WAL، فيكون نموها عرضاً لعمر المعاملة لا إذناً بحذفها. يشكل الملفان -wal و-shm جزءاً من الحالة الحية والتنسيق. وقد يغفل نسخ الرئيسي وحده أثناء العمل محتوى معتمداً.
اختر النسخ ونقاط الفحص وفق توثيق الإصدار والحمل. لا تحذف الملفات المصاحبة أو تفرض نقاط فحص اعتباطية أو تغير synchronous كحل عام للانشغال. راجع القراء الطويلين والعبارات المفتوحة ومدة الكتابة والتخزين والإلغاء. حدّد الإعادة لتجنب عاصفة محاولات متزامنة، ولا تكرر أثراً خارجياً غير قابل للعكس عميانياً.
فصل السلوك المختبر من إرشادات تصحيح الإنتاج
يصف توثيق WAL الرسمي المراجع في 2026-10-25 سباق إعادة ضبط WAL، المصحح في 3.51.3 والترقيعات المختارة 3.44.6 و3.50.7. يتضمن السيناريو تزامن الكتابة ونقاط الفحص بين اتصالات، وقد يفسد القاعدة. لذلك يمثل إصدار التحقق 3.45.1 هدفاً تعليمياً تاريخياً لا توصية لنشر WAL إنتاجياً. لا تحاول الأمثلة إعادة إنتاج السباق.
افحص بناء المورّد والتصحيح المنطبق فعلياً، ولا تقارن نصوص الإصدارات معجمياً أو تفترض أن الإطار يضم محركاً مصححاً. احفظ نسخة قابلة للاستعادة واختبر تزامن التطبيق ببرنامج مدعوم ومصحح. إذا تجاوزت متطلبات تعدد الكتّاب أو مشاركة الملف عن بُعد أو التشغيل النموذج المضمن، فقيّم بنية تخزين أخرى بدلاً من وعد WAL بإزالة كل قيد.
الأخطاء الشائعة وإصلاحها
database is locked
- لماذا يحدث؟
- يملك اتصال آخر مورداً غير متوافق، أو تتعذر ترقية لقطة القراءة.
- كيف تصلحه؟
- صنّف الخطأ الممتد وحالة المعاملة قبل إعادة محدودة.
- كيف تتجنب تكراره؟
- قصّر المعاملات واختبر ترتيبات التداخل.
نمو WAL مع بقاء قارئ مفتوح.
- لماذا يحدث؟
- يحتفظ القارئ بلقطة قديمة تحتاجها آلية نقطة الفحص وإعادة الاستخدام.
- كيف تصلحه؟
- أنه معاملة القراءة المملوكة وافحص حالة نقطة الفحص الموثقة.
- كيف تتجنب تكراره؟
- تتبع عمر القارئ وتنظيف الإلغاء.
اعتبار مثال 3.45.1 نصيحة تصحيح.
- لماذا يحدث؟
- الخلط بين هوية بيئة التحقق وملاءمتها الحالية للإنتاج.
- كيف تصلحه؟
- راجع ملاحظات تصحيح WAL الرسمية وحالة ترقيعات المورّد.
- كيف تتجنب تكراره؟
- وثّق دعم الميزات وسياسة التصحيح منفصلين.
اختبر فهمك
1. لماذا قد يرى قارئ WAL القيمة 1 بعد اعتماد اتصال آخر القيمة 2؟
اعرض الحل المشروح
تحتفظ معاملة القراءة النشطة باللقطة السابقة. أنهها وابدأ عملاً جديداً لرؤية الاعتماد اللاحق. لا يثبت ذلك فشل الاعتماد، ولا ينبغي ترقية لقطة متقادمة إلى محاولة كتابة عمياء.
2. هل تحل مهلة انشغال طويلة كل تعارض كتابة؟
اعرض الحل المشروح
لا؛ قد تنتظر بعض التعارضات، لكنها لا تنشئ كتّاباً متعددين أو تصلح لقطة متقادمة. صنّف العملية واستخدم سياسة محدودة تعيد المعاملة كاملة عند الحاجة، مع حفظ هوية الطلب وأمان الآثار الخارجية.
أهم ما تعلمته
- تحفظ WAL تسلسل الكاتب الواحد مع محاذير تعتمد على الإصدار.
- تحتاج نتائج الانشغال تصنيفاً يراعي المعاملة.
- تشكل الملفات المصاحبة ونقاط الفحص والنسخ وحالة التصحيح عقد تشغيل واحداً.