Clique aqui para ler este artigo em pdf
Clique aqui para ler todos os artigos desta edição
ASP.NET Caching
Escalabilidade e desempenho ao seu alcance
por Miguel Ferreira
Um dos fatores críticos na construção de aplicações escaláveis e que demandam alto desempenho é a habilidade de fazer com que elas armazenem objetos, páginas ou fragmentos de páginas na memória visando ao acesso imediato. Ao armazenarmos essas entidades no servidor web, em proxies ou mesmo no navegador, acabamos com a necessidade de se recriar tais itens, uma vez que eles já estão disponíveis. O benefício imediato é a redução no processamento do servidor, utilizando a renderização e a lógica da camada de acesso a dados. Esse recurso é conhecido como caching e, no ASP.NET, fazemos uso de uma série de técnicas para 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).
Cache de Respostas (OutputCache)
O cache de respostas é a forma mais simples de usufruir do sistema de cache do ASP.NET sem precisar redesenhar ou alterar o código - o conteúdo a ser enviado ao cliente (resposta) é armazenado na 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 as requisições subseqüentes sejam servidas direto do cache, sem execução de código. Podemos, portanto, definir onde ocorrerá este caching - essa 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. A Figura 1 mostra como ocorre o funcionamento.
Figura 1. As requisições são servidas direto do cache de resposta, sem necessidade de execução de código
É necessário, também, ter controle sobre a vida útil dessa resposta no cache – para isso, é preciso especificar o 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). Esses parâmetros visam definir o comportamento das páginas que os recebem - quais dados deverão ser exibidos/armazenados, formato de exibição etc. Vale lembrar que os parâmetros são submetidos com a requisição e podem ser especificados na querystring (GET) ou por meio de um formulário (POST).
Tomemos como exemplo uma requisição feita aos servidores da NW Traders, na qual procuramos exibir os produtos de determinada categoria – veja o diagrama na Figura 2. Podemos especificar uma página que receba o parâmetro CatID, apresentando um grid descritivo dos produtos dessa categoria e seus respectivos fornecedores. Para facilitar nossa vida, o dba da NW Traders disponibilizou uma Stored Procedure (ProdutosPorCategoria – Listagem 1) que recebe exatamente esse CatID como parâmetro e retorna os produtos em questão.
Figura 2. Diagrama das tabelas
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 – Listagem 2.), percebe-se facilmente que, se pretendemos colocar essas informações no cache, é preciso que existam múltiplas versões baseadas no parâmetro CatID. Devemos instruir essas versões por meio do atributo VaryByParam. Não se esqueça de que os atributos VaryByParam e Duration são obrigatórios (veja a Tabela 1). Nossa diretiva de OutputCache, a ser adicionada à ProdutosPorCategoria.aspx, ficará assim:
Para testar a funcionalidade, basta navegar em três ou quatro categorias e depois pausar o SQL Server. Verificamos que, quando selecionamos novamente as categorias já visualizadas, as informações são servidas direto do cache de respostas. Veja o resultado na Figura 3. Se tentarmos obter informações não “cacheadas” – outros CatID(s) – será tentado um acesso à fonte de dados e uma exceção será gerada. Lembre-se também que a diretiva está definindo que essas respostas permaneçam no cache por 120 segundos.
Figura 3. Execução da página ProdutosPorCategoria.aspx.
Teste, também, o comportamento da página com a seguinte diretiva:
Tabela 1. Descrição dos atributos da diretiva de OutputCache
Duration |
Obrigatório. Tempo, em segundos, em que a página deverá existir no cache; |
Location |
Especifica onde a resposta deve ser colocada no cache. “Any” (padrão): pode estar localizada no navegador, no servidor proxy ou no servidor que processou a requisição; “Client”: O cache permanece alocado no navegador que originou a requisição; “Downstream”: pode ser “cacheado” 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 nos proxies; |
VaryByParam |
Obrigatório. Define, por meio dos parâmetros presentes nas requisições, que sejam mantidas múltiplas versões de uma mesma página 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 |
VaryByHeader |
Varia o cache de acordo com um header específico. O header é sempre enviado pelo navegador nas requisições. Podemos, por exemplo, variar a linguagem do cliente: VaryByHeader="Accept-Language"; |
VaryByCustom |
Permite especificar variações customizadas no global.asax - pode-se variar versões por Browser (padrão) ou ainda por alguma variável de sessão – nesse caso, a versão permanece associada a um cliente em particular. |
Listagem 2. ProdutosPorCategoria.aspx
Cache de Fragmento
Em alguns casos usar o cache em toda a página não é a melhor solução. Páginas onde é preciso montar um menu baseado no perfil do usuário é um exemplo típico de cache em apenas uma parte da página. Já em áreas comuns a todos, por exemplo, notícias, é preciso fazer o cachê para toda a região. Este é o cenário ideal para o uso do fragment caching.
O cache de fragmentos consiste no cache de UCs (user controls - arquivos .ascx) e suporta os mesmos atributos do cache de repostas (com exceção, por motivos óbvios, do location). Esses UCs suportam também 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), como, por exemplo, um DropDownList. 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) tenham a mesma versão em cache. Para fazer isso, definimos o atributo shared como true (o valor padrão é false).
Se Varybycontrol é especificado, o atributo VaryByParam pode ser omitido:
Finalmente, vale lembrar que podemos utilizar o cache manipulando os métodos HttpCachePolicy.SetExpires e HttpCachePolicy.SetCacheability na 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, quando requisitamos algum recurso colocado no cache de respostas, não ocorre nenhuma execução de código. Já no cache de objetos, precisaremos tomar algumas decisões em tempo de execução. Os desenvolvedores web que estiverem lendo estas linhas se lembrarão dos objetos presentes 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. Cabe lembrar que o escopo do cache do ASP.NET é o application domain, de modo que não podemos acessá-lo a partir de outras aplicações ou processos. Outro ponto relevante é que o objeto de Cache é recriado a cada reinicialização da aplicação, o que nos remete à certa similaridade com o objeto Application. A principal diferença entre esses dois reside no fato de que o objeto de Cache proporciona funcionalidades 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 que não é alterada com freqüência (uma vez por semana em média), deva ficar em cache o maior tempo possível, mas que essa relação expire tão logo sejam alteradas, removidas ou inseridas novas categorias no banco de dados. Esse cenário foi escolhido por apresentar uma das dificuldades encontradas pelo cache de objetos: não foi prevista nenhuma 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 poderão forçar a remoção desse 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 com base em alterações efetivadas em um ou mais arquivos no disco, diretórios ou mesmo em 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 determinado período, se o item for acessado, o período será 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 - Default, High, NotRemovable, BelowNormal, AboveNormal);
· CallBack: define um método de callback para notificar a aplicação de que determinado item foi removido. Em outras palavras, quando o item é removido, o método apontado é executado com o objetivo de repopular o cache.
Na Listagem 3, aperfeiçoamos o método RecuperaCategorias da página ProdutosPorCategoria.aspx de forma que ele controle a existência da relação de categorias (dataset) no cache de objetos.
Listagem 3. Método RecuperaCategorias, 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 3 vemos que no servidor web a relação de categorias é colocada em cache com uma dependência de arquivos apontando para um diretório na rede “\\servidorsql\rede$\Categorias.XML” - lembre-se de que devemos configurar o compartilhamento e as respectivas permissões[i], ou obteremos: “Failed to start monitoring changes to '\\servidorsql\rede$\'”.
Fica agora a questão: como instruir o banco de dados SQL Server a acessar esse diretório local e alterar o arquivo Categorias.xml (c:\rede\Categorias.xml)? Uma das soluções é usar uma aplicação console (Listagem 6) disparada a partir de um trigger (Listagens 4 e 5).
Listagem 4. 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 5. Gatilho InvalidaCache - Trigger criado na tabela Categories do catálogo Northwind (SQL Server) que chama a UDF da Listagem 3.
CREATE TRIGGER InvalidaCacheCat ON Categories
AFTER
INSERT,
DELETE,
UPDATE
AS
BEGIN
EXEC InvalidaCache 'Categorias'
END
Listagem 6. Aplicação console 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 = "< MSDN><SQLDEPEND rnd='" + _r.Next(1111111, 9999999).ToString() + "' /> </MSDN>"
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
Execute a aplicação console (Figura 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, no nosso caso, invalidará o item no cache do servidor web. Um teste mais realista consiste em alterar o nome de uma das categorias e observar a alteração no arquivo c:\rede\Categorias.xml. Feito isto, o servidor web invalidará a relação de categorias (Dataset) colocada em cache por meio da dependência associada ao arquivo remoto \\SERVIDORSQL\REDE$\Categorias.xml.
Figura 4. Execução da aplicação de console
CONCLUSÃO
É importante dominar o funcionamento do caching no ASP.NET, uma vez que esse recurso pode nos garantir um aumento substancial na performance da aplicação, sem muito esforço.
Nosso objetivo aqui foi apresentar o assunto aos neófitos e oferecer um artifício técnico bastante útil aos iniciados. Existem diversos outros pontos a serem discutidos sobre o ASP.NET caching, mas deixaremos essa abordagem mais completa para outra oportunidade.
[i] Para saber mais sobre como configurar a identidade do processo ASP.NET, consulte a MSDN Library:
Designing Distributed Applications with Visual Studio .NET - ASP.NET Process Identity
http://msdn.microsoft.com/library/en-us/vsent7/html/vxconapplicationidentity.asp