ObjectRepository - corak repositori dalam memori .NET untuk projek rumah anda

Mengapa menyimpan semua data dalam ingatan?

Untuk menyimpan data tapak atau bahagian belakang, keinginan pertama kebanyakan orang yang waras ialah memilih pangkalan data SQL. 

Tetapi kadangkala terlintas di fikiran bahawa model data tidak sesuai untuk SQL: sebagai contoh, apabila membina carian atau graf sosial, anda perlu mencari hubungan yang kompleks antara objek. 

Situasi paling teruk ialah apabila anda bekerja dalam satu pasukan dan rakan sekerja tidak tahu cara membina pertanyaan pantas. Berapa banyak masa yang anda habiskan untuk menyelesaikan masalah N+1 dan membina indeks tambahan supaya SELECT pada halaman utama akan selesai dalam jumlah masa yang munasabah?

Satu lagi pendekatan popular ialah NoSQL. Beberapa tahun yang lalu terdapat banyak gembar-gembur mengenai topik ini - untuk sebarang kesempatan yang sesuai mereka menggunakan MongoDB dan gembira dengan jawapan dalam bentuk dokumen json (by the way, berapa banyak tongkat yang anda perlu masukkan kerana pautan bulat dalam dokumen?).

Saya cadangkan mencuba kaedah alternatif lain - mengapa tidak cuba menyimpan semua data dalam memori aplikasi, secara berkala menyimpannya ke storan rawak (fail, pangkalan data jauh)? 

Memori telah menjadi murah, dan sebarang data yang mungkin untuk kebanyakan projek kecil dan sederhana akan dimuatkan ke dalam memori 1 GB. (Sebagai contoh, projek rumah kegemaran saya ialah penjejak kewangan, yang menyimpan statistik harian dan sejarah perbelanjaan, baki dan transaksi saya selama setahun setengah, menggunakan hanya 45 MB memori.)

Kelebihan:

  • Akses kepada data menjadi lebih mudah - anda tidak perlu risau tentang pertanyaan, pemuatan malas, ciri ORM, anda bekerja dengan objek C# biasa;
  • Tiada masalah yang berkaitan dengan akses daripada urutan yang berbeza;
  • Sangat pantas - tiada permintaan rangkaian, tiada terjemahan kod ke dalam bahasa pertanyaan, tidak perlu (nyah) pensirilan objek;
  • Ia boleh diterima untuk menyimpan data dalam sebarang bentuk - sama ada dalam XML pada cakera, atau dalam SQL Server, atau dalam Azure Table Storage.

Cons:

  • Penskalaan mendatar hilang, dan akibatnya, penggunaan masa henti sifar tidak dapat dilakukan;
  • Jika aplikasi ranap, anda mungkin kehilangan sebahagian data. (Tetapi aplikasi kami tidak pernah ranap, bukan?)

Bagaimana ia berfungsi?

Algoritma adalah seperti berikut:

  • Pada permulaan, sambungan diwujudkan dengan storan data, dan data dimuatkan;
  • Model objek, indeks utama dan indeks hubungan (1:1, 1:Banyak) dibina;
  • Langganan dibuat untuk perubahan dalam sifat objek (INotifyPropertyChanged) dan untuk menambah atau mengalih keluar elemen pada koleksi (INotifyCollectionChanged);
  • Apabila langganan dicetuskan, objek yang diubah ditambahkan pada baris gilir untuk menulis ke storan data;
  • Perubahan pada storan disimpan secara berkala (pada pemasa) dalam benang latar belakang;
  • Apabila anda keluar dari aplikasi, perubahan juga disimpan pada storan.

Contoh kod

Menambah kebergantungan yang diperlukan

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

Kami menerangkan model data yang akan disimpan dalam storan

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

Kemudian model objek:

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

Dan akhirnya, kelas repositori itu sendiri untuk mengakses data:

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

Buat contoh ObjectRepository:

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

Jika projek itu akan menggunakan HangFire

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

Memasukkan objek baharu:

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

Dengan panggilan ini, objek ParentModel ditambahkan pada kedua-dua cache tempatan dan baris gilir untuk menulis ke pangkalan data. Oleh itu, operasi ini mengambil O(1), dan objek ini boleh digunakan dengan serta-merta.

Sebagai contoh, untuk mencari objek ini dalam repositori dan mengesahkan bahawa objek yang dikembalikan adalah contoh yang sama:

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

Apa yang sedang berlaku? Tetapkan () pulangan Kamus Jadual, yang mengandungi ConcurrentDictionary dan menyediakan fungsi tambahan indeks primer dan sekunder. Ini membolehkan anda mempunyai kaedah untuk mencari mengikut Id (atau indeks pengguna lain yang sewenang-wenangnya) tanpa lelaran sepenuhnya ke atas semua objek.

Apabila menambah objek ke ObjectRepository langganan ditambahkan untuk menukar sifatnya, jadi sebarang perubahan dalam sifat juga menyebabkan objek ini ditambahkan pada baris gilir tulis. 
Mengemas kini sifat dari luar kelihatan sama seperti bekerja dengan objek POCO:

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

Anda boleh memadamkan objek dengan cara berikut:

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

Ini juga menambah objek pada baris gilir pemadaman.

Bagaimanakah penjimatan berfungsi?

ObjectRepository apabila objek yang dipantau berubah (sama ada menambah atau memadam, atau menukar sifat), menimbulkan acara ModelDiubahmelanggan Penyimpanan. Perlaksanaan Penyimpanan apabila sesuatu kejadian berlaku ModelDiubah perubahan dimasukkan ke dalam 3 baris gilir - untuk menambah, untuk mengemas kini, dan untuk memadam.

Juga pelaksanaan Penyimpanan selepas permulaan, mereka mencipta pemasa yang menyebabkan perubahan disimpan setiap 5 saat. 

Di samping itu, terdapat API untuk memaksa panggilan simpan: ObjectRepository.Save().

Sebelum setiap simpan, operasi yang tidak bermakna terlebih dahulu dialih keluar daripada baris gilir (contohnya, peristiwa pendua - apabila objek ditukar dua kali atau objek ditambah/dialih keluar dengan cepat), dan barulah simpan itu sendiri. 

Dalam semua kes, keseluruhan objek semasa disimpan, jadi ada kemungkinan objek disimpan dalam susunan yang berbeza daripada yang telah diubah, termasuk versi objek yang lebih baharu daripada pada masa objek itu ditambahkan pada baris gilir.

Apa lagi yang ada?

  • Semua perpustakaan adalah berdasarkan .NET Standard 2.0. Boleh digunakan dalam mana-mana projek .NET moden.
  • API adalah selamat untuk thread. Koleksi dalaman dilaksanakan berdasarkan SerentakDictionary, pengendali acara sama ada mempunyai kunci atau tidak memerlukannya. 
    Satu-satunya perkara yang perlu diingat ialah menelefon ObjectRepository.Save();
  • Indeks sewenang-wenang (memerlukan keunikan):

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

Siapa yang menggunakannya?

Secara peribadi, saya mula menggunakan pendekatan ini dalam semua projek hobi kerana ia mudah dan tidak memerlukan perbelanjaan yang besar untuk menulis lapisan akses data atau menggunakan infrastruktur yang berat. Secara peribadi, menyimpan data dalam litedb atau fail biasanya cukup untuk saya. 

Tetapi pada masa lalu, apabila EscapeTeams permulaan yang tidak berfungsi (Saya fikir inilah, wang - tetapi tidak, pengalaman lagi) - digunakan untuk menyimpan data dalam Penyimpanan Jadual Azure.

Планы на будущее

Saya ingin membetulkan salah satu kelemahan utama pendekatan ini - penskalaan mendatar. Untuk melakukan ini, anda memerlukan sama ada transaksi yang diedarkan (sic!), atau membuat keputusan yang bersungguh-sungguh bahawa data yang sama daripada kejadian yang berbeza tidak seharusnya berubah, atau biarkan mereka berubah mengikut prinsip "siapa yang terakhir adalah betul."

Dari sudut pandangan teknikal, saya melihat skema berikut mungkin:

  • Simpan EventLog dan Snapshot bukannya model objek
  • Cari kejadian lain (tambah titik akhir semua kejadian pada tetapan? udp discovery? master/slave?)
  • Replikasi antara kejadian EventLog melalui sebarang algoritma konsensus, seperti RAFT.

Terdapat juga masalah lain yang membimbangkan saya - pemadaman lata, atau pengesanan kes pemadaman objek yang mempunyai pautan dari objek lain. 

Kod sumber

Jika anda telah membaca sehingga ke sini, yang tinggal hanyalah membaca kod; ia boleh didapati di GitHub:
https://github.com/DiverOfDark/ObjectRepository

Sumber: www.habr.com

Tambah komen