Perchè almacenà tutti i dati in memoria?
Per almacenà u situ web o dati backend, u primu desideriu di a maiò parte di e persone sana serà di sceglie una basa di dati SQL.
Ma qualchì volta u pensamentu vene in mente chì u mudellu di dati ùn hè micca adattatu per SQL: per esempiu, quandu custruisce una ricerca o gràficu suciale, avete bisognu di ricercà relazioni cumplessi trà l'uggetti.
A peor situazione hè quandu travagliate in una squadra è un cullegu ùn sapi micca cumu fà dumande veloci. Quantu tempu avete passatu à risolve i prublemi N + 1 è à custruisce indici supplementari in modu chì u SELECT in a pagina principale finisce in un tempu raghjone?
Un altru approcciu populari hè NoSQL. Parechji anni fà ci era assai hype intornu à questu tema - per ogni occasione cunvene anu implementatu MongoDB è eranu felici cù e risposte in forma di documenti json. (A propositu, quante crutches avete da inserisce per via di i ligami circulari in i ducumenti ?).
Suggeriu di pruvà un altru mètudu alternativu - perchè ùn pruvate micca di almacenà tutte e dati in a memoria di l'applicazione, salvendu periodicamente in un almacenamentu aleatoriu (file, basa di dati remota)?
A memoria hè diventata economica, è qualsiasi dati pussibuli per a maiò parte di i prughjetti chjuchi è mediani si mette in 1 GB di memoria. (Per esempiu, u mo prughjettu di casa preferitu hè
Pros:
- L'accessu à e dati diventa più faciule - ùn avete micca bisognu di preoccupassi di e dumande, di carica lazy, di funzioni ORM, di travaglià cù l'uggetti C# ordinali;
- Ùn ci sò micca prublemi assuciati cù l'accessu da diversi fili;
- Moltu veloce - senza richieste di rete, senza traduzzione di codice in una lingua di quistione, senza bisognu di (de) serializazione di l'uggetti;
- Hè accettatu per almacenà dati in ogni forma - sia in XML in discu, o in SQL Server, o in Azure Table Storage.
Cons:
- A scala horizontale hè persa, è in u risultatu, l'implementazione zero downtime ùn pò esse fatta;
- Sè l 'applicazzioni crashes, vi pò parzialmente perde dati. (Ma a nostra applicazione ùn si ferma mai, nò?)
Cumu viaghja?
L'algoritmu hè u seguente:
- À u principiu, una cunnessione hè stabilita cù l'almacenamiento di dati, è i dati sò caricati;
- Un mudellu d'ughjettu, indici primari è indici relazionali (1: 1, 1: Many) sò custruitu;
- Un abbonamentu hè creatu per i cambiamenti in e proprietà di l'ughjettu (INotifyPropertyChanged) è per aghjunghje o sguassà elementi à a cullezzione (INotifyCollectionChanged);
- Quandu l'abbunamentu hè attivatu, l'ughjettu cambiatu hè aghjuntu à a fila per scrive à l'almacenamiento di dati;
- I cambiamenti à l'almacenamiento sò salvati periodicamente (in un timer) in un filu di fondo;
- Quandu esce da l'applicazione, i cambiamenti sò ancu salvati in u almacenamiento.
Esempiu di codice
Aghjunghjendu i dependenzi necessarii
// Основная библиотека
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
Descrivimu u mudellu di dati chì serà guardatu in u almacenamiento
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; }
}
Allora u mudellu di l'ughjettu:
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;
}
È infine, a classa di repository stessu per accede à i dati:
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();
}
}
Crea una istanza di ObjectRepository:
var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();
Se u prugettu aduprà HangFire
public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}
Inserisce un novu ogettu:
var newParent = new ParentModel()
repository.Add(newParent);
Cù sta chjama, l'ughjettu ParentModel hè aghjuntu à a cache locale è à a fila per scrive à a basa di dati. Dunque, sta operazione piglia O (1), è questu ughjettu pò esse travagliatu immediatamente.
Per esempiu, per truvà questu ughjettu in u repositoriu è verificate chì l'ughjettu restituitu hè a stessa istanza:
var parents = repository.Set<ParentModel>();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));
Chì succede ? Set () torna TableDictionary, chì cuntene Dictionary Concurrent è furnisce funziunalità supplementari di l'indici primari è secundarii. Questu permette di avè metudi per a ricerca per Id (o altri indici d'utilizatori arbitrarii) senza iterazione cumplettamente nantu à tutti l'uggetti.
Quandu aghjunghje l'uggetti à Repository d'oggetti un abbunamentu hè aghjuntu à cambià e so proprietà, cusì ogni cambiamentu di pruprietà hè ancu risultatu in questu oggettu chì hè aghjuntu à a fila di scrittura.
L'aghjurnà e proprietà da l'esternu s'assumiglia à travaglià cù un oggettu POCO:
myParent.Children.First().Property = "Updated value";
Pudete sguassà un oggettu in i seguenti modi:
repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove<ParentModel>(x => !x.Children.Any());
Questu aghjunghje ancu l'ughjettu à a fila di eliminazione.
Cumu u travagliu di salvezza?
Repository d'oggetti quandu l'uggetti monitorati cambianu (aghjunghje o sguassate, o mudificate proprietà), suscite un avvenimentu Mudellu cambiatuabbonatu à ISmagazzinu. Implementazioni ISmagazzinu quandu un avvenimentu accade Mudellu cambiatu i cambiamenti sò posti in fila 3 - per aghjunghje, per aghjurnà è per sguassà.
Ancu implementazioni ISmagazzinu dopu à l'inizializazione, creanu un timer chì face chì i cambiamenti sò salvati ogni 5 seconde.
Inoltre, ci hè una API per furzà una chjama di salvezza: ObjectRepository.Save ().
Prima di ogni salvezza, l'operazioni senza significatu sò prima sguassate da a fila (per esempiu, l'avvenimenti duplicati - quandu un ughjettu hè cambiatu duie volte o l'oggetti aghjustati / sguassati rapidamente), è solu dopu a salvezza stessu.
In tutti i casi, tuttu l'ughjettu attuale hè salvatu, cusì hè pussibule chì l'uggetti sò salvati in un ordine diffirenti di quelli chì sò stati cambiati, cumprese e versioni più recenti di l'uggetti chì à u mumentu chì sò stati aghjuntu à a fila.
Chì ci hè altru ?
- Tutte e librerie sò basate nantu à .NET Standard 2.0. Pò esse usatu in ogni prughjettu mudernu .NET.
- L'API hè thread safe. I cullizzioni internu sò implementati basatu nantu Dictionary Concurrent, i gestori di l'avvenimenti o anu chjusi o ùn ne anu micca bisognu.
L'unicu chì vale a pena ricurdà hè di chjamà ObjectRepository.Save (); - Indici arbitrari (esigene unicità):
repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");
Quale si usa ?
In modu persunale, aghju cuminciatu à aduprà stu approcciu in tutti i prughjetti di hobby perchè hè cunvene è ùn hè micca bisognu di grandi spese per scrive una capa d'accessu di dati o implementà una infrastruttura pesante. In modu persunale, l'almacenamiento di dati in litedb o un schedariu hè di solitu abbastanza per mè.
Ma in u passatu, quandu l'iniziu oramai difunta EscapeTeams (Pensu chì quì hè, soldi - ma micca, sperienza di novu) - utilizatu per almacenà dati in Azure Table Storage.
Piani di u futuru
Vogliu riparà unu di i principali disadvantages di stu approcciu - scala horizontale. Per fà questu, avete bisognu di transazzione distribuitu (sic!), O di piglià una decisione forte chì i stessi dati da diverse istanze ùn deve micca cambià, o lasciate cambià secondu u principiu "quale hè l'ultimu hè ghjustu".
Da un puntu di vista tecnicu, vecu u schema seguente pussibule:
- Store EventLog è Snapshot invece di mudellu d'ughjettu
- Truvate altre istanze (aghjunghje endpoints di tutte l'istanze à i paràmetri? udp discovery? master/slave?)
- Replica trà e istanze di EventLog via qualsiasi algoritmu di cunsensu, cum'è RAFT.
Ci hè ancu un altru prublema chì mi preoccupa - eliminazione in cascata, o rilevazione di casi di eliminazione di l'uggetti chì anu ligami da altri oggetti.
Codice fonte
Sè avete lettu finu à quì, allora tuttu ciò chì resta hè di leghje u codice; pò esse truvatu in GitHub:
Source: www.habr.com