Cache Sincronizado com Spring Framework

Imagine sua aplicação WPF de cotação de preço de serviços que se comunica com um servidor de aplicações usando WCF, e que tenha que carregar todos os dias uma lista de preços atualizada de um sistema legado antigo e lento. Todos os usuários geralmente abrem a aplicação no começo do expediente e a aplicação só pode começar a rodar após a atualização da lista de preços. A lista de preços é enorme e o legado demora por volta de 10 minutos para processar a lista e transmiti-la para a aplicação cliente.

Nesse cenário, a primeira otimização que deve ser feita é cachear essa lista de preços no servidor de aplicações. Idealmente esse processo de cache deve ser feito antes do expediente dos funcionários começar, logo após a atualização da lista de preços no legado. Mas devido a restrições impostas pela maneira que o legado é acessado (no exemplo em questão), esse pré-cache não pode ser feito e ele só pode ocorrer quando o primeiro usuário do sistema abre a aplicação.

Uma solução fácil para cachear essa informação é usar a infraestrutura de caching do Spring Framework. No servidor de aplicações, o serviço que acessa o legado para obter a lista de preços é gerenciada pelo Spring. Basta usar o proxy AOP com um Cache Advice para habilitar o cache (veja mais informações sobre o Cache do Spring aqui).

Porém, a implementação do cache do Spring não é sincronizada. No cenário descrito acima, significa que enquanto a primeira chamada ao legado não completar, outras mais serão executadas no legado pois o cache do Spring diz que o resultado da chamada não esta cacheado e prossegue com a execução do serviço para obter o valor.

Isso significa que se 100 usuário abrirem a aplicação em 10 minutos, que é o tempo que dura a execução de 1 chamada ao legado, serão feitas 100 chamadas ao legado, que ficará sobrecarregado e irá demorar mais de 10 minutos para executar 1 chamada. Para resolver esse problema, basta sincronizar o acesso ao cache, fazendo que apenas 1 chamada ao legado seja feita por dia.

O cache do Spring funciona como um interceptador de chamadas (Method Interceptor). Antes de executar o método, o interceptador verifica se o valor da chamada já esta armazenado no cache. Se está, o interceptador retorna esse valor e nem executa o método interceptado. Senão, prossegue com a chamada e quando o método termina sua execução, o interceptador armazena o valor retornado.

Nossa solução para o cache sincronizado consiste basicamente em um Decorator que expande o comportamento de um cache existente do Spring adicionando o sincronismo que precisamos. Quando o valor do método não esta cacheado, o decorator cria um objeto de sincronismo (implementado com ManualResetEvent) para que chamadas subsequentes para o mesmo método espere a execução da primeira chamada terminar. Quando a primeira chamada termina, o objeto de sincronismo é notificado e descartado. As outras chamadas que estava esperando são liberadas e reprocessadas.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using AopAlliance.Intercept;
using Spring.Aspects.Cache;
using Spring.Caching;

namespace SpringTutorial.Util
{
    public class ServerCache : BaseCacheAdvice, ICache
    {
        private ICache decorated;
        private Dictionary<object, ManualResetEvent> locks = new Dictionary<object, ManualResetEvent>();

        public int Timeout { get; set; }

        public ServerCache(ICache decorated)
        {
            this.decorated = decorated;
            //Default Timeout 20 minutes
            Timeout = 20 * 60 * 1000;
        }

        public void Clear()
        {
            decorated.Clear();
        }

        public int Count
        {
            get { return decorated.Count; }
        }

        public object Get(object key)
        {
            object value = decorated.Get(key);
            if (value != null)
            {
                return value;
            }
            //Not cached
            ManualResetEvent keyLock = null;
            lock (locks)
            {
                if (!locks.TryGetValue(key, out keyLock))
                {
                    locks.Add(key, new ManualResetEvent(false));
                    keyLock = null;
                }
            }
            if (keyLock != null)
            {
                keyLock.WaitOne(Timeout);
                return Get(key);
            }
            else
            {
                return decorated.Get(key);
            }
        }

        public void Insert(object key, object value, TimeSpan timeToLive)
        {
            Insert(key, value);
        }

        public void Insert(object key, object value)
        {
            decorated.Insert(key, value);
            lock (locks)
            {
                ManualResetEvent keyLock = null;
                if (locks.TryGetValue(key, out keyLock))
                {
                    locks.Remove(key);
                    keyLock.Set();
                }
            }
        }

        public ICollection Keys
        {
            get { return decorated.Keys; }
        }

        public void Remove(object key)
        {
            decorated.Remove(key);
        }

        public void RemoveAll(ICollection keys)
        {
            decorated.RemoveAll(new ArrayList(keys));
        }

        private void UnlockKey(object key)
        {
            lock (locks)
            {
                ManualResetEvent keyLock = null;
                if (locks.TryGetValue(key, out keyLock))
                {
                    keyLock.Set();
                    locks.Remove(key);
                }
            }
        }
    }
}

Complementando o Decorator acima, é necessário introduzir um comportamento adicional para o caso do método chamado lançar uma exceção. Sem esse complemento, o cache pode ficar travado caso o acesso ao legado dê algum erro. Esse complemento é outro interceptador de chamada.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using AopAlliance.Intercept;
using Spring.Aspects.Cache;
using Spring.Caching;

namespace SpringTutorial.Util
{
    public class ServerCache : BaseCacheAdvice, ICache, IMethodInterceptor
    {
        //(...)

        #region IMethodInterceptor Members
        public object Invoke(IMethodInvocation invocation)
        {
            CacheResultAttribute resultInfo = (CacheResultAttribute)GetCustomAttribute(invocation.Method, typeof(CacheResultAttribute));
            if (resultInfo != null)
            {
                IDictionary vars = PrepareVariables(invocation.Method, invocation.Arguments);
                object resultKey = resultInfo.KeyExpression.GetValue(null, vars);
                try
                {
                    return invocation.Proceed();
                }
                catch (Exception)
                {
                    //Unlock key so other thread could try to retrieve the values
                    UnlockKey(resultKey);
                    throw;
                }
            }
            else
            {
                return invocation.Proceed();
            }
        }
        #endregion
    }
}

A configuração do Spring:

<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.springframework.net http://www.springframework.net/xsd/spring-objects.xsd">

  <object id="cacheAspect" type="Spring.Aspects.Cache.CacheAspect, Spring.Aop">
  </object>

  <object id="springCache" type="Spring.Caching.NonExpiringCache, Spring.Core">
  </object>

  <object id="serverCache" type="SpringTutorial.Util.ServerCache, SpringTutorial.Util">
    <constructor-arg ref="springCache" />
  </object>

  <object id="pricebookServiceTarget" type="SpringTutorial.Core.PricebookService, SpringTutorial.Core">
  </object>

  <object id="pricebookService" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
    <property name="proxyInterfaces" value="SpringTutorial.IPricebookService"/>
    <property name="target" ref="pricebookServiceTarget"/>
    <property name="interceptorNames">
      <list>
        <value>serverCache</value>
        <value>cacheAspect</value>
      </list>
    </property>
  </object>

</objects>

Dado as restrições que temos na arquitetura atual do sistema, esse é o método menos intrusivo para resolver esse problema de performance no acesso ao legado.

Referencias:





Desenvolvido por hacklab/ com WordPress