ObjectRepository - Ev projeleriniz için .NET bellek içi depo modeli

Neden tüm verileri hafızada saklayalım?

Web sitesini veya arka uç verilerini depolamak için çoğu aklı başında insanın ilk arzusu bir SQL veritabanı seçmek olacaktır. 

Ancak bazen veri modelinin SQL'e uygun olmadığı düşüncesi akla geliyor: örneğin, bir arama veya sosyal grafik oluştururken nesneler arasındaki karmaşık ilişkileri aramanız gerekir. 

En kötü durum, bir ekipte çalıştığınızda ve bir meslektaşınızın hızlı sorguların nasıl oluşturulacağını bilmemesidir. Ana sayfadaki SELECT'in makul bir sürede tamamlanması için N+1 problemlerini çözmek ve ek dizinler oluşturmak için ne kadar zaman harcadınız?

Bir diğer popüler yaklaşım ise NoSQL'dir. Birkaç yıl önce bu konu etrafında çok fazla heyecan vardı; herhangi bir uygun durum için MongoDB'yi kullandılar ve json belgeleri biçimindeki yanıtlardan memnun kaldılar. (Bu arada, belgelerdeki dairesel bağlantılar nedeniyle kaç tane koltuk değneği takmak zorunda kaldınız?).

Başka bir alternatif yöntem denemenizi öneririm - neden tüm verileri uygulama belleğinde saklamayı, düzenli aralıklarla rastgele depolamaya (dosya, uzak veritabanı) kaydetmeyi denemiyorsunuz? 

Bellek ucuz hale geldi ve çoğu küçük ve orta ölçekli proje için mümkün olan her türlü veri 1 GB belleğe sığacak. (Örneğin, en sevdiğim ev projem finansal takipçiBir buçuk yıl boyunca harcamalarımın, bakiyelerimin ve işlemlerimin günlük istatistiklerini ve geçmişini tutan , yalnızca 45 MB bellek tüketiyor.)

Artıları:

  • Verilere erişim kolaylaşır; sorgular, geç yükleme, ORM özellikleri konusunda endişelenmenize gerek kalmaz, sıradan C# nesneleriyle çalışırsınız;
  • Farklı iş parçacıklarından erişimle ilgili herhangi bir sorun yoktur;
  • Çok hızlı - ağ isteği yok, kodun sorgu diline çevrilmesi yok, nesnelerin serileştirilmesine/serileştirilmesine gerek yok;
  • Verileri herhangi bir biçimde (diskteki XML'de, SQL Server'da veya Azure Tablo Depolama'da) depolamak kabul edilebilir.

Eksileri:

  • Yatay ölçeklendirme kaybolur ve bunun sonucunda sıfır kesinti süreli dağıtım yapılamaz;
  • Uygulama çökerse kısmen veri kaybedebilirsiniz. (Fakat uygulamamız asla çökmez, değil mi?)

Nasıl çalışır?

Algoritma aşağıdaki gibidir:

  • Başlangıçta veri deposuyla bağlantı kurulur ve veriler yüklenir;
  • Bir nesne modeli, birincil dizinler ve ilişkisel dizinler (1:1, 1:Çoklu) oluşturulur;
  • Nesne özelliklerindeki değişiklikler (INotifyPropertyChanged) ve koleksiyona öğe eklemek veya kaldırmak (INotifyCollectionChanged) için bir abonelik oluşturulur;
  • Abonelik tetiklendiğinde, değiştirilen nesne veri deposuna yazılmak üzere kuyruğa eklenir;
  • Depolamada yapılan değişiklikler periyodik olarak (bir zamanlayıcıda) bir arka plan iş parçacığına kaydedilir;
  • Uygulamadan çıktığınızda değişiklikler de depolama alanına kaydedilir.

Kod örneği

Gerekli bağımlılıkları ekleme

// Основная библиотека
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

Depolamada saklanacak veri modelini açıklıyoruz

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; }
}

Daha sonra nesne modeli:

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;
}

Ve son olarak verilere erişim için depo sınıfının kendisi:

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();
    }
}

Bir ObjectRepository örneği oluşturun:

var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
    
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();

Proje HangFire kullanacaksa

public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
    services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}

Yeni bir nesne ekleme:

var newParent = new ParentModel()
repository.Add(newParent);

Bu çağrı ile nesne EbeveynModeli veritabanına yazılmak üzere hem yerel önbelleğe hem de kuyruğa eklenir. Bu nedenle bu işlem O(1) alır ve bu nesneyle hemen çalışılabilir.

Örneğin, bu nesneyi depoda bulmak ve döndürülen nesnenin aynı örnek olduğunu doğrulamak için:

var parents = repository.Set<ParentModel>();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));

Ne oluyor? Ayarlamak () döner Tablo Sözlüğüiçeren Eşzamanlı Sözlük ve birincil ve ikincil dizinlerin ek işlevselliğini sağlar. Bu, tüm nesneler üzerinde tamamen yineleme yapmadan, Kimliğe (veya diğer isteğe bağlı kullanıcı dizinlerine) göre arama yapmak için yöntemlere sahip olmanızı sağlar.

Nesneleri eklerken Nesne Deposu özelliklerini değiştirmek için bir abonelik eklenir, dolayısıyla özelliklerdeki herhangi bir değişiklik bu nesnenin de yazma kuyruğuna eklenmesine neden olur. 
Özellikleri dışarıdan güncellemek, bir POCO nesnesiyle çalışmakla aynı görünür:

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

Bir nesneyi aşağıdaki yollarla silebilirsiniz:

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

Bu aynı zamanda nesneyi silme kuyruğuna da ekler.

Tasarruf nasıl çalışır?

Nesne Deposu izlenen nesneler değiştiğinde (ekleme, silme veya özellikleri değiştirme) bir olay başlatır ModelDeğiştirildiabone oldum Depolama. Uygulamalar Depolama bir olay meydana geldiğinde ModelDeğiştirildi değişiklikler 3 sıraya konur; ekleme, güncelleme ve silme için.

Ayrıca uygulamalar Depolama Başlatma sonrasında, değişikliklerin her 5 saniyede bir kaydedilmesine neden olan bir zamanlayıcı oluştururlar. 

Ek olarak, kaydetme çağrısını zorlamak için bir API vardır: ObjectRepository.Save().

Her kaydetmeden önce, anlamsız işlemler ilk önce kuyruklardan kaldırılır (örneğin, yinelenen olaylar - bir nesnenin iki kez değiştirildiği veya nesnelerin hızlı bir şekilde eklendiği/kaldırıldığı durumlar) ve ancak bundan sonra kaydetmenin kendisi. 

Her durumda, mevcut nesnenin tamamı kaydedilir; dolayısıyla, nesnelerin kuyruğa eklendikleri andaki daha yeni sürümleri de dahil olmak üzere, değiştirildikleri sırada farklı bir sırada kaydedilmeleri mümkündür.

Orada başka neler var?

  • Tüm kitaplıklar .NET Standard 2.0'ı temel alır. Herhangi bir modern .NET projesinde kullanılabilir.
  • API iş parçacığı açısından güvenlidir. Dahili koleksiyonlar aşağıdakilere göre uygulanır: Eşzamanlı Sözlükolay işleyicilerinin ya kilitleri vardır ya da bunlara ihtiyaçları yoktur. 
    Hatırlamaya değer tek şey aramaktır ObjectRepository.Save();
  • Rasgele indeksler (benzersizlik gerektirir):

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

Kim kullanıyor?

Kişisel olarak bu yaklaşımı tüm hobi projelerinde kullanmaya başladım çünkü kullanışlıdır ve bir veri erişim katmanı yazmak veya ağır altyapıyı dağıtmak için büyük masraflar gerektirmez. Kişisel olarak verileri litedb'de veya bir dosyada saklamak benim için genellikle yeterli oluyor. 

Ancak geçmişte, artık feshedilmiş bir girişim olan EscapeTeams (İşte burada, para diye düşündüm - ama hayır, yine deneyim) - verileri Azure Tablo Depolama'da depolamak için kullanılır.

Gelecek için planlar

Bu yaklaşımın ana dezavantajlarından biri olan yatay ölçeklendirmeyi düzeltmek istiyorum. Bunu yapmak için, ya dağıtılmış işlemlere (aynen böyle!) ihtiyacınız var ya da farklı örneklerden gelen aynı verilerin değişmemesi konusunda güçlü bir karar vermeniz ya da bunların "sonuncu olan haklıdır" ilkesine göre değişmesine izin vermeniz gerekiyor.

Teknik açıdan bakıldığında aşağıdaki şemayı mümkün olarak görüyorum:

  • Nesne modeli yerine EventLog ve Snapshot'ı depolayın
  • Diğer örnekleri bulun (tüm örneklerin uç noktalarını ayarlara ekleyin? udp keşfi? ana/bağımlı?)
  • RAFT gibi herhangi bir konsensüs algoritması aracılığıyla EventLog örnekleri arasında çoğaltma yapın.

Beni endişelendiren başka bir sorun daha var - kademeli silme veya diğer nesnelerden bağlantıları olan nesnelerin silinme durumlarının tespiti. 

Kaynak kodu

Buraya kadar okuduysanız geriye kalan tek şey kodu okumaktır; kodu GitHub'da bulabilirsiniz:
https://github.com/DiverOfDark/ObjectRepository

Kaynak: habr.com

Yorum ekle