Clique aqui para ler este artigo em pdf imagem_pdf.jpg

msdn02_capa.jpg

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.

 

 image002.jpg

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.

 

image004.jpg

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:

 

code01.jpg 

 

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.

 

image006.jpg

Figura 3. Execução da página ProdutosPorCategoria.aspx.

 

Teste, também, o comportamento da página com a seguinte diretiva:

 

code02.jpg

 

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
devem ser separados por “;”;

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

 

code03.jpg

code04.jpg

 

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:

 

code05.jpg 

 

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.

 

 

image008.jpg

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