لماذا تخزين جميع البيانات في الذاكرة؟
لتخزين بيانات الموقع أو الواجهة الخلفية ، ستكون أول رغبة لمعظم الأشخاص العاقلين هي قاعدة بيانات SQL.
لكن في بعض الأحيان يتعلق الأمر بأن نموذج البيانات غير مناسب لـ SQL: على سبيل المثال ، عند إنشاء بحث أو رسم بياني اجتماعي ، تحتاج إلى البحث عن علاقات معقدة بين الكائنات.
أسوأ موقف هو عندما تعمل في فريق ، ولا يستطيع أحد الزملاء بناء استفسارات سريعة. كم من الوقت قضيته في حل مشاكل N + 1 وبناء فهارس إضافية بحيث تم تحديد SELECT في الصفحة الرئيسية في وقت معقول؟
نهج شعبي آخر هو NoSQL. قبل بضع سنوات كان هناك ضجة كبيرة حول هذا الموضوع - لأي فرصة ، قمنا بنشر MongoDB واستمتعنا بالإجابات في شكل وثائق json (بالمناسبة ، كم من العكازات كان لا بد من إدراجها بسبب الروابط الدائرية في الوثائق؟) .
لماذا لا تحاول تخزين جميع البيانات في ذاكرة التطبيق ، وحفظها بشكل دوري إلى التخزين التعسفي (ملف ، قاعدة البيانات عن بعد)؟
أصبحت الذاكرة رخيصة ، وسيتم احتواء أي بيانات محتملة من معظم المشاريع الصغيرة والمتوسطة الحجم على 1 غيغابايت من الذاكرة. (على سبيل المثال ، مشروع منزلي المفضل - متتبع مالي يحتفظ بالإحصائيات اليومية وتاريخًا لنفقاتي وأرصدي ومعاملاتي لمدة عام ونصف ، يستهلك 45 ميغابايت فقط من الذاكرة.)
الايجابيات:
- أصبح الوصول إلى البيانات أسهل - لا داعي للقلق بشأن الاستعلامات والتحميل البطيء وميزات ORM والعمل مع كائنات C # العادية ؛
- لا توجد مشاكل مرتبطة بالوصول من مؤشرات ترابط مختلفة؛
- سريع جدًا - لا توجد طلبات لشبكة ، ولا توجد تعليمات برمجية للترجمة إلى لغة الاستعلام ، ولا يوجد تسلسل للأشياء ؛
- يجوز تخزين البيانات بأي شكل - على الأقل في XML على القرص ، على الأقل في SQL Server ، على الأقل في Azure Table Storage.
سلبيات:
- تم فقد القياس الأفقي ، ونتيجة لذلك ، لا يمكن إجراء تعطل صفري ؛
- إذا تعطل التطبيق ، يمكنك فقد البيانات جزئيًا. (لكن تطبيقنا لا يتعطل أبدًا ، أليس كذلك؟)
كيف يعمل؟
الخوارزمية هي كما يلي:
- في البداية ، يتم إنشاء اتصال بمستودع البيانات ، ويتم تنزيل البيانات ؛
- نموذج كائن ، فهارس أساسية ، ومؤشرات علاقة (1: 1 ، 1: كثير) مبنية ؛
- يتم إنشاء اشتراك لتغيير خصائص الكائنات (INotifyPropertyChanged) ولإضافة أو إزالة عناصر إلى المجموعة (INotifyCollectionChanged) ؛
- عندما يتم تشغيل الاشتراك - يضاف الكائن الذي تم تغييره إلى قائمة الانتظار للكتابة إلى مستودع البيانات ؛
- بشكل دوري (حسب الموقت) ، يتم حفظ التغييرات في التخزين في دفق الخلفية ؛
- عند الخروج من التطبيق ، يتم أيضًا حفظ التغييرات في المستودع.
مثال رمز
وصفنا نموذج البيانات الذي سيتم تخزينه في المستودع public class ParentEntity : BaseEntity { public ParentEntity(Guid id) => Id = id; } public class ChildEntity : BaseEntity { public ChildEntity(Guid id) => Id = id; public Guid ParentId { get; set; } public string Value { get; set; } }
ثم نموذج الكائن: public class ParentModel : ModelBase { public ParentModel(ParentEntity entity) { Entity = entity; } public ParentModel() { Entity = new ParentEntity(Guid.NewGuid()); }
وأخيرًا ، فئة المستودع نفسها للوصول إلى البيانات: public class MyObjectRepository : ObjectRepositoryBase { public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance) { IsReadOnly = true;
إنشاء مثيل ObjectRepository:
var memory = new MemoryStream(); var db = new LiteDatabase(memory); var dbStorage = new LiteDbStorage(db); var repository = new MyObjectRepository(dbStorage); await repository.WaitForInitialize();
إذا كان المشروع سيستخدم HangFire public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository) { services.AddHangfire(s => s.UseHangfireStorage(objectRepository)); }
إدراج كائن جديد:
var newParent = new ParentModel() repository.Add(newParent);
في هذه المكالمة ، تتم إضافة كائن ParentModel إلى ذاكرة التخزين المؤقت المحلية وقائمة انتظار الكتابة إلى قاعدة البيانات. لذلك ، تأخذ هذه العملية O (1) ، ويمكنك العمل على الفور مع هذا الكائن.
على سبيل المثال ، للعثور على هذا الكائن في المستودع وتأكد من أن الكائن الذي تم إرجاعه هو نفس المثيل:
var parents = repository.Set<ParentModel>(); var myParent = parents.Find(newParent.Id); Assert.IsTrue(ReferenceEquals(myParent, newParent));
ماذا يحدث مع هذا؟ تقوم المجموعة <ParentModel> () بإرجاع TableDictionary <ParentModel> ، والذي يحتوي على ConcurrentDictionary <ParentModel ، ParentModel> ويوفر وظائف إضافية للفهارس الأساسية والثانوية. يتيح لك ذلك امتلاك طرق للبحث حسب المعرف (أو الفهارس المخصصة التعسفية الأخرى) دون تعداد كل الكائنات بالكامل.
عند إضافة كائنات إلى ObjectRepository ، تتم إضافة اشتراك لتغيير خصائصه ، وبالتالي فإن أي تغيير في الخصائص يؤدي أيضًا إلى إضافة هذا الكائن إلى قائمة انتظار الكتابة.
يبدو تحديث الخصائص من الخارج مثل العمل مع كائن POCO:
myParent.Children.First().Property = "Updated value";
يمكنك حذف كائن بالطرق التالية:
repository.Remove(myParent); repository.RemoveRange(otherParents); repository.Remove<ParentModel>(x => !x.Children.Any());
هذا أيضًا يضيف الكائن إلى قائمة انتظار الحذف.
كيف يعمل الحفظ؟
كائن ObjectRepository عند تغيير الكائنات المتعقبة (إضافة أو إزالة ، وتغيير الخصائص) يثير الحدث ModelChanged ، الذي يتم الاشتراك فيه مع IStorage . تلخص تطبيقات IStorage ، عند حدوث حدث ModelChanged ، التغييرات في 3 قوائم انتظار - إضافة وتحديث وحذف.
أيضًا ، تقوم تطبيقات IStorage أثناء التهيئة بإنشاء مؤقت يؤدي كل 5 ثوانٍ إلى حفظ التغييرات.
بالإضافة إلى ذلك ، هناك API لفرض استدعاء حفظ: ObjectRepository.Save () .
قبل كل عملية حفظ ، تتم إزالة العمليات التي لا معنى لها أولاً من قوائم الانتظار (على سبيل المثال ، الأحداث المكررة - عندما يتغير كائن مرتين أو إضافة / إزالة سريعة للكائنات) ، وعندها فقط يتم الحفظ نفسه.
في جميع الحالات ، يتم الاحتفاظ بالكائن بأكمله ، لذلك من الممكن أن يتم حفظ الكائنات بترتيب مختلف عن تلك التي تم تغييرها ، بما في ذلك الإصدارات الأحدث من الكائنات عن وقت الإضافة إلى قائمة الانتظار.
ماذا هناك؟
- تستند جميع المكتبات إلى .NET Standard 2.0. يمكن استخدامه في أي مشروع .NET حديث.
- واجهة برمجة التطبيقات آمنة. تستند المجموعات الداخلية إلى ConcurrentDictionary ، ومعالجات الأحداث إما لديهم تأمين أو لا يحتاجون إليها.
الشيء الوحيد الذي يجب تذكره هو استدعاء ObjectRepository.Save ()؛ - الفهارس المخصصة (تتطلب التفرد):
repository.Set<ChildModel>().AddIndex(x => x.Value); repository.Set<ChildModel>().Find(x => x.Value, "myValue");
من يستخدمه؟
شخصيا ، بدأت في استخدام هذا النهج في جميع مشاريع الهوايات ، لأنها مريحة ولا تتطلب نفقات كبيرة لكتابة طبقة الوصول إلى البيانات أو نشر بنية تحتية ثقيلة. شخصيا ، كقاعدة عامة ، عادة ما يكون تخزين البيانات في litedb أو في ملف كافيا بالنسبة لي.
ولكن في الماضي ، عندما تم إنشاء EscapeTeams في وقت بدء التشغيل مع الفريق ( ظنوا أنهم كانوا ، والمال - ولكن لا ، تجربة مرة أخرى ) - استخدموا Azure Table Storage لتخزين البيانات.
الخطط المستقبلية
أود إصلاح أحد العيوب الرئيسية لهذا النهج - القياس الأفقي. للقيام بذلك ، تحتاج إما إلى المعاملات الموزعة (كذا!) ، أو اتخاذ قرار قوي الإرادة بعدم تغيير البيانات نفسها من الحالات المختلفة ، أو السماح لها بالتغيير وفقًا لمبدأ "من هو الأخير - هذا صحيح".
من الناحية الفنية ، أرى أن المخطط التالي ممكن:
- قم بتخزين EventLog و Snapshot بدلاً من طراز الكائن
- البحث عن الحالات الأخرى (إضافة نقاط النهاية لجميع الحالات؟ Udp discovery؟ Master / slave؟ إلى الإعدادات)
- النسخ المتماثل بين مثيلات سجل الأحداث من خلال أي من خوارزميات الإجماع ، مثل RAFT.
هناك أيضًا مشكلة أخرى تزعجني - وهي الحذف المتتالي ، أو اكتشاف حالات حذف الكائنات التي يتم الرجوع إليها من كائنات أخرى.
شفرة المصدر
إذا قرأت ما يصل إلى هنا - فتبقى الكود الوحيد المطلوب قراءته ، يمكن أن يكون كذلك
وجدت على جيثب .