É muito comum em nossas aplicações termos alguns “gargalos” no acesso a nossa base de dados. Normalmente, isso ocorre devido a algumas tabelas da base de dados serem muito consultadas.

Em alguns casos, uma indexação correta pode solucionar o problema, porém, em outros casos pode ser necessário inserir um novo elemento na arquitetura da nossa aplicação.

Mas antes… Um momento jabá

Além desse site, eu me aventurei em criar conteúdos em vídeos e criei o meu canal no YouTube.

Lá, fiz um compromisso de que quando batermos a marca de 100 inscritos eu realizaria a primeira live ao vivo para tirar dúvidas e bater um papo. No momento em que escrevo esse post falta muito pouco para atingirmos essa marca.

Então não deixe de conferir, se inscrever e compartilhar.

Cache para aliviar o banco de dados

Um recurso muito utilizado, é a inserção de cache na aplicação para diminuir o acesso ao banco de dados. Cache nada mais é do que armazenar dados que são acessados com mais frequência em uma unidade de acesso mais rápido, normalmente na memória ram.

Assim, antes de acessar o banco de dados, eu verifico se o dado existe no cache e caso não exista, aí sim, eu vou no banco de dados, obtenho o dado e armazeno no cache para futuras consultas.

Cache com NHibernate

No NHibernate existem 2 níveis de cache, primeiro nível (L1) e segundo nível (L2).

O cache de primeiro nível está ligado a sessão do NHibernate, ou seja, toda sessão criada pelo Session Factory possuirá um cache próprio.

A imagem abaixo ilustra o cache de primeiro nível:



Porém, em casos que eu preciso ter um cache compartilhado, o cache de primeiro nível não atende, já que uma sessão não tem acesso ao cache de outra sessão. Assim, eu preciso de um cache centralizado, onde todas as sessões possuam acesso.

Aplicações Web é um exemplo de onde o uso de um cache compartilhado pode me oferecer um melhor desempenho em relação ao acesso da minha base de dados, pois eu consigo armazenar em cache dados que possuem muito acesso em comum entre as requisições.

É aí que entra o cache de segundo nível, o cache L2 está diretamente ligado ao Session Factory, ou seja, todas as sessões compartilharão desse cache.

Segue a imagem abaixo com a inclusão do cache de segundo nível:

Implementando Cache L2 com Redis

O NHibernate possui diferentes providers para utilizar o cache L2 e todos eles são listados na documentação do NHibernate.

Nesse exemplo, eu utilizo o Redis como cache. Como banco de dados, utilizo o SQL Server e a conhecida base de dados Northwind. Todo o código fonte e um arquivo docker compose com o Redis e o banco de dados SQL Server se encontram no meu GitHub

O primeiro passo, além de ter um Redis e o Banco Northwind disponíveis para acesso, é referenciar a biblioteca para a utilização do Redis como cache:

dotnet add package NHibernate.Caches.StackExchangeRedis 

Em seguida, devemos configurar o SessionFactory da aplicação:

OBS: No exemplo,preferi utilizar o FluentNHibernate para configurar o NHibernate, no lugar dos arquivos xmls. Na documentação do NHibernate existe um exemplo utilizando xml.

            ISessionFactory sessionFactory = Fluently.Configure()
                         .Database(MsSqlConfiguration.MsSql2012.ConnectionString("Data Source=localhost;Initial Catalog=Northwind;User ID=sa;Password=Northwind0123")
                         .ShowSql().FormatSql())
                         .Cache(c => 
                            c.UseSecondLevelCache().ProviderClass<NHibernate.Caches.StackExchangeRedis.RedisCacheProvider>()
                         )
                         .Mappings(m =>
                             m.FluentMappings.AddFromAssemblyOf<EmployeesMap>()
                         )
                         .ExposeConfiguration(cfg => cfg.SetProperty("cache.configuration", "localhost:6379"))
                         .BuildSessionFactory();

Na linha 4 e 5, configuro o provider de cache (no caso o Redis) e na linha 10 configuro a connection string do Redis.

Alterando o mapeamento

Eu preciso especificar no mapeamento das minhas entidades, quais terão suporte ao cache. Para isso, é necessário incluir o elemento cache no mapeamento da classe.

 public class ProductsMap : ClassMap<Products>
    {
        public ProductsMap()
        {
            Cache.NonStrictReadWrite();
            Table("[Products]");            
            Id(x => x.ProductID, "[ProductID]").GeneratedBy.Identity();
            Map(x => x.ProductName, "[ProductName]").Not.Nullable().Length(80);
            References(x => x.Supplier)
                .ForeignKey("[Products.FK_Products_Suppliers]")
                .Columns("[SupplierID]");
            References(x => x.Category)
                .ForeignKey("[Products.FK_Products_Categories]")
                .Columns("[CategoryID]");
            Map(x => x.QuantityPerUnit, "[QuantityPerUnit]").Nullable().Length(40);
            HasMany(x => x.OrdersDetail).Table("OrderDetails").KeyColumn("ProductID");
            Map(x => x.UnitPrice, "[UnitPrice]").Nullable().Length(8).Precision(19);
            Map(x => x.UnitsInStock, "[UnitsInStock]").Nullable().Length(2).Precision(5);
            Map(x => x.UnitsOnOrder, "[UnitsOnOrder]").Nullable().Length(2).Precision(5);
            Map(x => x.ReorderLevel, "[ReorderLevel]").Nullable().Length(2).Precision(5);
            Map(x => x.Discontinued, "[Discontinued]").Not.Nullable().Length(1);
        }
    }

Na linha 5, está a configuração de cache para leitura e escrita, também é possível configurar para apenas leitura.

Hora de executar…

Agora, precisamos testar para verificar se realmente a implementação de cache me permite diminuir o acesso ao banco de dados.

Para isso, eu criei um projeto console com duas sessions e ambas irão consultar o produto com Id igual a 1. Porém, veremos no console que apenas na consulta da primeira session foi executado SQL no banco, enquanto que a consulta da segunda session foi utilizando o cache L2.

Segue abaixo o código utilizado:

            using ISession session1 = sessionFactory.OpenSession();
            Products product1 = session1.Get<Products>(1);

            System.Console.WriteLine(product1.ProductName);

            using ISession session2 = sessionFactory.OpenSession();
            Products product2 = session2.Get<Products>(1);

            System.Console.WriteLine(product2.ProductName);

Executando a aplicação, é possível ver pela a saída gerada pelo console que a primeira session executa o SQL no banco e imprime na tela o nome do produto, enquanto a segunda session consome o cache L2 e imprime novamente o nome do produto no console.

Utilizar o recurso de cache L2 pode significar um ganho significativo de desempenho nas nossas aplicações. Lembrando que todo o código fonte se encontra no meu GitHub e que na documentação do NHibernate é possível ver mais detalhes sobre cache .

E para quem quiser saber mais sobre o NHibernate, separei a live que participei no Canal DotNet:

Além da live do Gago sobre NHibernate no .NET Core: uma visão geral

Até a próxima!!!