08 أكتوبر 2025·8 دقيقة قراءة

ترقيم الفواتير الآمن من حالات التزامن لتجنّب التكرارات والفجوات

تعرّف على أنماط عملية لترقيم الفواتير مقاوم للتزامن بحيث يمكن لعدة مستخدمين إنشاء فواتير أو تذاكر دون تكرار أو فجوات غير متوقعة.

ترقيم الفواتير الآمن من حالات التزامن لتجنّب التكرارات والفجوات

ما الذي يحدث عندما ينشئ شخصان سجلات في نفس الوقت

تخيل مكتبًا مزدحمًا عند الساعة 4:55 مساءً. اثنان من الموظفين ينتهيان من فاتورة ويضغطان حفظ في غضون ثانية من بعضهما. على الشاشتين يظهر لفترة قصيرة "الفاتورة #1042". يسجل أحدهما فقط، بينما يفشل الآخر، أو الأسوأ من ذلك، كلا السجلين يُخزَّن بنفس الرقم. هذا هو العرض العملي الأكثر شيوعًا: أرقام مكررة تظهر فقط تحت حمل مرتفع.

التذاكر تتصرف بالمثل. عميلان ينشئان تذكرة جديدة لنفس الزبون في نفس اللحظة، ونظامك يحاول "التقاط الرقم التالي" عن طريق النظر إلى آخر سجل. إذا قرأت الطلبات القيمة "الأخيرة" نفسها قبل أن يكتب أي منهما، فيمكن أن يختارا نفس الرقم التالي.

العَرَض الثاني أكثر دقة: تخطّي الأرقام. قد ترى #1042 ثم #1044 مع غياب #1043. يحدث هذا غالبًا بعد خطأ أو إعادة محاولة. يخصّ أحد الطلبات رقمًا ثم يفشل الحفظ بسبب خطأ تحقق من الصحة، أو مهلة، أو إغلاق المستخدم لعلامة التبويب. أو قد تعيد مهمة خلفية المحاولة بعد تقطع الشبكة وتأخذ رقمًا جديدًا بينما المحاولة الأولى قد استهلكت واحدًا.

لفواتير، هذا مهم لأن الترقيم جزء من مسار التدقيق. يتوقع المحاسبون تحديد كل فاتورة بشكل فريد، وقد يشير الزبائن إلى أرقام الفواتير في المدفوعات أو رسائل الدعم. بالنسبة للتذاكر، الرقم هو المعرف الذي يستخدمه الجميع في المحادثات والتقارير والتصدير. التكرارات تخلق ارتباكًا. الأرقام المفقودة قد تثير تساؤلات أثناء المراجعات، حتى لو لم يحدث شيء غير شريف.

إليك التوقع الرئيسي الذي يجب توضيحه مبكرًا: ليست كل طريقة ترقيم يمكن أن تكون آمنة ضد التزامن وبدون فجوات في الوقت نفسه. الترقيم المقاوم للتزامن (دون تكرارات، حتى مع وجود مستخدمين متعددين) قابل للتحقيق ويجب أن يكون أمرًا لا تفاوض فيه. الترقيم بدون فجوات ممكن أيضًا، لكنه يتطلب قواعد إضافية وغالبًا يغير كيفية تعاملِك مع المسودات، والإخفاقات، والإلغاءات.

طريقة جيدة لإطار المشكلة هي أن تسأل ماذا يجب أن تضمن أرقامك:

  • يجب ألا تتكرر أبدًا (فريدة، دائمًا)
  • يجب أن تكون متزايدة في الغالب (مرغوب)
  • يجب ألا تتخطى أبدًا (فقط إن صممت لها)

بمجرد اختيارك للقاعدة، يصبح الحل التقني أسهل للاختيار.

لماذا تحدث التكرارات والفجوات

تتبع معظم التطبيقات نمطًا بسيطًا: المستخدم يضغط حفظ، التطبيق يطلب الرقم التالي للفاتورة أو التذكرة، ثم يُدرَج السجل الجديد بهذا الرقم. يبدو آمنًا لأنه يعمل تمامًا عندما يكون مستخدم واحد فقط.

تبدأ المشكلة عندما يحدث حفظان تقريبًا في نفس الوقت. يمكن أن تصل كلا الطلبين إلى خطوة "الحصول على الرقم التالي" قبل أن ينهي أي منهما الإدراج. إذا رأت القراءتان القيمة "التالي" نفسها، فسيحاولان كلاهما كتابة نفس الرقم. هذه حالة سباق: النتيجة تعتمد على التوقيت، لا على المنطق.

المخطط الزمني النموذجي يكون كالتالي:

  • يقرأ الطلب A الرقم التالي: 1042
  • يقرأ الطلب B الرقم التالي: 1042
  • يدخِل الطلب A الفاتورة 1042
  • يحاول الطلب B إدخال الفاتورة 1042 (أو يفشل إذا منعت قاعدة فريدة ذلك)

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

الفجوات مشكلة مختلفة. تحدث عندما "تحجز" نظامك رقمًا، لكن السجل لا يصبح فاتورة أو تذكرة مُلتزمة فعلًا. الأسباب الشائعة هي فشل المدفوعات، أخطاء التحقق المتأخرة، المهلات، أو إغلاق المستخدم لعلامة التبويب بعد تخصيص الرقم. حتى لو فشل الإدراج ولم يُحفظ شيء، قد يكون الرقم قد استُهلك بالفعل.

التزامن المخفي يجعل الأمر أسوأ لأنه نادرًا ما يكون مجرد "اثنين من البشر يضغطان حفظًا". قد يكون لديك أيضًا:

  • عملاء API ينشئون سجلات بشكل متوازي
  • استيرادات تعمل على دفعات
  • مهام خلفية تُولد فواتير ليلًا
  • إعادة محاولات من تطبيقات موبايل باتصال متقطع

إذًا الأسباب الجذرية هي: (1) تعارضات التوقيت عندما تقرأ طلبات متعددة نفس قيمة العداد، و(2) تخصيص الأرقام قبل أن تتأكد أن الترانزكشن سينجح. أي خطة لترقيم مقاوم للتزامن يجب أن تقرر أي نتيجة يمكنك تحملها: لا تكرارات، لا فجوات، أو كلاهما، وتحت أي أحداث بالضبط (مسودات، إعادة محاولات، إلغاءات).

قرّر قاعدة الترقيم قبل اختيار الحل

قبل تصميم ترقيم مقاوم للتزامن، اكتب ما يجب أن يعنيه الرقم في عملك. الخطأ الأكثر شيوعًا هو اختيار طريقة تقنية أولًا ثم اكتشاف أن القواعد المحاسبية أو القانونية تتوقع شيئًا مختلفًا.

ابدأ بفصل هدفين غالبًا ما يختلطان:

  • فريد: لا توجد فاتورتان أو تذكرتان تشتركان في نفس الرقم.
  • بدون فجوات: الأرقام فريدة وأيضًا متتالية بصرامة (لا أرقام مفقودة).

العديد من الأنظمة الحقيقية تهدف إلى "فريد فقط" وتقبل الفجوات. يمكن أن تحدث الفجوات لأسباب طبيعية: فتح المستخدم لمسودة ثم التخلي عنها، فشل دفعة بعد تخصيص الرقم، أو إنشاء سجل ثم إلغاؤه. للتذاكر، عادةً الفجوات لا تهم. حتى للفواتير، يقبل كثير من الفرق الفجوات إذا كان بالإمكان شرحها في مسار تدقيق (ملغاة، ملغاة، اختبار، الخ.). ترقيم بدون فجوات ممكن، لكنه يفرض قواعد إضافية وغالبًا يضيف احتكاكًا.

بعد ذلك، قرّر نطاق العداد. اختلاف صياغة صغيرة يغيّر التصميم كثيرًا:

  • هل تسري سلسلة واحدة على كل شيء أم سلاسل منفصلة لكل شركة/مستأجر؟
  • هل تُعاد الضبط كل سنة (2026-000123) أم لا تُعاد أبدًا؟
  • هل توجد سلاسل مختلفة للفواتير مقابل الملاحظات الائتمانية مقابل التذاكر؟
  • هل تحتاج إلى صيغة صديقة للبشر (بادئات، فواصل) أم مجرد رقم داخلي؟

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

إذا كنت تحتاج فعلاً إلى "بدون فجوات"، كن صريحًا بشأن الأحداث المسموح بها بعد تخصيص رقم. على سبيل المثال، هل يمكن حذف فاتورة، أم تُلغى فقط؟ هل يمكن للمستخدمين حفظ مسودات بدون رقم وتخصيص الرقم عند الاعتماد النهائي؟ هذه الاختيارات غالبًا ما تكون أهم من تقنية قاعدة البيانات.

اكتب القاعدة في SPEC قصيرة قبل البناء:

  • ما أنواع السجلات التي تستخدم السلسلة؟
  • ما الذي يجعل الرقم "مستخدمًا" (مسودة، مُرسلة، مدفوعة)؟
  • ما النطاق (عالمي، لكل شركة، لكل سنة، لكل سلسلة)؟
  • كيف تتعامل مع الإلغاءات والتصحيحات؟

في AppMaster، هذا النوع من القواعد يوضع بجانب نموذج البيانات وتدفق العمليات، حتى ينفّذ الفريق السلوك نفسه في كل مكان (API، واجهة الويب، والموبايل) بدون مفاجآت.

النهج الشائعة وماذا يضمن كل منها

عند الحديث عن "ترقيم الفواتير"، غالبًا ما يختلط الناس بين هدفين مختلفين: (1) عدم توليد نفس الرقم مرتين، و(2) عدم وجود فجوات. معظم الأنظمة يمكن أن تضمن الأولى بسهولة. الثانية أصعب لأن الفجوات يمكن أن تظهر أي وقت يفشل فيه ترانزكشن، تُهجر مسودة، أو يُلغى سجل.

النهج 1: تسلسل قاعدة البيانات (تفرد سريع)

تسلسل PostgreSQL هو أبسط طريقة للحصول على أرقام فريدة ومتزايدة تحت الحمل. يتدرج جيدًا لأن قاعدة البيانات مصممة لمنح قيم متسلسلة بسرعة، حتى مع العديد من المستخدمين.

ما تحصل عليه: تفرد وترتيب (متزايد في الغالب). ما لا تحصل عليه: أرقام بدون فجوات. إذا فشل الإدراج بعد تخصيص رقم، فإن ذلك الرقم "يُحرق"، وسترى فجوة.

النهج 2: قيد فريد مع إعادة محاولة (دع قاعدة البيانات تقرر)

هنا تولد رقمًا مرشحًا (منطق التطبيق)، تحاول الحفظ، وتعتمد على قيد UNIQUE لرفض التكرارات. إذا حصل تصادم، تعيد المحاولة برقم جديد.

يمكن أن يعمل هذا، لكنه يصبح صاخبًا عند تزامن عالي. قد تواجه مزيدًا من المحاولات الفاشلة، مزيدًا من الترنزكشنات الفاشلة، وذروة يصعب تتبعها. أيضًا لا يضمن الأرقام بدون فجوات ما لم تُدمجه مع قواعد حجز صارمة، مما يزيد التعقيد.

النهج 3: صف عدّاد مع قفل (تهدف إلى عدم وجود فجوات)

إذا كنت بحاجة فعلًا إلى أرقام بدون فجوات، النمط الشائع هو جدول عدّاد مخصّص (صف واحد لكل نطاق ترقيم مثل لكل سنة أو لكل شركة). تقفل ذلك الصف في ترانزكشن، تزيده، وتستخدم القيمة الجديدة.

هذا أقرب شيء لغياب الفجوات في تصميم قاعدة بيانات عادي، لكنه له تكلفة: ينشئ "نقطة ساخنة" واحدة يجب أن تنتظرها كل الكتابات. كما يزيد من مخاطر الأخطاء التشغيلية (ترانزكشنات طويلة، مهلات، وdeadlocks).

النهج 4: خدمة حجز منفصلة (لحالات خاصة فقط)

يمكن لخدمة "ترقيم" منفصلة أن تجمع القواعد عبر عدة تطبيقات أو قواعد بيانات. تُستحق عادةً عندما يكون لديك عدة أنظمة تصدر الأرقام ولا يمكنك توحيد الكتابات.

المقايضة هي خطر تشغيلي: أضفت خدمة أخرى يجب أن تكون صحيحة ومتاحة جدًا ومتسقة.

هنا طريقة عملية للتفكير في الضمانات:

  • التسلسل: فريد، سريع، يقبل الفجوات
  • فريد + إعادة محاولة: فريد، بسيط عند حمل منخفض، قد ينهار تحت حمل عالي
  • صف عدّاد مقفل: يمكن أن يكون بدون فجوات، أبطأ تحت تزامن عالي
  • خدمة منفصلة: مرن عبر الأنظمة، أعلى تعقيدًا وأنماط فشل

إذا بنيت هذا في أداة بدون كود مثل AppMaster، تنطبق نفس الخيارات: قاعدة البيانات هي المكان الذي تعيش فيه الصحة. منطق التطبيق يساعد في إعادة المحاولة ورسائل أخطاء واضحة، لكن الضمان النهائي يجب أن يأتي من القيود والترانزكشنات.

خطوة بخطوة: منع التكرارات باستخدام التسلسلات والقيود الفريدة

اشحن نظام تذاكر قابل للتوسع
صمّم نموذج مبدئي لمكتب مساعدة حيث تظل أرقام التذاكر فريدة عبر الويب والموبايل وAPI.
ابدأ

إذا كان هدفك الرئيسي منع التكرارات (وليس ضمان عدم وجود فجوات)، النمط البسيط والموثوق هو: دع قاعدة البيانات تولّد معرفًا داخليًا، وفرض التفرد على الرقم الظاهر للعميل.

ابدأ بفصل المفهومين. استخدم قيمة مولّدة من قاعدة البيانات (identity/sequence) كمفتاح أساسي للانضمامات والتحرير والتصدير. احتفظ بعمود invoice_no أو ticket_no كعمود منفصل يُعرض للناس.

إعداد عملي في PostgreSQL

إليك نهجًا شائعًا في PostgreSQL يبقي منطق "الرقم التالي" داخل قاعدة البيانات، حيث يُدار التزامن بشكل صحيح.

-- Internal, never-shown primary key
create table invoices (
  id bigint generated always as identity primary key,
  invoice_no text not null,
  created_at timestamptz not null default now()
);

-- Business-facing uniqueness guarantee
create unique index invoices_invoice_no_uniq on invoices (invoice_no);

-- Sequence for the visible number
create sequence invoice_no_seq;

الآن ولّد رقم العرض عند الإدراج (لا عن طريق عمل select max(invoice_no) + 1). أحد الأنماط البسيطة هو تهيئة قيمة التسلسل داخل INSERT:

insert into invoices (invoice_no)
values (
  'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;

حتى لو ضغط 50 مستخدمًا "إنشاء فاتورة" في نفس الوقت، كل INSERT يحصل على قيمة تسلسل مختلفة، وفهرس التفرد يمنع أي تكرار عرضي.

ماذا تفعل عند حدوث تصادم

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

على مستوى التطبيق، تعامل مع خطأ انتهاك القيد الفريد بحلقة إعادة محاولة صغيرة:

  • جرّب الإدراج
  • إذا حصلت على خطأ قيد فريد على invoice_no، جرّب مرة أخرى
  • توقّف بعد عدد محدود من المحاولات وأظهر رسالة خطأ واضحة

هذا يعمل جيدًا لأن المحاولات تُثار فقط عندما يحدث شيء غير اعتيادي، مثل مسارين ينتجان نفس الصيغة.

قلّل نافذة السباق

لا تُحسِب الرقم في واجهة المستخدم، ولا تُحجز الأرقام بقراءة أولًا ثم الإدراج لاحقًا. ولّده أقرب ما يمكن إلى كتابة قاعدة البيانات.

إذا كنت تستخدم AppMaster مع PostgreSQL، يمكنك نمذجة id كمفتاح أساسي مُولّد في Data Designer، إضافة قيد فريد على invoice_no، وتوليد invoice_no خلال تدفق الإنشاء بحيث يحدث مع INSERT. بهذه الطريقة تظل قاعدة البيانات مصدر الحقيقة، وتبقى مشاكل التزامن محصورة حيث PostgreSQL أقوى.

خطوة بخطوة: بناء عدّاد بدون فجوات باستخدام قفل الصف

تحوّل إلى بدون فجوات عند الحاجة
نفّذ صفوف عدّاد وقفل FOR UPDATE داخل تدفق ترانزكشن واحد.
إنشاء تطبيق

إذا كنت فعلاً بحاجة إلى أرقام بدون فجوات (لا أرقام مفقودة)، يمكنك استخدام جدول عدّاد تراكنزكشن وعمليات قفل الصف. الفكرة بسيطة: لا يمكن لأي ترانزكشن أخذ الرقم التالي لنطاق معين إلا بمفرده، لذا تُمنح الأرقام بترتيب.

أولًا، قرّر نطاقك. العديد من الفرق تحتاج سلاسل منفصلة لكل شركة، لكل سنة، أو لكل سلسلة (مثل INV مقابل CRN). يخزن جدول العداد آخر رقم مستخدم لكل نطاق.

إليك نمط عملي لترقيم مقاوم للتزامن باستخدام أقفال صفوف PostgreSQL:

  1. أنشئ جدولًا، على سبيل المثال number_counters، بأعمدة مثل company_id, year, series, last_number، ومفتاح فريد على (company_id, year, series).
  2. ابدأ ترانزكشن قاعدة بيانات.
  3. قفل صف العداد لنطاقك باستخدام SELECT last_number FROM number_counters WHERE ... FOR UPDATE.
  4. احسب next_number = last_number + 1، حدّث صف العداد إلى last_number = next_number.
  5. أدخل صف الفاتورة أو التذكرة باستخدام next_number، ثم ارتكب الترانزكشن.

العنصر الحاسم هو FOR UPDATE. تحت الحمل، لا تحصل على تكرارات. كما أنك لا تحصل على فجوات من "اثنين من المستخدمين حصلا على نفس الرقم"، لأن الترنزاكشن الثاني لا يستطيع قراءة وزيادة نفس صف العداد حتى يلتزم الأول (أو يتراجع). بدلًا من ذلك، ينتظر طلب المستخدم الثاني لفترة وجيزة. ذلك الانتظار هو ثمن أن تكون بدون فجوات.

تهيئة نطاق جديد

تحتاج أيضًا لخطة لأول مرة يظهر فيها نطاق (شركة جديدة، سنة جديدة، سلسلة جديدة). خياران شائعان:

  • إنشاء صفوف العداد مسبقًا (مثلاً إنشاء صفوف السنة القادمة في ديسمبر).
  • الإنشاء عند الطلب: حاول إدراج صف العداد مع last_number = 0، وإذا كان موجودًا بالفعل فارجع إلى التدفق الطبيعي للقفل والزيادة.

إذا بنيت هذا في أداة بدون كود مثل AppMaster، احتفظ بكامل تسلسل "قفل، زيادة، إدراج" داخل ترانزكشن واحد في منطق الأعمال، حتى يحدث كله أو لا يحدث شيء.

حالات الحافة: المسودات، الحفظ الفاشل، الإلغاءات، والتعديلات

تظهر معظم أخطاء الترقيم في الأجزاء الفوضوية: مسودات لا تُنشر، حفظ يفشل، فواتير تُلغى، وسجلات تُعدّل بعد أن رآها أحدهم. إذا أردت ترقيمًا مقاومًا للتزامن، تحتاج قاعدة واضحة متى يصبح الرقم "حقيقيًا".

أكبر قرار هو التوقيت. إذا خصّصت رقمًا بمجرد أن يضغط المستخدم "جديد فاتورة"، ستحصل على فجوات من المسودات المهجورة. إذا خصصت فقط عند اعتماد الفاتورة (نشرها، إصدارها، إرسالها، أو أيًا كان معنى "نهائي" في عملك)، يمكنك الحفاظ على أرقام أكثر تماسكا وسهلة الشرح.

الحفظ الفاشل والتراجع هي المكان الذي غالبًا ما تتصادم فيه التوقعات مع سلوك قاعدة البيانات. مع تسلسل نموذجي، بمجرد أن يُؤخذ رقم فهو مأخوذ، حتى لو فشل الترانزكشن لاحقًا. هذا طبيعي وآمن، لكنه قد يخلق فجوات. إذا كانت سياساتك تتطلب أرقامًا بدون فجوات، يجب تخصيص الرقم فقط في الخطوة النهائية وفقط إذا التزام الترانزكشن. عادةً يعني ذلك قفل صف عدّاد واحد، كتابة الرقم النهائي، والالتزام كوحدة واحدة. إذا فشل أي خطوة، لا يُخصّص شيء.

الإلغاءات والملغيات لا يجب أبداً "إعادة استخدام" رقمًا. احتفظ بالرقم وغيّر الحالة. يتوقع المراجعون والعملاء أن يبقى التاريخ متسقًا حتى عند تصحيح المستند.

التعديلات أبسط: بمجرد أن يصبح الرقم مرئيًا خارج النظام، اعتبره دائمًا. لا تعيد ترقيم فاتورة أو تذكرة بعد مشاركتها أو تصديرها أو طباعتها. إذا احتجت تصحيحًا، أنشئ مستندًا جديدًا واربطه بالقديم (مثل مذكرة ائتمان أو مستند بديل)، لكن لا تُعد كتابة التاريخ.

مجموعة قواعد عملية يتبناها كثير من الفرق:

  • المسودات بلا أرقام نهائية (استخدم معرفًا داخليًا أو "DRAFT").
  • خصّص الرقم فقط عند "النشر/الإصدار"، داخل نفس الترانزكشن مع تغيير الحالة.
  • الإلغاءات والملغيات تحافظ على الرقم، لكن تحصل على حالة وسبب واضح.
  • الأرقام المطبوعة/المرسلة بالبريد الإلكتروني لا تتغير.
  • الاستيرادات تحافظ على الأرقام الأصلية وتحدّث العداد ليبدأ بعد أكبر قيمة مستوردة.

الهجرات والاستيرادات تحتاج عناية خاصة. إذا انتقلت من نظام آخر، أحضر أرقام الفواتير الحالية كما هي، ثم ضع عدّادك ليبدأ بعد أكبر قيمة مستوردة. أيضًا قرّر ماذا تفعل مع الصيغ المتضاربة (مثل بادئات مختلفة لكل سنة). عادةً من الأفضل تخزين "رقم العرض" كما كان، والحفاظ على مفتاح داخلي منفصل.

مثال: مركز مساعدة ينشئ تذاكر بسرعة، لكن كثيرًا منها مسودات. خصّص رقم التذكرة فقط عندما يضغط الوكيل "إرسال إلى العميل". هذا يتجنّب إهدار الأرقام على المسودات المهجورة ويحافظ على تسلسل مرئي متوافق مع التواصل مع العميل.

في أداة بدون كود مثل AppMaster، نفس الفكرة تنطبق: احتفظ بالمسودات كسجلات بدون رقم عام، ثم ولّد الرقم النهائي أثناء خطوة "التقديم" في محرر العمليات التجارية التي تلتزم بنجاح.

أخطاء شائعة تسبب التكرارات أو الفجوات المفاجئة

اجعل المسودات آمنة بطبيعتها
أنشئ خطوة نهائية تمنح الأرقام فقط عندما يتم نشر السجل فعلاً.
ابنِ الآن

معظم مشاكل الترقيم تنبع من فكرة واحدة بسيطة: التعامل مع الرقم كقيمة عرض بدلًا من حالة مشتركة. عندما يحفظ عدة أشخاص في نفس الوقت، يحتاج النظام إلى مكان واضح واحد ليقرر الرقم التالي، وقاعدة واضحة لما يحدث عند الفشل.

خطأ كلاسيكي هو استخدام SELECT MAX(number) + 1 في كود التطبيق. يبدو جيدًا في اختبار المستخدم الواحد، لكن يمكن لطلبين أن يقرآ نفس MAX قبل أن يلتزم أي منهما. كلاهما يولدان نفس القيمة التالية، وتحصل على تكرار. حتى لو أضفت "تحقق ثم إعادة محاولة"، يمكنك أن تولد حملاً زائدًا وذروات غريبة تحت ضغط الذروة.

مصدر شائع آخر للتكرارات هو توليد الرقم على جهة العميل (المتصفح أو الموبايل) قبل الحفظ. العميل لا يعرف ماذا يفعل المستخدمون الآخرون، ولا يمكنه حجز رقم بأمان إذا فشل الحفظ. الأرقام المولدة من العميل جيدة لتسميات مؤقتة مثل "مسودة 12"، لكن ليست لمعرفات رسمية.

الفجوات تفاجئ الفرق التي تفترض أن التسلسلات خالية من الفجوات. في PostgreSQL، التسلسلات مصممة للتفرد، لا للاستمرارية الكاملة. يمكن أن تُخطى أرقام عند تراجع ترانزكشن، عند جلب معرفات مسبقًا، أو عند إعادة تشغيل قاعدة البيانات. هذا سلوك طبيعي. إذا كان مطلبك الحقيقي "لا تكرارات"، فالتسلسل مع قيد فريد عادةً هو الحل الصحيح. إذا كان مطلبك الحقيقي هو "بدون فجوات فعلية"، فستحتاج إلى نمط مختلف (عادة قفل صف) ويجب أن تتقبل بعض التنازلات في معدل الاستجابة.

القفل يمكن أن يفشل أيضًا عندما يكون واسعًا جدًا. قفل عالمي واحد لكل الترقيم يجبر كل عملية إنشاء على الاصطفاف، حتى لو كان بالإمكان فصل العدادات حسب الشركة أو الموقع أو نوع المستند. ذلك يبطئ النظام ويجعل المستخدمين يشعرون أن الحفظ "يتعطل" عشوائيًا.

إليك الأخطاء الجديرة بالتحقق عند تنفيذ ترقيم مقاوم للتزامن:

  • استخدام MAX + 1 (أو "إيجاد آخر رقم") بدون قيد فريد على مستوى قاعدة البيانات.
  • توليد الأرقام النهائية على العميل ثم محاولة "تصحيح التعارضات لاحقًا".
  • توقع أن تكون تسلسلات PostgreSQL خالية من الفجوات، ثم معاملة الفجوات كأخطاء.
  • قفل عدّاد واحد مشترك لكل شيء بدلًا من تقسيم العدادات حيث يلزم.
  • الاختبار فقط بمستخدم واحد، حتى لا تظهر حالات السباق حتى الإطلاق.

نصيحة اختبار عملية: شغّل اختبار تزامن بسيط ينشئ من 100 إلى 1000 سجل متوازيًا ثم يفحص التكرارات والفجوات غير المتوقعة. إذا بنيت في أداة بدون كود مثل AppMaster، نفس القاعدة تنطبق: تأكد أن الرقم النهائي يُعيّن داخل ترانزكشن خادمي واحد، لا في تدفق الواجهة.

فحوص سريعة قبل الإطلاق

اختبر تدفق الإنشاء تحت الضغط
شغّل اختبارات إنشاء متوازية واكشف حالات السباق قبل أن يصل المستخدمون.
ابدأ البناء

قبل نشر ترقيم الفواتير أو التذاكر، قم بمراجعة سريعة للأجزاء التي عادةً ما تفشل تحت حمل فعلي. الهدف بسيط: كل سجل يحصل على رقم تجاري واحد بالضبط، وتظل قواعدك صحيحة حتى عندما يضغط 50 شخصًا "إنشاء" في نفس الوقت.

إليك قائمة تدقيق عملية قبل الإطلاق لرقم مقاوم للتزامن:

  • تأكد أن حقل الرقم التجاري له قيد فريد في قاعدة البيانات (ليس فقط فحص واجهة المستخدم). هذه هي خط دفاعك الأخير إذا اصطدم طلبان.
  • تأكد أن الرقم يُعيّن داخل نفس ترانزكشن قاعدة البيانات الذي يحفظ السجل. إذا كان تعيين الرقم والحفظ مفصولين عبر طلبات، سترى تكرارات في النهاية.
  • إذا تطلبت أرقامًا بدون فجوات، خصّص الرقم فقط عند اعتماد السجل النهائي (مثلاً عند إصدار الفاتورة، لا عند إنشاء المسودة). المسودات، النماذج المهجورة، والمدفوعات الفاشلة هي مصدر الفجوات.
  • أضف استراتيجية إعادة محاولة للتصادمات النادرة. حتى مع قفل الصف أو التسلسلات، يمكنك مواجهة أخطاء التسلسل أو deadlock أو انتهاك فريد في حالات توقيت حافة. إعادة محاولة بسيطة مع تأخير قصير عادةً تكفي.
  • اختبر الضغط مع 20 إلى 100 إنشاء متزامن عبر كل نقاط الدخول: الواجهة، API العام، والاستيرادات الضخمة. اختبر مزيجًا واقعيًا مثل النبضات، الشبكات البطيئة، وإرسال مزدوج.

طريقة سريعة للتحقق من إعدادك هي محاكاة لحظة مزدحمة في مركز مساعدة: وكيلاْن يفتحان نموذج "تذكرة جديدة"، أحدهما يقدّم من الويب بينما مهمة استيراد تُدخِل تذاكر من صندوق بريد في نفس الوقت. بعد التشغيل، تحقّق أن كل الأرقام فريدة، بالشكل الصحيح، وأن الإخفاقات لا تترك سجلات نصف محفوظة.

إذا بنيت سير العمل في AppMaster، نفس المبادئ تنطبق: ضع تعيين الرقم في ترانزكشن قاعدة البيانات، اعتمد على قيود PostgreSQL، واختبر كل من إجراءات الواجهة ونقاط نهاية الAPI التي تنشئ الكيان نفسه. هنا يشعر كثير من الفرق بالأمان في الاختبارات اليدوية لكن يفاجأون في اليوم الأول للدخول الحقيقي.

مثال: تذاكر مكتب مساعدة مزدحمة وما الذي تفعله بعد ذلك

تخيل مكتب دعم حيث الوكلاء ينشئون تذاكر طوال اليوم في الويب، بينما تكامل خارجي ينشئ تذاكر من أداة الدردشة والبريد الإلكتروني. الجميع يتوقع أرقام تذاكر مثل T-2026-000123، ويتوقعون أن كل رقم يشير إلى تذكرة واحدة بالضبط.

النهج الساذج: قراءة "آخر رقم تذكرة"، إضافة 1، حفظ التذكرة. تحت الحمل، قد يقرأ طلبان نفس "آخر رقم" قبل أن يحفظ أي منهما. كلاهما يحسب نفس الرقم التالي، وتحصل على تكرارات. إذا حاولت "تصحيح" ذلك بالإعادة بعد فشل، غالبًا ما تخلق فجوات دون قصد.

قاعدة البيانات يمكنها إيقاف التكرارات حتى لو كان كود تطبيقك ساذجًا. أضف قيدًا فريدًا على عمود ticket_number. ثم عندما يحاول طلبان نفس الرقم، يفشل أحد الإدراجين وتعيد المحاولة بشكل نظيف. هذه هي جوهرية ترقيم مقاوم للتزامن أيضًا: دع قاعدة البيانات تفرض التفرد، لا واجهة المستخدم.

الترقيم بدون فجوات يغير سير العمل. إذا كنت تطلب عدم وجود فجوات، عادةً لا يمكنك تخصيص الرقم النهائي عند إنشاء التذكرة أولًا (مسودة). بدلًا من ذلك، أنشئ التذكرة بحالة مثل Draft وبدون ticket_number نهائي. خصّص الرقم فقط عندما تُنهِي التذكرة، بحيث لا تُهدر الأرقام على المسودات.

تصميم جدول بسيط يبدو هكذا:

  • tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
  • ticket_counters: key (مثلاً "tickets_2026"), next_number

في AppMaster، يمكنك نمذجة هذا في Data Designer بأنواع PostgreSQL، ثم بناء المنطق في Business Process Editor:

  • إنشاء تذكرة: إدراج تذكرة بحالة=Draft وبدون ticket_number
  • إنهاء التذكرة: ابدأ ترانزكشن، اقفل صف العداد، عيّن ticket_number، زد next_number، ارفع الترانزكشن
  • اختبار: شغّل عمليتي "إنهاء" في نفس الوقت وتأكد أنك لا تحصل على تكرارات

ما الذي تفعله بعد ذلك: ابدأ بقاعدة (فريد فقط مقابل بدون فجوات فعلي). إذا كنت تقبل الفجوات، فالتسلسل مع قيد فريد عادةً يكفي ويبقي التدفق بسيطًا. إذا كنت تحتاج أن تكون بدون فجوات، انقل الترقيم إلى خطوة الإنهاء واعتبر "المسودة" كحالة من الدرجة الأولى. ثم اختبر التحميل مع وكلاء متعددين يضغطون في نفس الوقت ومع تكاملات API ترسل دفعات، لترى السلوك قبل أن يواجهه المستخدمون الحقيقيون.

من السهل أن تبدأ
أنشئ شيئًا رائعًا

تجربة مع AppMaster مع خطة مجانية.
عندما تكون جاهزًا ، يمكنك اختيار الاشتراك المناسب.

البدء
ترقيم الفواتير الآمن من حالات التزامن لتجنّب التكرارات والفجوات | AppMaster