ObjectRepository - الگوی مخزن درون حافظه دات نت برای پروژه های خانگی شما

چرا همه داده ها را در حافظه ذخیره می کنیم؟

برای ذخیره داده های وب سایت یا باطن، اولین خواسته اکثر افراد عاقل انتخاب یک پایگاه داده SQL خواهد بود. 

اما گاهی اوقات این فکر به ذهن خطور می کند که مدل داده برای SQL مناسب نیست: برای مثال، هنگام ساخت یک نمودار جستجو یا اجتماعی، باید روابط پیچیده بین اشیاء را جستجو کنید. 

بدترین وضعیت زمانی است که شما در یک تیم کار می کنید و یک همکار نمی داند چگونه پرس و جوهای سریع بسازد. چقدر برای حل مسائل N+1 و ایجاد نمایه های اضافی صرف کردید تا SELECT در صفحه اصلی در مدت زمان معقولی تکمیل شود؟

روش محبوب دیگر NoSQL است. چندین سال پیش هیاهوی زیادی در مورد این موضوع وجود داشت - برای هر موقعیت مناسب آنها MongoDB را مستقر کردند و از پاسخ ها در قالب اسناد json خوشحال بودند. (در ضمن، به دلیل لینک های دایره ای در اسناد، چند عصا باید وارد کنید؟).

پیشنهاد می‌کنم روش جایگزین دیگری را امتحان کنید - چرا سعی نکنید تمام داده‌ها را در حافظه برنامه ذخیره کنید، و به طور دوره‌ای آن را در ذخیره‌سازی تصادفی (فایل، پایگاه داده راه دور) ذخیره نکنید؟ 

حافظه ارزان شده است و هر داده ممکن برای اکثر پروژه های کوچک و متوسط ​​در 1 گیگابایت حافظه جای می گیرد. (به عنوان مثال، پروژه خانه مورد علاقه من است ردیاب مالیکه آمار و تاریخچه روزانه هزینه ها، موجودی ها و تراکنش های من را به مدت یک سال و نیم نگه می دارد، فقط 45 مگابایت حافظه مصرف می کند.)

مزایا:

  • دسترسی به داده‌ها آسان‌تر می‌شود - نیازی نیست نگران پرس و جوها، بارگذاری تنبل، ویژگی‌های ORM باشید، با اشیاء C# معمولی کار می‌کنید.
  • هیچ مشکلی در ارتباط با دسترسی از موضوعات مختلف وجود ندارد.
  • بسیار سریع - بدون درخواست شبکه، بدون ترجمه کد به زبان پرس و جو، بدون نیاز به (غیر) سریال سازی اشیاء.
  • ذخیره داده ها به هر شکلی قابل قبول است - چه در XML روی دیسک، چه در SQL Server یا در Azure Table Storage.

منفی:

  • مقیاس افقی از بین می رود، و در نتیجه، استقرار زمان خاموشی صفر نمی تواند انجام شود.
  • اگر برنامه خراب شود، ممکن است تا حدی اطلاعات را از دست بدهید. (اما برنامه ما هرگز خراب نمی شود، درست است؟)

چگونه کار می کند؟

الگوریتم به شرح زیر است:

  • در شروع، یک اتصال با ذخیره سازی داده برقرار می شود و داده ها بارگیری می شوند.
  • یک مدل شی، شاخص های اولیه و شاخص های رابطه ای (1:1، 1:Many) ساخته می شوند.
  • یک اشتراک برای تغییرات در خصوصیات شی (INotifyPropertyChanged) و برای افزودن یا حذف عناصر به مجموعه (INotifyCollectionChanged) ایجاد می شود.
  • هنگامی که اشتراک فعال می شود، شی تغییر یافته به صف برای نوشتن در ذخیره سازی داده اضافه می شود.
  • تغییرات ذخیره سازی به صورت دوره ای (در یک تایمر) در یک موضوع پس زمینه ذخیره می شود.
  • هنگامی که از برنامه خارج می شوید، تغییرات نیز در حافظه ذخیره می شود.

نمونه کد

افزودن وابستگی های لازم

// Основная библиотека
Install-Package OutCode.EscapeTeams.ObjectRepository
    
// Хранилище данных, в котором будут сохраняться изменения
// Используйте то, которым будете пользоваться.
Install-Package OutCode.EscapeTeams.ObjectRepository.File
Install-Package OutCode.EscapeTeams.ObjectRepository.LiteDb
Install-Package OutCode.EscapeTeams.ObjectRepository.AzureTableStorage
    
// Опционально - если нужно хранить модель данных для Hangfire
// Install-Package OutCode.EscapeTeams.ObjectRepository.Hangfire

مدل داده ای که در حافظه ذخیره می شود را توضیح می دهیم

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 Guid? NullableId => null;
    
    // Пример связи 1:Many
    public IEnumerable<ChildModel> Children => Multiple<ChildModel>(x => x.ParentId);
    
    protected override BaseEntity Entity { get; }
}
    
public class ChildModel : ModelBase
{
    private ChildEntity _childEntity;
    
    public ChildModel(ChildEntity entity)
    {
        _childEntity = entity;
    }
    
    public ChildModel() 
    {
        _childEntity = new ChildEntity(Guid.NewGuid());
    }
    
    public Guid ParentId
    {
        get => _childEntity.ParentId;
        set => UpdateProperty(() => _childEntity.ParentId, value);
    }
    
    public string Value
    {
        get => _childEntity.Value;
        set => UpdateProperty(() => _childEntity.Value, value
    }
    
    // Доступ с поиском по индексу
    public ParentModel Parent => Single<ParentModel>(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

و در نهایت، خود کلاس مخزن برای دسترسی به داده ها:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Для тестов, позволяет не сохранять изменения в базу
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Если используется Hangfire и необходимо хранить модель данных для Hangfire в ObjectRepository
        // this.RegisterHangfireScheme(); 
    
        Initialize();
    }
}

یک نمونه 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));

چه اتفاقی می افتد؟ تنظیم () برمی گرداند دیکشنری جدول، که شامل فرهنگ لغت همزمان و عملکرد اضافی شاخص های اولیه و ثانویه را فراهم می کند. این به شما امکان می دهد تا روش هایی برای جستجو بر اساس شناسه (یا سایر نمایه های کاربر دلخواه) بدون تکرار کامل روی همه اشیا داشته باشید.

هنگام اضافه کردن اشیا به ObjectRepository اشتراکی برای تغییر خصوصیات آنها اضافه می شود، بنابراین هر تغییری در ویژگی ها منجر به اضافه شدن این شی به صف نوشتن می شود. 
به روز رسانی ویژگی ها از بیرون مانند کار با یک شی POCO به نظر می رسد:

myParent.Children.First().Property = "Updated value";

شما می توانید یک شی را به روش های زیر حذف کنید:

repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove<ParentModel>(x => !x.Children.Any());

این نیز شی را به صف حذف اضافه می کند.

پس انداز چگونه کار می کند؟

ObjectRepository هنگامی که اشیاء نظارت شده تغییر می کنند (افزودن یا حذف، یا تغییر خصوصیات)، یک رویداد را افزایش می دهد مدل تغییر کردمشترک در ذخیره سازی. پیاده سازی ها ذخیره سازی زمانی که یک رویداد رخ می دهد مدل تغییر کرد تغییرات در 3 صف قرار می گیرند - برای اضافه کردن، برای به روز رسانی، و برای حذف.

همچنین پیاده سازی ها ذخیره سازی پس از مقداردهی اولیه، آنها یک تایمر ایجاد می کنند که باعث می شود تغییرات هر 5 ثانیه ذخیره شوند. 

علاوه بر این، یک API برای اجبار یک تماس ذخیره وجود دارد: ObjectRepository.Save().

قبل از هر ذخیره، ابتدا عملیات بی معنی از صف ها حذف می شود (به عنوان مثال، رویدادهای تکراری - زمانی که یک شی دو بار تغییر کرد یا به سرعت اشیاء اضافه/حذف شد)، و تنها پس از آن خود ذخیره می شود. 

در همه موارد، کل شی فعلی ذخیره می شود، بنابراین ممکن است اشیا با ترتیبی متفاوت از تغییرشان ذخیره شوند، از جمله نسخه های جدیدتر اشیاء نسبت به زمانی که به صف اضافه شده اند.

چه چیز دیگری آنجاست؟

  • همه کتابخانه ها بر اساس دات نت استاندارد 2.0 هستند. قابل استفاده در هر پروژه مدرن دات نت.
  • API ایمن است. مجموعه های داخلی بر اساس پیاده سازی می شوند فرهنگ لغت همزمان، گردانندگان رویداد یا قفل دارند یا به آنها نیاز ندارند. 
    تنها چیزی که ارزش به خاطر سپردن دارد تماس گرفتن است ObjectRepository.Save();
  • شاخص های دلخواه (نیاز به منحصر به فرد بودن):

repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");

چه کسی از آن استفاده می کند؟

من شخصاً استفاده از این رویکرد را در همه پروژه های سرگرمی شروع کردم زیرا راحت است و برای نوشتن یک لایه دسترسی به داده یا استقرار زیرساخت های سنگین نیازی به هزینه های زیادی ندارد. شخصاً معمولاً ذخیره داده ها در litedb یا یک فایل برای من کافی است. 

اما در گذشته، زمانی که استارت آپ منقرض شده EscapeTeams (من فکر کردم اینجا پول است - اما نه، دوباره تجربه کنید) - برای ذخیره داده ها در Azure Table Storage استفاده می شود.

برنامه های آینده

من می خواهم یکی از معایب اصلی این روش را برطرف کنم - مقیاس بندی افقی. برای انجام این کار، یا به تراکنش‌های توزیع‌شده نیاز دارید (sic!)، یا با اراده قوی تصمیم بگیرید که داده‌های مشابه از نمونه‌های مختلف نباید تغییر کنند، یا اجازه دهید طبق اصل «آخرین حق با کیست» تغییر کنند.

از نقطه نظر فنی، من طرح زیر را ممکن می بینم:

  • ذخیره EventLog و Snapshot به جای شی مدل
  • نمونه‌های دیگر را پیدا کنید (نقاط پایانی همه نمونه‌ها را به تنظیمات اضافه کنید؟ کشف udp؟ master/slave؟)
  • بین موارد EventLog از طریق هر الگوریتم توافقی مانند RAFT تکرار کنید.

همچنین مشکل دیگری وجود دارد که من را نگران می کند - حذف آبشاری یا تشخیص موارد حذف اشیایی که پیوندهایی از اشیاء دیگر دارند. 

منبع

اگر تا اینجا را خوانده‌اید، تنها چیزی که باقی می‌ماند خواندن کد است؛ این کد را می‌توانید در GitHub پیدا کنید:
https://github.com/DiverOfDark/ObjectRepository

منبع: www.habr.com

اضافه کردن نظر