التزامن وسلامة البيانات: الأنماط اللي أنقذت الإنتاج عنا
أنماط تزامن إنتاجية لأنظمة المؤسسات. ملكية الحقول، القفل المتفائل، العقود التعاونية، مخازن الـ idempotency، إدارة الإصدارات، وطبقات حوكمة المعاملات.
حالة السباق اللي ما بتشوفها إلا بالإنتاج
حالات السباق غير مرئية بالتطوير. جهازك المحلي يشغّل عملية وحدة. مجموعة الاختبارات تشتغل بالتسلسل. كل شي يشتغل. بعدين تنشر على الإنتاج مع 4 pods للويب و3 pods للـ workers، وعمليتين يعدّلون نفس السجل بنفس الوقت. وحدة تكتب فوق تغييرات الثانية. البيانات تتلف بصمت. ما حدا ينتبه لحد ما يشتكي عميل.
صلّحنا حالات سباق عبر عدة أنظمة مؤسسات: منصات CMS مع 20 محرر وworkers بالخلفية، منصات تجارة مع معالجة طلبات متزامنة، وأنظمة ذكاء اصطناعي مع سير عمل agents متوازية. الأنماط بهالمقال هي اللي صمدت.
للسياق الأوسع، شوف دليل بنية الأنظمة ودليل البنية المدفوعة بالأحداث. لتزامن الـ CMS تحديداً، دليل سير عمل Pimcore يغطي هالأنماط بالتفصيل.
ملكية الحقول: مين يقدر يكتب شو
السبب الجذري لأغلب أخطاء التزامن بالمؤسسات: عدة كاتبين يعدّلون نفس السجل من خلال نفس مسار الحفظ بدون تنسيق.
نظام CMS عنده محررين يكتبون وصف المنتجات وworkers يولّدون صور مصغرة. كلهم ينادون save(). كلهم يحفظون الكائن كامل. إذا الـ worker حفظ بعد ما حمّل بس قبل ما يحفظ المحرر، حفظ المحرر يكتب فوق الصورة المصغرة حقت الـ worker. إذا المحرر حفظ أول، حفظ الـ worker يكتب فوق وصف المحرر.
الحل: خصّص كل حقل لمالك.
field_ownership:
Product:
editor_owned:
- name
- description
- images
system_owned:
- thumbnail
- searchIndex
- checksum
- lastSyncTimestamp
shared:
- categories
- price
- availability
| المجال | المالك | مسار التعديل | استراتيجية التعارض |
|---|---|---|---|
| ملك المحرر | مستخدمين الأدمن | حفظ عادي | ما في تعارض (بس المحررين يكتبون) |
| ملك النظام | Workers/تكاملات | طبقة معاملات مع أقفال | إعادة محاولة عند التعارض |
| مشترك | الاثنين | طبقة معاملات مع حل تعارضات | قابل للضبط: إعادة محاولة، تخطي، دمج |
الحقول الملك للمحرر تمر من مسار الحفظ العادي. الحقول الملك للنظام تمر من طبقة معاملات مع أقفال وفحوصات إصدار. الحقول المشتركة تستخدم استراتيجيات حل تعارضات صريحة.
بدون ملكية حقول، أنت تعتمد على الحظ. مع ملكية الحقول، النظام يفرض مين يقدر يكتب شو ويحل التعارضات بشكل حتمي.
القفل المتفائل مع فحوصات الإصدار
القفل المتفائل يفترض إنه التعارضات نادرة. بدل ما يقفل قبل التعديل، يتحقق إذا السجل تغيّر بين التحميل والحفظ.
async function updateWithOptimisticLock(
productId: string,
updateFn: (product: Product) => void,
maxRetries: number = 3,
): Promise<Product> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const product = await productRepo.findById(productId, { force: true });
const versionBefore = product.versionCount;
updateFn(product);
// تحقق إنه الإصدار ما تغيّر قبل الحفظ
const currentVersion = await productRepo.getVersionCount(productId);
if (currentVersion !== versionBefore) {
if (attempt === maxRetries - 1) {
throw new ConcurrencyError(
`Product ${productId} was modified concurrently (version ${versionBefore} -> ${currentVersion})`
);
}
continue; // أعد المحاولة ببيانات جديدة
}
await product.save();
return product;
}
}
فحص الإصدار مو ذري بهالمثال. للذرية الحقيقية، استخدم دعم قاعدة البيانات:
-- PostgreSQL: فحص إصدار ذري + تحديث
UPDATE products
SET name = $1, version_count = version_count + 1
WHERE id = $2 AND version_count = $3;
-- إذا 0 صفوف تأثرت: تم اكتشاف تعديل متزامن
// TypeORM: @VersionColumn للقفل المتفائل التلقائي
@Entity()
class Product {
@VersionColumn()
version!: number;
// TypeORM يتحقق من الإصدار تلقائياً عند الحفظ
// يرمي OptimisticLockVersionMismatchError عند التعارض
}
القفل المتفائل يشتغل كويس لما التعارضات نادرة (أقل من 5% من الكتابات). لسيناريوهات التنافس العالي (عدة workers يعالجون نفس السجل)، استخدم الأقفال التعاونية بدلاً.
أقفال العقد التعاونية
لما عدة workers يتنافسون على نفس المورد، القفل التعاوني يسلسل الوصول. بخلاف الـ mutexes الموزعة، الأقفال التعاونية تستخدم دلالات العقد: القفل ينتهي بعد TTL، مما يمنع الـ deadlocks من workers متوقفة.
// Redis: SET NX EX ذري مع ملكية قائمة على الرمز
class RedisLockProvider {
async acquire(key: string, ttlSeconds: number = 30): Promise<Lock | null> {
const token = crypto.randomBytes(16).toString('hex');
const acquired = await this.redis.set(key, token, 'NX', 'EX', ttlSeconds);
return acquired ? new Lock(key, token, ttlSeconds) : null;
}
async release(lock: Lock): Promise<void> {
// فحص وحذف ذري عبر سكريبت Lua
const script = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
`;
await this.redis.eval(script, 1, lock.key, lock.token);
}
async extend(lock: Lock, ttlSeconds: number): Promise<boolean> {
const script = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('expire', KEYS[1], ARGV[2])
else
return 0
end
`;
return !!(await this.redis.eval(script, 1, lock.key, lock.token, ttlSeconds));
}
}
نبضات القفل
الـ TTL لحاله مو كافي. إذا عملية شغّلت أطول من المتوقع، القفل ينتهي وworker ثاني ياخذه. هلق عندك اثنين workers يشتغلون بنفس الوقت.
async function executeWithLock(key: string, operation: () => Promise<void>) {
const lock = await lockProvider.acquire(key, 30);
if (!lock) throw new LockError(`Could not acquire lock: ${key}`);
// نبض: مدّد القفل إذا العملية أخذت وقت أطول
const heartbeat = setInterval(async () => {
const extended = await lockProvider.extend(lock, 30);
if (!extended) {
clearInterval(heartbeat);
// القفل انسرق، ألغِ العملية
throw new LockError(`Lock stolen during operation: ${key}`);
}
}, 15000); // مدّد كل 15 ثانية (50% من الـ TTL)
try {
await operation();
} finally {
clearInterval(heartbeat);
await lockProvider.release(lock);
}
}
تسلسل نطاق القفل
عمليات مختلفة تحتاج دقة قفل مختلفة:
| النطاق | نمط المفتاح | حالة الاستخدام |
|---|---|---|
| العنصر | lock:product:123 | حفظ الكائن كامل |
| مجموعة الحقول | lock:product:123:generatedAssets | تحديث جزئي (صور مصغرة فقط) |
| العملية | lock:product:123:thumbnail:en | عملية واحدة محددة |
نفس المنتج + نفس مجموعة الحقول -> انتظر/أعد المحاولة (تسلسلي)
نفس المنتج + مجموعات مختلفة -> متوازي (آمن)
منتجات مختلفة -> دائماً متوازي
النطاقات الأضيق تسمح بتوازي أكثر. الـ worker حق الصور المصغرة والـ indexer حق البحث يقدرون يعالجون نفس المنتج بنفس الوقت إذا قفلوا مجموعات حقول مختلفة.
مخازن الـ Idempotency
إعادة محاولات الشبكة، إعادة تسليم الرسائل، وإعادة تشغيل سير العمل تخلّي نفس العملية تنفذ عدة مرات. بدون idempotency، تحصل سجلات مكررة، أو شحنات مزدوجة، أو إيميلات مكررة.
interface IdempotencyEntry {
key: string; // مفتاح ذو معنى تجاري
scope: string; // فئة العملية
status: string; // PENDING | COMPLETED | FAILED
requestHash: string; // SHA-256 للمدخلات المنسقة
resultId?: string; // معرّف المورد المنشأ
expiresAt: Date; // TTL للتنظيف
createdAt: Date;
}
class IdempotencyStore {
async checkAndAcquire(key: string, scope: string, requestHash: string): Promise<IdempotencyResult> {
try {
await this.db.insert('idempotency_keys', {
key, scope, requestHash,
status: 'PENDING',
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000),
});
return { acquired: true };
} catch (error) {
if (isDuplicateKeyError(error)) {
const existing = await this.db.findOne({ key, scope });
if (existing.status === 'COMPLETED') {
return { acquired: false, cached: true, resultId: existing.resultId };
}
if (existing.status === 'PENDING' && isStale(existing)) {
// PENDING قديم: المحاولة السابقة تعطلت، اسمح بإعادة المحاولة
await this.db.update({ key, scope }, { status: 'PENDING', createdAt: new Date() });
return { acquired: true };
}
return { acquired: false, inProgress: true };
}
throw error;
}
}
async complete(key: string, scope: string, resultId: string): Promise<void> {
await this.db.update({ key, scope }, { status: 'COMPLETED', resultId });
}
}
تصميم المفتاح
مفتاح الـ idempotency لازم يكون ذو معنى تجاري:
| العملية | مكونات المفتاح | مثال |
|---|---|---|
| إضافة للمفضلة | customerId + productVariantId + wishlistId | wish:cust_123:var_456:wl_789 |
| إرسال تقييم | customerId + productId | review:cust_123:prod_456 |
| إرسال إشعار | recipient + category + entityRef + dayBucket | notify:sara@beispiel.de:stock:var_456:2026-04-20 |
| استبدال نقاط الولاء | customerId + orderId + points | redeem:cust_123:ord_789:500 |
| سجل استيراد ERP | sourceRecordId + importBatchId | import:erp_456:batch_20260420 |
نموذجين مختلفين للـ idempotency:
- Idempotency الـ API: للتعديلات اللي يبدأها المستخدم. العميل يوفر المفتاح أو يتولّد من hash المدخلات. الاستجابة المخزنة تُعاد عند التكرار.
- Idempotency المهام: للمعالجة بالخلفية. مفتاح dedupe بحمولة المهمة. يستخدم قيود قاعدة البيانات، علامات الإكمال، أو فحوصات المفتاح التجاري.
لا تخلطهم أبداً. عندهم دورات حياة واستراتيجيات تنظيف مختلفة.
مشكلة انفجار الإصدارات
بالأنظمة اللي فيها workers بالخلفية ينادون save()، كل عملية حفظ تنشئ إصدار. مع 6 workers يعالجون كل تغيير منتج، عملية حفظ واحدة من المحرر تولّد 6+ إصدارات غير ضرورية. خلال أشهر، المنتجات تراكم آلاف الإصدارات اللي تستهلك التخزين، تبطئ واجهة تاريخ الإصدارات، وتخلّي التغييرات التحريرية الفعلية مستحيلة تنلاقى.
الحل: حراس إصدار بنطاق محدد يمنعون إنشاء الإصدارات أثناء عمليات النظام مع الحفاظ على الإصدارات لحفظ المحررين.
// حارس إصدار بعد مرجعي
class ScopedVersionGuard {
private static refCount = 0;
suppress(): void {
ScopedVersionGuard.refCount++;
if (ScopedVersionGuard.refCount === 1) {
VersionManager.disable();
}
}
restore(): void {
ScopedVersionGuard.refCount--;
if (ScopedVersionGuard.refCount === 0) {
VersionManager.enable();
}
}
}
// الاستخدام: العمليات المتداخلة تشتغل صح
const outerGuard = new ScopedVersionGuard();
outerGuard.suppress();
try {
product.setThumbnail(asset);
product.save(); // ما ينشأ إصدار
const innerGuard = new ScopedVersionGuard();
innerGuard.suppress();
try {
product.setChecksum(hash);
product.save(); // لسا ما ينشأ إصدار
} finally {
innerGuard.restore(); // العدّاد ينزل من 2 لـ 1، لسا مكبوت
}
} finally {
outerGuard.restore(); // العدّاد ينزل من 1 لـ 0، الإصدارات ترجع تشتغل
}
النتيجة: حفظ المحررين ينشئ إصدارات (سجل التدقيق محفوظ). حفظ الـ workers ينشئ سجلات عمليات (مراقبة بدون انتفاخ الإصدارات).
لكيف ننفذ هالشي بـ Pimcore تحديداً، شوف دليل سير عمل Pimcore اللي يغطي حارس الإصدار حق PimTx بالتفصيل.
الأخطاء الشائعة
-
ما في ملكية حقول. بدونها، كل كاتب يتنافس على كل حقل من خلال نفس مسار الحفظ. حدّد مين يملك شو قبل ما تكتب أول سطر كود متزامن.
-
قفل متفائل بدون إعادة محاولة. اكتشاف التعارض مو كافي. العملية لازم تعيد المحاولة ببيانات جديدة. حدّد عدد أقصى لإعادة المحاولة وتعامل مع الاستنفاد بأناقة.
-
تعطيل قفل عام. علامة
disable()عامة تخرب لما عدة عمليات تشتغل بنفس الوقت. استخدم حراس بنطاق محدد وعد مرجعي. -
مفاتيح idempotency بدون معنى تجاري. UUID عشوائي كمفتاح idempotency ما يمنع شي. المفتاح لازم يشفّر العملية التجارية: مين، شو، متى.
-
ما في نبض على الأقفال الطويلة. إذا العملية أطول من الـ TTL، القفل ينتهي وworker ثاني يدخل. مدّد القفل عند 50% من الـ TTL.
-
تجاهل سجلات PENDING القديمة. إذا worker تعطّل وهو ماسك مفتاح idempotency بحالة PENDING، العملية محجوبة للأبد. اكتشف واسترجع السجلات القديمة.
-
القفل بالدقة الغلط. أقفال على مستوى العنصر تسلسل كل شي. أقفال مجموعات الحقول تسمح بالتوازي حيث هو آمن. اختر أضيق نطاق آمن.
-
ما في استراتيجية إدارة إصدارات. كل
save()ينشئ إصدار هو السلوك الافتراضي. بالأنظمة اللي فيها workers، هالشي ينشئ آلاف الإصدارات غير المفيدة. اكبت الإصدارات لعمليات النظام.
النقاط الرئيسية
-
ملكية الحقول تمنع أكثر حالات السباق شيوعاً. حدّد أي حقول ملك المحررين، أيها ملك الـ workers، وأيها مشتركة. سجل الملكية يحدد استراتيجية القفل وحل التعارضات.
-
القفل المتفائل للكتابات منخفضة التنافس. تحقق من عدد الإصدار قبل الحفظ. أعد المحاولة عند التعارض. استخدم دعم قاعدة البيانات (TypeORM
@VersionColumn، تحديث PostgreSQL الشرطي) للذرية. -
العقود التعاونية للموارد عالية التنافس. Redis SET NX EX مع ملكية قائمة على الرمز. نبض عند 50% من الـ TTL. سكريبتات Lua للعمليات الذرية. لا تستخدم mutexes موزعة بدون TTL أبداً.
-
مفاتيح الـ idempotency لازم تكون ذات معنى تجاري. شفّر دلالات العملية (مين + شو + متى) بالمفتاح. افصل idempotency الـ API عن idempotency المهام.
-
حراس الإصدار يحافظون على سجلات التدقيق بدون انفجار. كبت بعد مرجعي لعمليات النظام. التداخل يشتغل صح. حفظ المحررين لسا ينشئ إصدارات.
نطبق هالأنماط عبر مشاريع البرمجيات المخصصة وخطوط هندسة البيانات حقتنا. إذا عندك مشاكل تزامن بالإنتاج، تواصل مع فريقنا أو اطلب عرض سعر.
المواضيع المغطاة
أدلة ذات صلة
البنية المدفوعة بالأحداث عملياً: شو اللي فعلاً يخرب
أنماط بنية مدفوعة بالأحداث من الإنتاج الفعلي. عواصف الأحداث، حلقات المزامنة الثنائية، dead letters، مخازن الـ idempotency، والاختيار بين Kafka وRabbitMQ وBullMQ وSymfony Messenger.
اقرأ الدليلتصميم Workflow في Pimcore للمؤسسات: البنية اللي تتحمل 20 محرر
كيف تصمم workflows في Pimcore لفرق المؤسسات. فصل الحالة بثلاث طبقات، ملكية الحقول، التحكم بالأحداث، إدارة الإصدارات، وأمان استيراد ERP.
اقرأ الدليلالدليل الشامل لأنظمة الذكاء الاصطناعي الوكيلي
دليل تقني لأنظمة الذكاء الاصطناعي الوكيلي في بيئات الأعمال. تعرف على البنية والقدرات والتطبيقات العملية للوكلاء المستقلين.
اقرأ الدليلجاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟
فريقنا متخصص في بناء أنظمة ذكاء اصطناعي جاهزة للإنتاج. خلينا نحكي كيف نقدر نساعد.
ابدأ محادثة