Usando leitura e gravação em cache distribuído

Autor: Iqbal Khan

Com a explosão de aplicativos da Web de transações extremamente altas, SOA, computação em grade e outros aplicativos de servidor, o armazenamento de dados não consegue acompanhar. O motivo é que o armazenamento de dados não pode continuar adicionando mais servidores para expandir, ao contrário das arquiteturas de aplicativos extremamente escaláveis.

Nessas situações, o cache distribuído na memória oferece uma excelente solução para gargalos de armazenamento de dados. Ele abrange vários servidores (chamados de cluster) para agrupar sua memória e manter todo o cache sincronizado entre os servidores. E ele pode continuar aumentando esse cluster de cache indefinidamente, assim como os servidores de aplicativos. Isso reduz a pressão no armazenamento de dados para que não seja mais um gargalo de escalabilidade.

Existem duas maneiras principais pelas quais as pessoas usam um cache distribuído:

  • Cache à parte: É aqui que o aplicativo é responsável por ler e gravar no banco de dados e o cache não interage com o banco de dados. O cache é "mantido de lado" como um armazenamento de dados na memória mais rápido e escalável. O aplicativo verifica o cache antes de ler qualquer coisa do banco de dados. E o aplicativo atualiza o cache depois de fazer qualquer atualização no banco de dados. Dessa forma, o aplicativo garante que o cache é mantido sincronizado com o banco de dados.
  • Leitura/Gravação (RT/WT): É aqui que o aplicativo trata o cache como o principal armazenamento de dados e lê dados dele e grava dados nele. A cache é responsável pela leitura e escrita destes dados na base de dados, aliviando assim a aplicação desta responsabilidade.
Arquitetura de cache de leitura/gravação
Figura 1: Arquitetura de cache de leitura/gravação

Benefícios do Read-through e Write-through em relação ao Cache-aside

Cache-aside é uma técnica muito poderosa e permite que você emita consultas de banco de dados complexas envolvendo junções e consultas aninhadas e manipule dados da maneira que desejar. Apesar disso, Leia / Gravação tem várias vantagens sobre o cache-aside, conforme mencionado abaixo:

  • Simplifique o código do aplicativo: Na abordagem do cache-aside, o código do aplicativo continua a ter complexidade e dependência direta do banco de dados e até duplicação de código se vários aplicativos estiverem lidando com os mesmos dados. A leitura/gravação move parte do código de acesso a dados de seus aplicativos para a camada de armazenamento em cache. Isso simplifica drasticamente seus aplicativos e abstrai o banco de dados ainda mais claramente.
  • Melhor escalabilidade de leitura com Read-through: Existem muitas situações em que um cache-item expira e vários threads de usuário paralelos acabam atingindo o banco de dados. Multiplicando isso com milhões de itens armazenados em cache e milhares de solicitações paralelas de usuários, a carga no banco de dados se torna visivelmente maior. Mas o Read-through mantém o item de cache no cache enquanto busca a cópia mais recente dele no banco de dados. Em seguida, ele atualiza o item de cache. O resultado é que o aplicativo nunca vai ao banco de dados para esses itens de cache e a carga do banco de dados é mantida no mínimo.
  • Melhor desempenho de gravação com Write-behind: No cache-aside, o aplicativo atualiza o banco de dados diretamente de forma síncrona. Considerando que, um Escrever- atrás permite que seu aplicativo atualize rapidamente o cache e retorne. Em seguida, ele permite que o cache atualize o banco de dados em segundo plano.
  • Melhor escalabilidade do banco de dados com Write-behind: Com Write-behind, você pode especificar limites de limitação para que as gravações do banco de dados não sejam executadas tão rápido quanto as atualizações do cache e, portanto, a pressão no banco de dados não seja muito. Além disso, você pode agendar as gravações do banco de dados para ocorrer fora do horário de pico, novamente para minimizar a pressão.
  • Atualizar automaticamente o cache na expiração: Leia permite que o cache recarregue automaticamente um objeto do banco de dados quando ele expirar. Isso significa que seu aplicativo não precisa acessar o banco de dados em horários de pico porque os dados mais recentes estão sempre no cache.
  • Atualizar automaticamente o cache nas alterações do banco de dados: A leitura permite que o cache recarregue automaticamente um objeto do banco de dados quando seus dados correspondentes forem alterados no banco de dados. Isso significa que o cache está sempre atualizado e seu aplicativo não precisa acessar o banco de dados nos horários de pico porque os dados mais recentes estão sempre no cache.

A leitura/gravação não se destina a ser usada para todos os acessos a dados em seu aplicativo. Ele é mais adequado para situações em que você está lendo linhas individuais do banco de dados ou lendo dados que podem mapear diretamente para um item de cache individual. Também é ideal para dados de referência que devem ser mantidos no cache para leituras frequentes, mesmo que esses dados sejam alterados periodicamente.

Desenvolvendo um manipulador de leitura

Um manipulador de leitura é registrado com o servidor de cache e permite que o cache leia dados diretamente do banco de dados. O NCache server fornece uma interface de manipulador de leitura que você precisa implementar. Isso permite NCache para chamar seu manipulador de leitura.

public class SqlReadThruProvider : IReadThruProvider
    {
        private SqlConnection _connection;
        
        // Called upon startup to initialize connection
        public void Init(IDictionary parameters, string cacheId)
        {
            _connection = new SqlConnection(parameters["connstring"]);
            _connection.Open();
        }
        
        // Called at the end to close connection
        public void Dispose()
        {
            _connection.Close();
        }
        
        // Responsible for loading object from external data source
        public ProviderCacheItem LoadFromSource(string key)
        {
            string sql = "SELECT * FROM Customers WHERE ";
            sql += "CustomerID = @ID";
            
            SqlCommand cmd = new SqlCommand(sql, _connection);
            cmd.Parameters.Add("@ID", System.Data.SqlDbType.VarChar);
            
            // Let's extract actual customerID from "key"
            int keyFormatLen = "Customers:CustomerID:".Length;
            string custId = key.Substring(keyFormatLen,
            key.Length - keyFormatLen);
            
            cmd.Parameters["@ID"].Value = custId;
            
            // fetch the row in the table
            SqlDataReader reader = cmd.ExecuteReader();
            
            Customers customer = new Customers();
            // copy data from "reader" to "cust" object
            FillCustomers(reader, customer);
            
            ProviderCacheItem cacheItem = new ProviderCacheItem(customer);
            
            // specify a SqlCacheDependency for this object
            CacheDependency dep = new SqlCacheDependency(_connection.ToString(), cmd.ToString());
            cacheItem.Dependency = dep;
            
            return cacheItem;
        }

Init() executa certas tarefas de alocação de recursos, como estabelecer conexões com a fonte de dados principal, enquanto Dispose() destina-se a redefinir todas essas alocações. LoadFromSource() é o que o cache chama para ler os objetos.

Desenvolvendo um manipulador Write-Through

O manipulador de gravação é chamado quando o cache precisa gravar no banco de dados à medida que o cache é atualizado. Normalmente, o aplicativo emite uma atualização no cache por meio de adicionar, inserir ou remover.

public class SqlWriteThruProvider : IWriteThruProvider
    {
        private SqlConnection _connection;
        
        // Called upon startup to initialize connection
        public void Init(IDictionary parameters, string cacheId)
        {
            _connection = new SqlConnection((string)parameters["connstring"]);
            _connection.Open();
        }
        
        // Called at the end to close connection
        public void Dispose()
        {
            _connection.Close();
        }
        
    public OperationResult WriteToDataSource(WriteOperation operation)
    {
        int rowsChanged = 0;
        OperationResult result = new OperationResult(operation, OperationResult.Status.Failure);
        ProviderCacheItem cacheItem = operation.ProviderItem;
        Customers cust = cacheItem.GetValue<Customers>();
        string[] customer = {cust.Id,cust.ContactName,cust.CompanyName,
        cust.Address,cust.City, cust.Country,cust.PostalCode,
		cust.Phone,cust.Fax};
            
        SqlCommand cmd = _connection.CreateCommand();
        cmd.CommandText = String.Format(CultureInfo.InvariantCulture,
            "Update dbo.Customers " + "Set CustomerID='{0}'," +
            "ContactName='{1}',CompanyName='{2}'," +
            "Address='{3}',City='{4}'," +
            "Country='{5}',PostalCode='{6}'," +
            "Phone='{7}',Fax='{8}'" +
            "Where CustomerID = '{0}'", customer);
                
        rowsChanged = cmd.ExecuteNonQuery();
            if (rowsChanged > 0)
            {
                result.OperationStatus = OperationResult.Status.Success;
                return result;
            }
            return result;
        }
}

Init() executa tarefas de alocação de recursos, como estabelecer conexões com a fonte de dados, enquanto Dispose() destina-se a redefinir todas essas alocações. Save é o método que o cache chama para objetos write-through.

Chamando Read-Through e Write-Through do Aplicativo

O código de exemplo a seguir mostra o uso dos recursos de leitura/gravação do cache de um aplicativo Windows simples

using Alachisoft.NCache.Client;
...

internal class MainForm : System.Windows.Forms.Form
{
    /// Fetches record from the cache, which internally accesses the
    /// datasource using read-thru provider
    private void OnClickFind(object sender, System.EventArgs e)
    {
        Customer customer;
        Cache cache = NCache.Caches[CacheName];
        string key = cboCustomerID.Text.Trim();
        string providerName = cboReadThruProvider.Text;
        
        customer = (Customer) cache.Get(key,
                    providerName,
                    DSReadOption.ReadThru);
                    
                    ...
    }
    
    /// Updates the record using the cache, which internally accesses
    /// the datasource using write-thru provider
    private void OnClickUpdate(object sender, System.EventArgs e)
    {
        Cache cache = NCache.Caches[CacheName];
        Customer customer = new Customer();
        
        ...
        
        string key = customer.CustomerID;
        string providerName = cboWriteThruProvider.Text;
        
        cache.Insert(key, new CacheItem(customer), DSWriteOption.WriteThru, providerName, null);
        
        ...
    }
}

O que fazer a seguir?

© Copyright Alachisoft 2002 - . Todos os direitos reservados. NCache é uma marca registrada da Diyatech Corp.