التخزين المؤقت والنسخ والتقسيم
درس 2 من 3
2التخزين المؤقت والنسخ والتقسيم
الصورة الكبيرةأغلب التطبيقات تقرأ أكثر بكثير مما تكتب. كل قراءة تصل للقاعدة تكلّف. الذاكرة المؤقتة تحضر من الذاكرة، والنسخ توزع القراءات، والتقسيم يوزع البيانات نفسها حين لا تتسع لخادم.
ماذا تشاهد في الرسم: مسار القراءة يتوقف عند أول طبقة تملك الجواب. الكتابة تذهب للأساسية دائمًا ثم تُنسخ.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
في المقهى، القائمة لا تتغير كل دقيقة، فتطبعها وتعلقها بدل أن يسأل كل زبون المدير. هذه الذاكرة المؤقتة. وحين يكثر الزبائن تطبع نسخًا للطاولات: هذه نسخ القراءة. لكن لو غيّرت سعرًا ونسيت نسخة قديمة على طاولة، سيرى زبون السعر القديم: هذه مشكلة الإبطال.
أنماط التخزين المؤقت: cache-aside (التطبيق يقرأ ويملأ)، write-through (الكتابة للاثنين)، write-behind (الكتابة للذاكرة ثم القاعدة لاحقًا). الطرد: LRU، LFU، وTTL. الإبطال عند الكتابة أصعب جزء، ومشكلة الاندفاع (cache stampede) تُحل بقفل أو تجديد مبكر. نسخ القراءة: الأساسية تكتب، والنسخ تتبع بتأخر (replication lag)، فقراءة ما كتبته للتو قد تحتاج الأساسية (read-your-writes). التقسيم: توزيع الصفوف على قواعد حسب مفتاح، والتجزئة البسيطة hash % N تنقل أغلب المفاتيح عند تغير N، بينما التجزئة المتسقة تنقل نحو 1/N فقط. ونظرية CAP: عند الانقسام الشبكي تختار الاتساق أو الإتاحة.
قبل أن تبدأ
⚖️التوسع
الدرس السابق.
🍃NoSQL وRedis
درس NoSQL في مسار قواعد البيانات.
الفكرة الجوهرية
1ذاكرة LRU بنفسك
LRU يطرد الأقدم استخدامًا حين تمتلئ الذاكرة. OrderedDict يجعلها بضعة أسطر:
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity, self.data = capacity, OrderedDict()
self.hits = self.misses = 0
def get(self, key, load):
if key in self.data:
self.hits += 1
self.data.move_to_end(key)
return self.data[key]
self.misses += 1
value = load(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False)
return value
cache = LRUCache(capacity=2)
for product in ["latte", "mocha", "latte", "tea", "mocha", "latte"]:
cache.get(product, load=lambda k: f"{k} from db")
print("hits:", cache.hits, "misses:", cache.misses, "kept:", list(cache.data))2نسخ القراءة وتأخرها
المستخدم يغيّر اسمه ثم يفتح ملفه فورًا، والطلب يذهب لنسخة لم تصلها الكتابة بعد. محاكاة:
primary = {"name": "Sara"}
replica = dict(primary)
pending = [] # writes not yet copied
def write(key, value):
primary[key] = value
pending.append((key, value))
def replicate():
while pending:
key, value = pending.pop(0)
replica[key] = value
write("name", "Sara A.")
print("replica right after write:", replica["name"])
print("primary right after write:", primary["name"])
replicate()
print("replica after catch-up :", replica["name"])3التجزئة المتسقة
بـ hash % N، إضافة قاعدة رابعة تنقل أغلب المفاتيح. التجزئة المتسقة تضع القواعد على دائرة، وكل مفتاح يذهب لأول قاعدة بعده، فالإضافة تنقل جزءًا صغيرًا فقط:
import bisect
import hashlib
def h(text):
return int(hashlib.md5(text.encode()).hexdigest(), 16)
def ring(nodes, replicas=50):
points = sorted((h(f"{n}#{i}"), n) for n in nodes for i in range(replicas))
return [p for p, _ in points], [n for _, n in points]
def owner(key, hashes, names):
i = bisect.bisect(hashes, h(key)) % len(hashes)
return names[i]
keys = [f"user:{i}" for i in range(1000)]
before = {k: f"db{h(k) % 3}" for k in keys}
after = {k: f"db{h(k) % 4}" for k in keys}
print("modulo moved :", sum(before[k] != after[k] for k in keys), "of 1000")
r3, r4 = ring(["db0", "db1", "db2"]), ring(["db0", "db1", "db2", "db3"])
moved = sum(owner(k, *r3) != owner(k, *r4) for k in keys)
print("consistent moved :", moved, "of 1000")مثال محلول
المسألة: صفحة المنتج تعمل 8 استعلامات، والقاعدة عند 90% في الذروة، و95% من الطلبات قراءة. الحل: تخزين صفحة المنتج كاملة في Redis بمدة 5 دقائق، ومسحها عند تعديل المنتج، ونسخة قراءة لصفحات البحث. النتيجة: نسبة الإصابة 92% والقاعدة نزلت إلى 25%، دون تقسيم ودون تغيير في البنية الأساسية.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| خزّن كل شيء مؤقتًا. | ما يتغير كثيرًا أو يُقرأ نادرًا يضيف تعقيد الإبطال دون فائدة. |
| التقسيم أول حل للقاعدة البطيئة. | الفهارس، ثم الذاكرة المؤقتة، ثم نسخ القراءة، ثم خادم أكبر. التقسيم آخر الخيارات لأنه يعقّد كل شيء. |
اختبر فهمك
ذاكرة LRU ممتلئة وتحتاج مكانًا. ما الذي يُطرد؟
LRU = Least Recently Used.
مستخدم عدّل ملفه ولا يرى التعديل. السبب المرجح؟
تأخر النسخ أو الإبطال المنسي.
ميزة التجزئة المتسقة؟
modulo ينقل أغلب المفاتيح عند تغير العدد.
بطاقات مراجعة سريعة
تحقق من فهمك
ما أصعب جزء في التخزين المؤقت؟
الإبطال: معرفة متى تصبح القيمة المخزنة قديمة ومسحها.
لماذا لا تذهب الكتابة لنسخ القراءة؟
لأن مصدر الحقيقة واحد (الأساسية)، والنسخ تتبعه.
متى تفكر في التقسيم؟
حين لا تتسع الكتابات أو البيانات لخادم واحد حتى بعد الفهارس والذاكرة والنسخ وخادم أكبر.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.