Por que armazenar todos os dados na memória?
Para armazenar dados de site ou de back-end, o primeiro desejo da maioria das pessoas sãs será um banco de dados SQL.
Mas, às vezes, surge a ideia de que o modelo de dados não é adequado para SQL: por exemplo, ao criar uma pesquisa ou um gráfico social, você precisa procurar relacionamentos complexos entre objetos.
A pior situação é quando você trabalha em equipe e um colega não é capaz de criar consultas rápidas. Quanto tempo você gastou na solução de problemas de N + 1 e na criação de índices adicionais para que o SELECT na página principal funcionasse em um tempo razoável?
Outra abordagem popular é o NoSQL. Alguns anos atrás, houve um grande hype em torno desse tópico - para qualquer oportunidade, implantamos o MongoDB e ficamos felizes com as respostas na forma de json-documents (a propósito, quantas muletas tiveram que ser inseridas por causa de links circulares nos documentos?) .
Por que não tentar armazenar todos os dados na memória do aplicativo, salvando-os periodicamente em armazenamento arbitrário (arquivo, banco de dados remoto)?
A memória ficou barata e todos os dados possíveis da maioria dos projetos pequenos e médios cabem em 1 GB de memória. (Por exemplo, meu projeto doméstico favorito - um rastreador financeiro que mantém estatísticas diárias e um histórico de minhas despesas, saldos e transações por um ano e meio consome apenas 45 MB de memória.)
Prós:
- O acesso aos dados está se tornando mais fácil - não há necessidade de se preocupar com consultas, carregamento lento, recursos ORM, trabalhar com objetos C # comuns;
- Não há problemas associados ao acesso de diferentes segmentos;
- Muito rápido - sem solicitações de rede, sem tradução de código no idioma da consulta, sem (des) serialização de objetos;
- É permitido armazenar dados de qualquer forma - pelo menos em XML no disco, pelo menos no SQL Server, pelo menos no Armazenamento de Tabelas do Azure.
Contras:
- A escala horizontal é perdida e, como resultado, a implantação de tempo de inatividade zero não pode ser feita;
- Se o aplicativo falhar, você poderá perder parcialmente os dados. (Mas nosso aplicativo nunca falha, certo?)
Como isso funciona?
O algoritmo é o seguinte:
- No início, uma conexão com o data warehouse é estabelecida e os dados são baixados;
- Um modelo de objeto, índices primários e índices de relacionamento (1: 1, 1: Many) são criados;
- Uma assinatura é criada para alterar as propriedades dos objetos (INotifyPropertyChanged) e para adicionar ou remover elementos da coleção (INotifyCollectionChanged);
- Quando a assinatura é acionada - o objeto alterado é adicionado à fila para gravação no armazém de dados;
- Periodicamente (por timer), as alterações no armazenamento são salvas no fluxo em segundo plano;
- Quando você sai do aplicativo, as alterações no repositório também são salvas.
Exemplo de código
Adicione as dependências necessárias Descrevemos o modelo de dados que será armazenado no repositório 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; } }
Então o modelo de objeto: public class ParentModel : ModelBase { public ParentModel(ParentEntity entity) { Entity = entity; } public ParentModel() { Entity = new ParentEntity(Guid.NewGuid()); }
E, finalmente, a própria classe de repositório para acessar dados: public class MyObjectRepository : ObjectRepositoryBase { public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance) { IsReadOnly = true;
Crie uma instância do ObjectRepository:
var memory = new MemoryStream(); var db = new LiteDatabase(memory); var dbStorage = new LiteDbStorage(db); var repository = new MyObjectRepository(dbStorage); await repository.WaitForInitialize();
Se o projeto usar o HangFire public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository) { services.AddHangfire(s => s.UseHangfireStorage(objectRepository)); }
Insira um novo objeto:
var newParent = new ParentModel() repository.Add(newParent);
Nesta chamada, o objeto ParentModel é adicionado ao cache local e à fila de gravação no banco de dados. Portanto, essa operação usa O (1) e você pode trabalhar imediatamente com esse objeto.
Por exemplo, para encontrar este objeto no repositório e verifique se o objeto retornado é a mesma instância:
var parents = repository.Set<ParentModel>(); var myParent = parents.Find(newParent.Id); Assert.IsTrue(ReferenceEquals(myParent, newParent));
O que acontece com isso? O conjunto <ParentModel> () retorna um TableDictionary <ParentModel> , que contém ConcurrentDictionary <ParentModel, ParentModel> e fornece funcionalidade adicional para índices primários e secundários. Isso permite que você tenha métodos para pesquisar por ID (ou outros índices personalizados arbitrários) sem enumerar completamente todos os objetos.
Quando objetos são adicionados ao ObjectRepository , uma assinatura é adicionada para alterar suas propriedades, portanto, qualquer alteração nas propriedades também faz com que esse objeto seja adicionado à fila de gravação.
Atualizar propriedades de fora parece o mesmo que trabalhar com um objeto POCO:
myParent.Children.First().Property = "Updated value";
Você pode excluir um objeto das seguintes maneiras:
repository.Remove(myParent); repository.RemoveRange(otherParents); repository.Remove<ParentModel>(x => !x.Children.Any());
Isso também adiciona o objeto à fila de exclusão.
Como funciona a conservação?
O ObjectRepository ao alterar objetos rastreados (adicionando ou excluindo ou alterando propriedades) gera o evento ModelChanged , no qual IStorage está inscrito . As implementações do IStorage, quando ocorre um evento ModelChanged , resumem as alterações em três filas - adicionar, atualizar e excluir.
Além disso, as implementações do IStorage durante a inicialização criam um timer que a cada 5 segundos faz com que as alterações sejam salvas.
Além disso, há uma API para forçar uma chamada de salvamento: ObjectRepository.Save () .
Antes de cada salvamento, as primeiras operações sem sentido são removidas das filas (por exemplo, eventos duplicados - quando um objeto é alterado duas vezes ou a adição / remoção rápida de objetos) e somente então o salvamento em si.
Em todos os casos, o objeto inteiro é mantido, portanto, é possível que os objetos sejam salvos em uma ordem diferente da que foram alterados, incluindo versões mais recentes dos objetos do que no momento da inclusão na fila.
O que mais existe?
- Todas as bibliotecas são baseadas no .NET Standard 2.0. Pode ser usado em qualquer projeto .NET moderno.
- A API é segura para threads. As coleções internas são baseadas no ConcurrentDictionary , os manipuladores de eventos têm bloqueios ou não precisam deles.
A única coisa a lembrar é chamar ObjectRepository.Save (); - Índices personalizados (exigem exclusividade):
repository.Set<ChildModel>().AddIndex(x => x.Value); repository.Set<ChildModel>().Find(x => x.Value, "myValue");
Quem está usando?
Pessoalmente, comecei a usar essa abordagem em todos os projetos de hobby, porque é conveniente e não exige grandes despesas para gravar uma camada de acesso a dados ou implantar uma infraestrutura pesada. Pessoalmente, como regra, armazenar dados no litedb ou em um arquivo geralmente é suficiente para mim.
Mas, no passado, quando o EscapeTeams, a inicialização tardia, era feita com a equipe (eles pensavam que eram dinheiro - mas não, experimentam novamente ) - eles usavam o Armazenamento de Tabela do Azure para armazenar dados.
Planos futuros
Gostaria de corrigir uma das principais desvantagens dessa abordagem - escala horizontal. Para fazer isso, você precisa de transações distribuídas (sic!), Ou tomar uma decisão decidida de que os mesmos dados de diferentes instâncias não devem mudar ou deixá-los mudar de acordo com o princípio "quem é o último - está certo".
Do ponto de vista técnico, vejo o seguinte esquema possível:
- Armazene EventLog e Snapshot em vez do modelo de objeto
- Encontre outras instâncias (adicione pontos de extremidade de todas as instâncias? Descoberta de Udp? Mestre / escravo? Às configurações)
- Replicar entre instâncias do EventLog através de qualquer um dos algoritmos de consenso, como o RAFT.
Há também outro problema que me incomoda - é a exclusão em cascata ou a detecção de casos de exclusão de objetos referenciados de outros objetos.
Código fonte
Se você ler até aqui - somente o código ainda será lido, pode ser
encontrado no github .