ObjectRepository - Pola repositori dalam memori .NET untuk proyek rumah Anda

Mengapa menyimpan semua data di memori?

Untuk menyimpan data situs web atau backend, keinginan pertama kebanyakan orang waras adalah memilih database SQL. 

Namun terkadang muncul pemikiran bahwa model data tidak cocok untuk SQL: misalnya, saat membuat penelusuran atau grafik sosial, Anda perlu mencari hubungan kompleks antar objek. 

Situasi terburuk adalah ketika Anda bekerja dalam tim dan seorang rekan kerja tidak tahu cara membuat pertanyaan cepat. Berapa banyak waktu yang Anda habiskan untuk menyelesaikan masalah N+1 dan membuat indeks tambahan sehingga SELECT di halaman utama dapat diselesaikan dalam jangka waktu yang wajar?

Pendekatan populer lainnya adalah NoSQL. Beberapa tahun yang lalu ada banyak hype seputar topik ini - untuk setiap kesempatan mereka menggunakan MongoDB dan senang dengan jawaban dalam bentuk dokumen json (Ngomong-ngomong, berapa banyak kruk yang harus Anda masukkan karena adanya tautan melingkar di dokumen?).

Saya sarankan mencoba metode alternatif lain - mengapa tidak mencoba menyimpan semua data dalam memori aplikasi, menyimpannya secara berkala ke penyimpanan acak (file, database jarak jauh)? 

Memori menjadi murah, dan semua kemungkinan data untuk sebagian besar proyek kecil dan menengah akan muat dalam memori 1 GB. (Misalnya, proyek rumah favorit saya adalah pelacak keuangan, yang menyimpan statistik harian dan riwayat pengeluaran, saldo, dan transaksi saya selama satu setengah tahun, hanya menggunakan memori 45 MB.)

Pro:

  • Akses ke data menjadi lebih mudah - Anda tidak perlu khawatir tentang kueri, pemuatan lambat, fitur ORM, Anda bekerja dengan objek C# biasa;
  • Tidak ada masalah yang terkait dengan akses dari thread yang berbeda;
  • Sangat cepat - tidak ada permintaan jaringan, tidak ada terjemahan kode ke dalam bahasa kueri, tidak perlu (de)serialisasi objek;
  • Menyimpan data dalam bentuk apa pun dapat diterima - baik dalam XML pada disk, atau di SQL Server, atau di Azure Table Storage.

Cons:

  • Penskalaan horizontal hilang, dan akibatnya, penerapan zero downtime tidak dapat dilakukan;
  • Jika aplikasi mogok, Anda mungkin kehilangan sebagian data. (Tetapi aplikasi kita tidak pernah crash, kan?)

Bagaimana cara kerjanya?

Algoritmanya adalah sebagai berikut:

  • Pada awalnya, koneksi dibuat dengan penyimpanan data, dan data dimuat;
  • Model objek, indeks utama, dan indeks relasional (1:1, 1:Banyak) dibangun;
  • Langganan dibuat untuk perubahan properti objek (INotifyPropertyChanged) dan untuk menambah atau menghapus elemen ke koleksi (INotifyCollectionChanged);
  • Ketika langganan dipicu, objek yang diubah ditambahkan ke antrian untuk ditulis ke penyimpanan data;
  • Perubahan pada penyimpanan disimpan secara berkala (pada pengatur waktu) di thread latar belakang;
  • Saat Anda keluar dari aplikasi, perubahan juga disimpan ke penyimpanan.

Contoh Kode

Menambahkan dependensi 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 menggambarkan model data yang akan disimpan dalam penyimpanan

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 objeknya:

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 terakhir, 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 instance ObjectRepository:

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

Jika proyek akan menggunakan HangFire

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

Memasukkan objek baru:

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

Dengan panggilan ini, objeknya Model Induk ditambahkan ke cache lokal dan antrian untuk menulis ke database. Oleh karena itu, operasi ini memerlukan O(1), dan objek ini dapat segera dikerjakan.

Misalnya, untuk menemukan objek ini di repositori dan memverifikasi bahwa objek yang dikembalikan adalah instance yang sama:

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

Apa yang terjadi dengan ini? Mengatur () kembali Kamus Tabel, yang mengandung Kamus Bersamaan dan menyediakan fungsionalitas tambahan dari indeks primer dan sekunder. Hal ini memungkinkan Anda memiliki metode untuk mencari berdasarkan Id (atau indeks pengguna sewenang-wenang lainnya) tanpa sepenuhnya mengulangi semua objek.

Saat menambahkan objek ke ObyekRepositori langganan ditambahkan untuk mengubah propertinya, sehingga setiap perubahan pada properti juga mengakibatkan objek ini ditambahkan ke antrian tulis. 
Memperbarui properti dari luar terlihat sama seperti bekerja dengan objek POCO:

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

Anda dapat menghapus suatu objek dengan cara berikut:

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

Ini juga menambahkan objek ke antrian penghapusan.

Bagaimana cara kerja menabung?

ObyekRepositori ketika objek yang dipantau berubah (baik menambah atau menghapus, atau mengubah properti), memunculkan suatu peristiwa Model Berubahberlangganan ke Penyimpanan. Implementasi Penyimpanan ketika suatu peristiwa terjadi Model Berubah perubahan dimasukkan ke dalam 3 antrian - untuk menambah, untuk memperbarui, dan untuk menghapus.

Juga implementasi Penyimpanan setelah inisialisasi, mereka membuat pengatur waktu yang menyebabkan perubahan disimpan setiap 5 detik. 

Selain itu, ada API untuk memaksa panggilan simpan: ObjectRepository.Simpan().

Sebelum setiap penyimpanan, operasi yang tidak berarti terlebih dahulu dihapus dari antrian (misalnya, peristiwa duplikat - ketika suatu objek diubah dua kali atau dengan cepat menambahkan/menghapus objek), dan baru kemudian penyimpanan itu sendiri. 

Dalam semua kasus, seluruh objek saat ini disimpan, sehingga ada kemungkinan bahwa objek disimpan dalam urutan yang berbeda dari yang diubah, termasuk versi objek yang lebih baru dibandingkan saat objek tersebut ditambahkan ke antrean.

Apalagi yang ada disana?

  • Semua perpustakaan didasarkan pada .NET Standard 2.0. Dapat digunakan di proyek .NET modern apa pun.
  • API ini aman untuk thread. Koleksi internal dilaksanakan berdasarkan Kamus Bersamaan, pengendali acara memiliki kunci atau tidak memerlukannya. 
    Satu-satunya hal yang perlu diingat adalah menelepon ObjectRepository.Simpan();
  • Indeks sewenang-wenang (membutuhkan keunikan):

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

Siapa yang menggunakannya?

Secara pribadi, saya mulai menggunakan pendekatan ini di semua proyek hobi karena nyaman dan tidak memerlukan biaya besar untuk menulis lapisan akses data atau menerapkan infrastruktur berat. Secara pribadi, menyimpan data di litedb atau file biasanya cukup bagi saya. 

Namun di masa lalu, ketika startup EscapeTeams (yang sekarang sudah tidak berfungsi)Saya pikir ini dia, uang - tapi tidak, pengalaman lagi) - digunakan untuk menyimpan data di Azure Table Storage.

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

Saya ingin memperbaiki salah satu kelemahan utama dari pendekatan ini - penskalaan horizontal. Untuk melakukan ini, Anda memerlukan transaksi terdistribusi (sic!), atau membuat keputusan berkemauan keras bahwa data yang sama dari instance yang berbeda tidak boleh berubah, atau membiarkannya berubah sesuai dengan prinsip “siapa yang terakhir adalah yang benar.”

Dari sudut pandang teknis, saya melihat skema berikut ini mungkin:

  • Simpan EventLog dan Snapshot alih-alih model objek
  • Temukan instance lain (tambahkan titik akhir semua instance ke pengaturan? penemuan udp? master/slave?)
  • Replikasi antar instans EventLog melalui algoritma konsensus apa pun, seperti RAFT.

Ada juga masalah lain yang membuat saya khawatir - penghapusan berjenjang, atau deteksi kasus penghapusan objek yang memiliki tautan dari objek lain. 

Sumber

Jika Anda sudah membaca sampai di sini, yang tersisa hanyalah membaca kodenya; kode tersebut dapat ditemukan di GitHub:
https://github.com/DiverOfDark/ObjectRepository

Sumber: www.habr.com

Tambah komentar