استخدام Reliable Collections

تقدم Service Fabric نموذج برمجة ذو حالة متاح لمطوري .NET عبر مجموعات موثوقة. على وجه التحديد، توفر خدمة Service Fabric قاموساً موثوقاً وفئات قائمة انتظار موثوقة. عند استخدام هذه الفئات، يتم تقسيم حالتك (لقابلية التوسع)، ونسخها نسخاً متماثلاً (للتوفر)، والتعامل معها داخل قسم (لدلالات ACID). دعونا نلقي نظرة على الاستخدام النموذجي لكائن قاموس موثوق ونرى ما يفعله بالفعل.

try
{
   // Create a new Transaction object for this partition
   using (ITransaction tx = base.StateManager.CreateTransaction())
   {
      // AddAsync takes key's write lock; if >4 secs, TimeoutException
      // Key & value put in temp dictionary (read your own writes),
      // serialized, redo/undo record is logged & sent to secondary replicas
      await m_dic.AddAsync(tx, key, value, cancellationToken);

      // CommitAsync sends Commit record to log & secondary replicas
      // After quorum responds, all locks released
      await tx.CommitAsync();
   }
   // If CommitAsync isn't called, Dispose sends Abort
   // record to log & all locks released
}
catch (TimeoutException)
{
   // choose how to handle the situation where you couldn't get a lock on the file because it was 
   // already in use. You might delay and retry the operation
   await Task.Delay(100);
}

تتطلب جميع العمليات على كائنات القاموس الموثوق (باستثناء ClearAsync، وهو أمر غير قابل للإلغاء)، كائن ITransaction. لقد ارتبط هذا الكائن به وأي وجميع التغييرات التي تحاول إجراؤها على أي قاموس موثوق وكائنات قائمة انتظار موثوقة أو أيٍ منهم داخل قسمٍ واحدٍ. يمكنك الحصول على كائن ITransaction عن طريق استدعاء أسلوب CreateTransaction الخاص بـ StateManager الخاص بالقسم.

في التعليمة البرمجية أعلاه، يتم تمرير كائن ITransaction إلى طريقة AddAsync الخاصة بقاموس موثوق. داخلياً، أساليب القاموس التي تقبل مفتاحاً تضع قفل القارئ/الكاتب المقترن بالمفتاح. إذا كان الأسلوب يعدِّل قيمة المفتاح، فإن الأسلوب يضع قفل الكتابة على المفتاح وإذا كان الأسلوب يقرأ فقط من قيمة المفتاح، فسيتم وضع قفل قراءة على المفتاح. نظراً لأن AddAsync يعدِّل قيمة المفتاح إلى القيمة الجديدة التي تم تمريرها، يتم وضع قفل الكتابة الخاص بالمفتاح. لذلك، إذا حاول مؤشرا ترابط (أو أكثر) إضافة قيم بنفس المفتاح في نفس الوقت، فسيضع مؤشر ترابط واحد على قفل الكتابة، وسيتم حظر مؤشرات الترابط الأخرى. بشكل افتراضي، يتم حظر الأساليب لمدة تصل إلى 4 ثوان للحصول على القفل؛ وبعد 4 ثوانٍ، تطرح الأساليب TimeoutException. توجد الأحمال الزائدة للأساليب مما يسمح لك بتمرير قيمة الوقت الصريحة إذا كنت تفضل ذلك.

عادةً ما تكتب تعليماتك البرمجية للتفاعل مع TimeoutException عن طريق التقاطها وإعادة محاولة العملية بأكملها (كما هو موضح في التعليمة البرمجية أعلاه). في هذه التعليمة البرمجية البسيطة، نحن نستدعي Task.Delay مروراً بـ 100 مللي ثانية في كل مرة فحسب. ولكن، في الواقع، قد يكون من الأفضل لك استخدام نوع من التأخير العكسي الأُسي بدلاً من ذلك.

بمجرد الحصول على القفل، يضيف AddAsync مراجع كائن المفتاح والقيمة إلى قاموس مؤقت داخلي مقترن بكائن ITransaction. يتم ذلك لتزويدك بدلالات "read-your-own-writes". أي، بعد استدعاء AddAsync، سيؤدي استدعاء لاحق إلى TryGetValueAsync باستخدام نفس كائن ITransaction إلى إرجاع القيمة حتى إذا لم تكن قد ثبتت المعاملة بعد.

ملاحظة

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

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

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

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

مجموعات موثوقة متغيرة

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

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

لتمكين الدعم المتغير في خدمتك، بادر بتعيين العلامة HasPersistedState في إعلان نوع الخدمة إلى false، مثل:

<StatefulServiceType ServiceTypeName="MyServiceType" HasPersistedState="false" />

ملاحظة

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

المخاطر الشائعة وكيفية تجنبها

الآن بعد أن فهمت كيفية عمل المجموعات الموثوقة داخلياً، دعنا نلقي نظرة على بعض أشكال سوء الاستخدام الشائعة لها. انظر التعليمة البرمجية أدناه:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // AddAsync serializes the name/user, logs the bytes,
   // & sends the bytes to the secondary replicas.
   await m_dic.AddAsync(tx, name, user);

   // The line below updates the property's value in memory only; the
   // new value is NOT serialized, logged, & sent to secondary replicas.
   user.LastLogin = DateTime.UtcNow;  // Corruption!

   await tx.CommitAsync();
}

عند العمل مع قاموس .NET عادي، يمكنك إضافة مفتاح/قيمة إلى القاموس ثم تغيير قيمة خاصية (مثل LastLogin). ومع ذلك، لن تعمل هذه التعليمة البرمجية على نحوٍ صحيحٍ مع قاموسٍ موثوقٍ. تذكر من المناقشة السابقة، أن استدعاء AddAsync ينشئ تسلسل كائنات المفاتيح/القيمة إلى صفائف البايت، ثم يحفظ الصفائف في ملفٍ محلي ويرسلها أيضاً إلى النُسخ المتماثلة الثانوية. إذا غيَّرت لاحقاً خاصيةً ما، فإن هذا يغير قيمة الخاصية في الذاكرة فقط دون أن يؤثر ذلك على الملف المحلي أو البيانات المُرسلة إلى النسخ المتماثلة. إذا تعطلت العملية، يتم التخلص مما هو موجود في الذاكرة. عندما تبدأ عملية جديدة أو إذا أصبحت نسخة متماثلة أخرى أساسية، فإن قيمة الخاصية القديمة هي ما هو متاح.

لا أستطيع أن أؤكد بما فيه الكفاية على مدى سهولة ارتكاب هذا النوع من الأخطاء المُوضَّح أعلاه. وسوف تتعلم فقط عن الخطأ إذا/عندما تنخفض المعالجة. الطريقة الصحيحة لكتابة التعليمات البرمجية هي ببساطة عكس السطرين:

using (ITransaction tx = StateManager.CreateTransaction())
{
   user.LastLogin = DateTime.UtcNow;  // Do this BEFORE calling AddAsync
   await m_dic.AddAsync(tx, name, user);
   await tx.CommitAsync();
}

فيما يلي مثال آخر يوضح خطأ شائعاً:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // Use the user's name to look up their data
   ConditionalValue<User> user = await m_dic.TryGetValueAsync(tx, name);

   // The user exists in the dictionary, update one of their properties.
   if (user.HasValue)
   {
      // The line below updates the property's value in memory only; the
      // new value is NOT serialized, logged, & sent to secondary replicas.
      user.Value.LastLogin = DateTime.UtcNow; // Corruption!
      await tx.CommitAsync();
   }
}

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

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

توضح التعليمة البرمجية أدناه الطريقة الصحيحة لتحديث قيمة في مجموعةٍ موثوقةٍ:

using (ITransaction tx = StateManager.CreateTransaction())
{
   // Use the user's name to look up their data
   ConditionalValue<User> currentUser = await m_dic.TryGetValueAsync(tx, name);

   // The user exists in the dictionary, update one of their properties.
   if (currentUser.HasValue)
   {
      // Create new user object with the same state as the current user object.
      // NOTE: This must be a deep copy; not a shallow copy. Specifically, only
      // immutable state can be shared by currentUser & updatedUser object graphs.
      User updatedUser = new User(currentUser);

      // In the new object, modify any properties you desire
      updatedUser.LastLogin = DateTime.UtcNow;

      // Update the key's value to the updateUser info
      await m_dic.SetValue(tx, name, updatedUser);
      await tx.CommitAsync();
   }
}

حدد أنواع البيانات غير القابلة للتغيير لمنع أخطاء المبرمج

من الناحية المثالية، نود أن يبلغ المحول البرمجي عن الأخطاء عندما تنتج عن طريق الخطأ تعليمة برمجية تغير حالة كائن من المفترض أن تعتبره غير قابل للتغيير. ولكن، لا يملك المحول البرمجي C# القدرة على القيام بذلك. لذلك، ولتجنب أخطاء المبرمج المحتملة، نوصي بشدة بتحديد الأنواع التي تستخدمها مع مجموعات موثوقة لتكون أنواعاً غير قابلة للتغيير. على وجه التحديد، هذا يُعني أنك تلتزم بأنواع القيم الأساسية (مثل الأرقام [Int32، UInt64، إلخ]، DateTime، Guid، TimeSpan، وما شابه ذلك). يمكنك أيضاً استخدام السلسلة. من الأفضل تجنب خصائص المجموعة لأن تسلسلها وإلغاء تسلسلها يمكن أن يضر بالأداء بصفةٍ متكررةٍ. ومع ذلك، إذا كنت ترغب في استخدام خصائص المجموعة، فإننا نوصي بشدة باستخدام مكتبة مجموعات .NET غير القابلة للتغيير (System.Collections.Immutable). هذه المكتبة متاحة للتنزيل من https://nuget.org. نوصي أيضاً بإغلاق فصولك الدراسية وجعل الحقول مصممةً للقراءة فقط كلما أمكن ذلك.

يوضح نوع UserInfo أدناه كيفية تعريف نوع غير قابل للتغيير مع الاستفادة من التوصيات المذكورة أعلاه.

[DataContract]
// If you don't seal, you must ensure that any derived classes are also immutable
public sealed class UserInfo
{
   private static readonly IEnumerable<ItemId> NoBids = ImmutableList<ItemId>.Empty;

   public UserInfo(String email, IEnumerable<ItemId> itemsBidding = null) 
   {
      Email = email;
      ItemsBidding = (itemsBidding == null) ? NoBids : itemsBidding.ToImmutableList();
   }

   [OnDeserialized]
   private void OnDeserialized(StreamingContext context)
   {
      // Convert the deserialized collection to an immutable collection
      ItemsBidding = ItemsBidding.ToImmutableList();
   }

   [DataMember]
   public readonly String Email;

   // Ideally, this would be a readonly field but it can't be because OnDeserialized
   // has to set it. So instead, the getter is public and the setter is private.
   [DataMember]
   public IEnumerable<ItemId> ItemsBidding { get; private set; }

   // Since each UserInfo object is immutable, we add a new ItemId to the ItemsBidding
   // collection by creating a new immutable UserInfo object with the added ItemId.
   public UserInfo AddItemBidding(ItemId itemId)
   {
      return new UserInfo(Email, ((ImmutableList<ItemId>)ItemsBidding).Add(itemId));
   }
}

نوع ItemId هو أيضاً نوع غير قابل للتغيير كما هو موضح هنا:

[DataContract]
public struct ItemId
{
   [DataMember] public readonly String Seller;
   [DataMember] public readonly String ItemName;
   public ItemId(String seller, String itemName)
   {
      Seller = seller;
      ItemName = itemName;
   }
}

تعيين إصدار المخطط (الترقيات)

داخلياً، تنشئ المجموعات الموثوقة تسلسل لكائناتك الخاصة باستخدام . DataContractSerializer من .NET. تستمر الكائنات المُدرجة بتسلسل في القرص المحلي للنسخة المتماثلة الأساسية ويتم نقلها أيضاً إلى النُسخ المتماثلة الثانوية. مع نضوج خدمتك، من المحتمل أنك ستحتاج إلى تغيير نوع البيانات (المخطط) التي تتطلبها خدمتك. تعامل مع تعيين إصدار لبياناتك بعنايةٍ فائقةٍ. أولاً وقبل كل شيء، يجب أن تكون دائماً قادراً على إلغاء تسلسل البيانات القديمة. على وجه التحديد، هذا يعني أن التعليمة البرمجية الخاصة بإلغاء التسلسل خاصتك يجب أن تكون متوافقةً مع الإصدارات السابقة بصفةٍ مطلقةٍ: يجب أن يكون الإصدار 333 من التعليمة البرمجية الخاصة بالخدمة خاصتك قادراً على العمل على البيانات الموضوعة في مجموعة موثوقة بواسطة الإصدار 1 من تعليمتك البرمجية الخاصة بالخدمة قبل 5 سنوات.

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

تحذير

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

بدلاً من ذلك، يمكنك إجراء ما يُشار إليه عادةً بالترقية المزدوجة. من خلال الترقية على مرحلتين، يمكنك ترقية الخدمة من V1 إلى V2: يحتوي V2 على التعليمة البرمجية التي تعرف كيفية التعامل مع تغيير المخطط الجديد ولكن هذه التعليمة البرمجية لا يتم تنفيذها. عندما تقرأ التعليمة البرمجية V2 بيانات V1، فإنها تعمل عليها وتكتب بيانات V1. بعد ذلك، وبعد اكتمال الترقية عبر جميع مجالات الترقية، يمكنك بطريقةٍ ما الإشارة إلى مثيلات V2 قيد التشغيل بأن الترقية قد اكتملت. (إحدى طرق الإشارة إلى ذلك هي طرح ترقية التكوين؛ وهذا ما يجعل هذه ترقية على مرحلتين.) الآن، يمكن لمثيلات V2 قراءة بيانات V1 وتحويلها إلى بيانات V2 والعمل عليها وكتابتها كبيانات V2. عندما تقرأ المثيلات الأخرى بيانات V2، فإنها لا تحتاج إلى تحويلها، بل تعمل عليها فقط، وتكتب بيانات V2.

الخطوات التالية

لمعرفة المزيد عن إنشاء عقود بيانات متوافقة، راجع عقود البيانات المتوافقة مع إعادة التوجيه

لمعرفة أفضل الممارسات المتعلقة بتعيين إصدارات عقود البيانات، راجع تعيين إصدار عقد البيانات

لمعرفة كيفية تنفيذ عقود البيانات المتسامحة مع الإصدارات، راجع عمليات رد الاتصال الخاصة بإنشاء تسلسل متسامح مع الإصدار

لمعرفة كيفية توفير بنية بيانات يمكن تشغيلها بينياً عبر إصدارات متعددة، راجع IExtensibleDataObject