ASP.NET Caching - Escalabilidade e desempenho à mão
TOC
Recolher sumário
Expandir sumário

ASP.NET Caching - Escalabilidade e desempenho à mão

por Miguel Ferreira - miguel.ferreira@dotnetraptors.com.br

Um dos fatores críticos na construção de aplicações escaláveis e que demandam alto desempenho é a habilidade de se armazenar objetos, páginas ou fragmentos de páginas na memória visando acesso imediato. Ao armazenarmos estas entidades no servidor web, proxies ou mesmo no navegador removemos a necessidade de se recriar tais itens uma vez que estes já estão disponíveis. O benefício imediato é a redução no processamento do servidor envolvendo a renderização e lógica da camada de acesso à dados. Este recurso é conhecido como caching, e no ASP.NET fazemos uso de uma série de técnicas para se armazenar páginas ou objetos entre as requisições.

Podemos dividir o caching em três tipos básicos: cache de respostas (OutputCaching), cache de fragmento (Fragment Caching) e cache de objetos (Object Cache);

Nesta página

Cache de Respostas (OutputCache)
Cache de Fragmento
Cache de Objetos
Conclusão

Cache de Respostas (OutputCache)

O cache de respostas é a forma mais simples de se tirar proveito do sistema de cache do ASP.NET, sem necessidade de redesenho ou alterações de código - o conteúdo a ser enviado ao cliente (resposta) é armazenado em memória e disponibilizado para as próximas requisições. De fato, todo o conteúdo dinâmico pode ser armazenado em mecanismos que suportem o HTTP 1.1 (servidor web, navegadores e proxies) de forma que requisições subsequentes sejam servidas direto do cache sem execução de código. Podemos, portanto, a definir onde ocorrerá este caching - esta definição é feita por meio do atributo Location, na diretiva de OutputCache, ou ainda, por meio do método HttpCachePolicy.SetCacheability dentro do código.

Cc517982.aspnetcache1(pt-br,MSDN.10).gif

Figura 1. As requisições são servidas direto do cache de resposta, sem execução de código

É necessário, também, ter controle sobre a duração da vida desta resposta no cache – deve-se especificar este tempo de expiração na própria página (.aspx) por meio do atributo Duration na diretiva de OutputCache.

Praticamente todas as aplicações web possuem páginas dinâmicas que recebem parâmetros transmitidos por meio do protocolo http (get ou post). Vale lembrar que os parâmetros são submetidos c/ a requisição e podem ser especificados na querystring (GET) ou por meio de POST. Estes parâmetros visam definir o comportamento das páginas que os recebem - quais dados deverão ser exibidos/armazenados, formato de exibição, etc...

Tomemos como exemplo uma requisição feita aos servidores da NW Traders, onde procuramos exibir os produtos de uma determinada categoria - ver diagrama ao lado. Podemos para tal especificar uma página que recebe o parâmetro CatID, apresentando um grid descritivo dos produtos desta categoria e seus respectivos fornecedores. Para facilitar nossa vida o dba da NW Traders disponibilizou uma Stored Procedure (ProdutosPorCategoria – listagem 1.) que recebe exatamente este CatID como parâmetro e retorna os produtos em questão.

Cc517982.aspnetcache2(pt-br,MSDN.10).gif

Listagem 1. Stored Procedure - EstoquePorCategoria

CREATE PROCEDURE dbo.ProdutosPorCategoria 
@CatID int
AS
SELECT     Products.ProductName, Products.ProductID, Products.UnitPrice, Products.QuantityPerUnit,
 Suppliers.CompanyName, Suppliers.City
FROM         Categories INNER JOIN
                      Products ON Categories.CategoryID = Products.CategoryID INNER JOIN
                      Suppliers ON Products.SupplierID = Suppliers.SupplierID
WHERE     (Categories.CategoryID = @CatID)
RETURN

Apesar de estarmos falando da mesma página (ProdutosPorCategoria.aspx), percebe-se facilmente que, se pretendemos colocar esta informação em cache, deverão existir múltiplas versões baseadas no parâmetro CatID. Devemos instruir estas versões por meio do atributo VaryByParam. Vale lembrar que os atributos VaryByParam e Duration são obrigatórios (ver Tabela 1. abaixo).

Nossa diretiva de OutputCache, a ser adicionada na ProdutosPorCategoria.aspx fica assim:

<%@ OutputCache Duration="120" VaryByParam="CatID" %>

Para testar a funcionalidade, basta navegar em três ou quatro categorias e depois pausar o SQL Server. Verificamos que, ao selecionarmos novamente as categorias já visualizadas, a informação será servida direto do cache de respostas. Se tentarmos obter informações não “cacheadas” – outros CatID(s), será tentado um acesso à fonte de dados e uma exceção será erguida. Lembre-se, ainda, que a diretiva está definindo que estas respostas devem permanecer no cache por 120 segundos.

Cc517982.aspnetcache3(pt-br,MSDN.10).gif

Sugerimos que seja testado o comportamento com:

<%@ OutputCache Duration="120" VaryByParam="none" %>

Duration

Obrigatório. Tempo, em segundos, especificando o período de existência da página no cache;

Location

Especifica onde a resposta deve ser colocada em cache. “Any” (padrão): pode estar localizada no navegador, no servidor proxy ou servidor que processou a requisição; “Client”: O cache fica alocado no navegador que originou a requisição; “Downstream”: pode ser “caheado” em qualquer dispositivo que suporte o http 1.1, exceto o servidor web; “None”: desabilitado; “Server”: cache de respostas alocado no servidor web; “ServerAndClient”: Nega a permanência do cache de resposta em proxies;

VaryByParam

brigatório. Define, por meio dos parâmetros presentes nas requisições, que múltiplas versões de uma mesma página devem ser mantidas no cache. “none” define uma única versão e “*” especifica diferentes versões para cada parâmetro ou combinação de parâmetros – os parâmetros devem ser separados por “;”;

VaryByHeader

Varia o cache baseando-se em um header específico. O header sempre é enviado pelo navegador nas requisições. Podemos, por exemplo, variar pela linguagem do cliente: VaryByHeader="Accept-Language";

VaryByCustom

Permite que variações customizadas sejam especificadas no global.asax - pode-se variar versões por "Browser" (padrão) ou ainda alguma variável de sessão – neste caso a versão fica associada a um determinado cliente.

Tabela 1. Descrição dos atributos da diretiva de OutputCache

Cache de Fragmento

Existem situações onde o “cacheamento” de toda uma página de resposta não é adequado. Um caso óbvio, seria àquele no qual o desenho da aplicação requerer que algumas partes/seções de uma página específica tenham versões específicas para um determinado grupo de usuários (e.g. menu baseado no perfil do usuário) e que outras partes sejam comuns à todos (e.g. conteúdo genérico c/ notícias gerais). Este é o cenário ideal para o uso do fragment caching.

O cache de fragmentos refere-se ao cache de UCs (user controls - arquivos .ascx) e suporta os mesmos atributos do cache de repostas (com exceção, por motivos óbvios, do location). Estes UCs suportam, ainda, o atributo VaryByControl que define a existência de múltiplas versões, no cache, do mesmo user control, baseadas nos valores assumidos pelos controles membros (embutidos no UC) - um DropDownList por exemplo. Por padrão cada user control é “cacheado” para cada página em separado, mas podemos definir, se for o caso, que todas as páginas hospedeiras (que embutem tais controles) terão a mesma versão em cache. Fazemos isto por meio do atributo shared definido como true (o valor padrão é false).

Se Varybycontrol é especificado o atributo VaryByParam pode ser omitido:

<%@ OutputCache Duration="120" VaryByControl="CatID" Shared="true" %>

Finalmente, vale lembrar que podemos utilizar o cache de resposta manipulando os métodos HttpCachePolicy.SetExpires e HttpCachePolicy.SetCacheability por meio da propriedade HttpResponse.Cache. O código a seguir, configura um tempo de expiração de 20 segundos:

Response.Cache.SetExpires(DateTime.Now.AddSeconds(20));

Cache de Objetos

Como podemos perceber na Figura 1., ao requisitarmos algum recurso colocado no cache de respostas, não ocorre execução de código, já no cache de objetos tomaremos algumas decisões em tempo de execução. Todo desenvolvedor web que estiver lendo estas linhas se lembrará de objetos sempre presente no nosso dia a dia: Session e Application. O ASP.NET nos apresenta um novo objeto para acesso a pares chave/valor - o objeto Cache. Vale lembrar que o escopo do cache do ASP.NET é o application domain de forma que não podemos acessa-lo a partir de outras aplicações ou processos. Outro ponto relevante é que o objeto de Cache é recriado a cada reinício da aplicação, o que nos lembra uma certa similaridade com o objeto Application. A principal diferença entre estes os dois reside no fato de que o objeto de Cache proporciona funcionalidade específicas, tais como dependências e políticas de expiração.

Suponhamos que o arquiteto da aplicação NW Traders especifique que a relação de categorias dos produtos comercializados pela empresa, que é relativamente pequena e não é alterada com frequência – uma vez por semana em média, deva ficar em cache o maior tempo possível, mas que, no entanto, esta relação expire imediatamente ao serem alteradas, removidas ou inseridas novas categorias no banco de dados. Este cenário foi escolhido por apresentar uma das dificuldades encontrada pelo cache de objetos: não está prevista uma forma de se estabelecer uma dependência direta com o banco de dados. Podemos, no entanto, forçar uma dependência por meio de artifícios técnicos.

Quando adicionamos um item ao cache de objetos, definimos algumas dependências que vão forçar a remoção deste item em determinadas circunstâncias. Vejamos a assinatura do método Insert:

public void Insert(string, object, CacheDependency, DateTime, TimeSpan, 
CacheItemPriority, CacheItemRemovedCallback);

Podemos, por meio do construtor, definir alguns comportamentos interessantes:

  • Dependências: invalida o item específico tendo em vista alterações efetivadas em um ou mais arquivos no disco, diretórios ou mesmo outros objetos no cache – expiração em cascata (tipo CacheDependency);

  • Expiração absoluta: momento no qual o item expira e é removido do cache (tipo DateTime);

  • Expiração deslizante: remove o item do cache se este não for acessado dentro de um determinado período, se for acessado o período é renovado (tipo TimeSpan);

  • Prioridade: quando a aplicação necessita de mais memória, os itens são removidos do cache para liberar recursos (processo conhecido como scavenging). Podemos, por meio da prioridade, definir quais itens serão removidos primeiro (Low, Normal - Defautl, High, NotRemovable, BelowNormal, AboveNormal);

  • CallBack: define um método de callback para notificar a aplicação que um determinado item foi removido. De forma simples, quando o item é removido o método apontado é executado para, por exemplo, repopular o cache;

Especificamos um método (RecuperaCategorias) que controlará a existência da relação de categorias no cache de objetos.

Listagem 2. Método RecuperaCategorias (C#) que define a expiração, no cache, da relação de categorias;

private DataSet RecuperaCategorias()
{
	DataSet ds = new DataSet();
	if (Cache["dsCategorias"]==null)
	{
		SqlConnection cn = new SqlConnection("data source=servidorsql;
		integrated security=true;initial catalog=northwind");
		SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM [Categories]",cn);
		da.SelectCommand.CommandType = CommandType.Text;
		da.Fill(ds, "Categorias");
		System.Web.Caching.CacheDependency DepArq = new System.Web.Caching.CacheDependency
		(@"\\servidorsql\rede$\Categorias.XML");
		Cache.Insert("dsCategorias", ds, DepArq,  System.Web.Caching.Cache
		.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, 
		System.Web.Caching.CacheItemPriority.Normal, null);
	}
	else
	{
		ds = (DataSet)(Cache["dsCategorias"]);
	}
	return ds;
}

Na Listagem 2. vemos que, no sevidor web, a relação de categorias é colocada em cache com uma dependência de arquivos apontando para um diretório na rede (no SQL Server) “\\SERVIDORSQL\REDE$\Categorias.XML” - lembre-se que devemos configurar o compartilhamento e as respectivas permissões , ou obteremos: “Failed to start monitoring changes to '\\servidorsql\rede$\'”.

Ok, mas como instruir o Banco de dados a acessar este diretório local e alterar o arquivo Categorias.xml (c:\rede\Categorias.xml)? Por meio de um aplicação console disparada a partir de um trigger (chamamos este procedimento de artifício técnico).

Listagem 3. Função InvalidaCache – UDF (User Defined Function) criada no catálogo Northwind (SQL Server);

CREATE FUNCTION InvalidaCache (@TableName VarChar(25))
RETURNS INT
AS
BEGIN
	DECLARE @CMDS VarChar(100)
	SET @CMDS = 'c:\rede\SQLDependency.exe ' + @TableName
	EXEC Master..xp_cmdshell @CMDS
	RETURN 0
END

Listagem 3. Gatilho InvalidaCache - Trigger criado na tabela Categories do catálogo Northwind (SQL Server);

CREATE TRIGGER InvalidaCacheCat ON Categories
AFTER
	INSERT,
	DELETE, 
	UPDATE 
AS 
BEGIN 
	EXEC InvalidaCache 'Categorias' 
END

Listagem 4. Aplicação console (VB) que irá alterar o arquivo local (deve estar no diretório “c:\rede\” - SQL Server)

Imports System
Imports System.IO

Namespace SQLDependency
    Module Invalidate
        Dim path As String = "c:\rede\"
        Sub Main(ByVal CmdArgs() As String)
            Try
                Dim _table As String = CmdArgs(0).ToString()
                Dim _r As New Random(Date.Now.Millisecond)
                Dim _value As String = ""
                path = path & _table & ".xml"
                Dim _SWriter As New StreamWriter(New FileStream(path, FileMode.Open, 
                FileAccess.Write))
                _SWriter.Write(_value)
                _SWriter.Close()
            Catch ex As Exception
                Throw ex
                'no futuro, quem sabe.....
            End Try
        End Sub
    End Module
End Namespace

Para fins de teste executamos, no servidor de dados, a aplicação console (Listagem 4.) passando como parâmetro o nome da tabela supostamente alterada: Categories. A aplicação irá alterar o arquivo com o nome da tabela no disco, o que, em nosso caso, vai invalidar o item no cache do servidor web. Um teste mais realista pode ser feito alterando-se o nome de uma das categorias e percebendo que o arquivo c:\rede\Categorias.xml é alterado. Feito isto, o servidor web irá invalidar a relação de categorias (Dataset) colocada em cache por meio da dependência associada ao arquivo remoto \\SERVIDORSQL\REDE$\Categorias.xml.

Cc517982.aspnetcache4(pt-br,MSDN.10).gif

Conclusão

É importante dominar o funcionamento do caching no ASP.NET uma vez que este recurso pode nos garantir um aumento substancial na performance da aplicação com quase nenhum esforço.

Nosso objetivo aqui foi o de introduzir o assunto aos neófitos e apresentar um artifício técnico bastante útil para os iniciados. Existem diversos outros pontos a serem discutidos sobre o ASP.NET caching, mas deixaremos uma abordagem mais completa para outra oportunidade.

Mostrar:
© 2016 Microsoft