2016-05-03 5 views
9

Il progetto è basato su MVC WebAPI.Qual è il modo migliore per memorizzare nella cache i risultati di una serializzazione di json.net in memoria?

Stiamo passando il contesto di autorizzazione di un client ai nostri server API come oggetto JSON serializzato nelle intestazioni delle attestazioni della richiesta. Questo non è un oggetto enorme: 6 proprietà e una raccolta di coppie chiave-valore basate su enum (fino a 6 elementi qui)

L'ampia maggioranza delle richieste di API si verificano ogni minuto (alcune più frequentemente) dallo stesso set dei clienti. Probabilmente 700-900 clienti (e in crescita), ognuno che invia le stesse affermazioni all'infinito, ogni minuto.

Per ogni richiesta, vari componenti del codice deserializzano questo oggetto probabilmente 5-6 volte. Questa deserializzazione causa un significativo drenaggio della CPU sui server.

Quale sarebbe il modo migliore per memorizzare in cache queste deserializzazioni? Un oggetto dizionario statico con chiavi con stringhe JSON serializzate, funzionerebbe bene o la ricerca sarebbe troppo lenta, in quanto queste stringhe sarebbero di dimensioni decenti?

EDIT: ogni azione di ogni controller viene filtrata attraverso questo attributo per assicurarsi che le chiamate hanno autorizzazioni appropriate

public class AccountResolveAttribute : ActionFilterAttribute 
{ 
    public override void OnActionExecuting(HttpActionContext context) 
    { 
     var controller = (ControllerBase) context.ControllerContext.Controller; 
     var identity = (ClaimsIdentity) controller.User.Identity; 

     var users = identity.Claims 
      .Where(c => c.Type == ClaimTypes.UserData.ToString()) 
      .Select(c => JsonConvert.DeserializeObject<UserInformation>(c.Value)) 
      .ToList(); 

     var accountId = controller.ReadAccountIdFromHeader(); 

     if (users.All(u => u.AccountId != accountId)) 
     { 
      throw new ApplicationException(string.Format("You have no rights for viewing of information on an account Id={0}", accountId)); 
     } 
    } 
} 

Ci sono richieste anche nel controller di base che interroga rivendicazioni, ma AccountResolve potrebbe probabilmente memorizzare nella cache il risultato della prima deserializzazione nel controller in modo che quelle chiamate non provino a deserializzare nuovamente. Tuttavia, le affermazioni sono sempre le stesse e sto solo cercando di trovare un modo per ottimizzare per non deserializzare ancora e ancora la stessa stringa. Ho provato a memorizzare la stringa di serializzazione come chiave e risultato in memoria in un ConcurrentDictionary statico globale, ma non sembra aver aiutato

+0

Si dice che vari componenti del codice deserializzano l'oggetto. Per curiosità, in che modo i componenti arrivano ai dati serializzati in primo luogo? Ad esempio: lo si ottiene dal principal del thread corrente, il principale viene passato come parametro, ecc.? –

+0

Un'altra domanda oltre a quella che ho appena chiesto: stai usando un contenitore IoC e, in tal caso, quale? –

+0

Yah, stiamo usando autofac – Igorek

risposta

1

Sembra esserci due aspetti a questa domanda:

  1. ciò che il titolo sta chiedendo
  2. Qualcosa sta mangiando fino cicli di CPU; l'ipotesi è che è a causa della deserializzazione di istanze Userinformation

per 1., sembra un ConcurrentDictionary si adatterebbe il disegno di legge supponendo che in realtà ci sono abbastanza limitate possibilità numero Userinformation (si parla questo nella questione); altrimenti non solo continueresti a prendere il costo della serializzazione, avresti essenzialmente qualcosa che assomiglia a una perdita di memoria.

Se si può tranquillamente fare l'ipotesi, ecco un esempio:

public static class ClaimsIdentityExtensions 
{ 
    private static readonly ConcurrentDictionary<string, UserInformation> CachedUserInformations = new ConcurrentDictionary<string, UserInformation>(); 
    public static IEnumerable<UserInformation> GetUserInformationClaims(this ClaimsIdentity identity) 
    { 
     return identity 
      .Claims 
      .Where(c => c.Type == ClaimTypes.UserData) 
      .Select(c => CachedUserInformations.GetOrAdd(
       c.Value, 
       JsonConvert.DeserializeObject<UserInformation>)); 
    } 
} 

Lei aveva detto che si è tentato di utilizzare ConcerrentDictionary, ma non ha aiutato. Sarei scioccato se le prestazioni di deserializzazione di un oggetto battessero una ricerca in un ConcurrentDictionary (di nuovo, facendo l'ipotesi di cui sopra), anche se le chiavi sono stringhe "lunghe". Senza esempi della classe UserInformation, è difficile sapere con certezza al 100% da parte nostra ... tuttavia, ecco un esempio che mostra che, data una UserInformation con una proprietà AccountId, l'approccio ConcurrentDictionary supera l'approccio brute-force-deserialization di un ordine di grandezza:

using System; 
using System.Collections.Concurrent; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Linq; 
using System.Security.Claims; 
using Newtonsoft.Json; 

namespace ConsoleApplication2 
{ 
    public class UserInformation 
    { 
     public int AccountId { get; set; } 
    } 

    public static class ClaimsIdentityExtensions 
    { 
     private static readonly ConcurrentDictionary<string, UserInformation> CachedUserInformations = new ConcurrentDictionary<string, UserInformation>(); 
     public static IEnumerable<UserInformation> GetUserInformationClaims(this ClaimsIdentity identity, bool withConcurrentDictionary) 
     { 
      if (withConcurrentDictionary) 
      { 
       return identity 
        .Claims 
        .Where(c => c.Type == ClaimTypes.UserData) 
        .Select(c => CachedUserInformations.GetOrAdd(
         c.Value, 
         JsonConvert.DeserializeObject<UserInformation>)); 
      } 

      return identity 
       .Claims 
       .Where(c => c.Type == ClaimTypes.UserData) 
       .Select(c => JsonConvert.DeserializeObject<UserInformation>(c.Value)); 
     } 
    } 

    class Program 
    { 
     static void Main() 
     { 
      var identity = new ClaimsIdentity(new[] 
      { 
       new Claim(ClaimTypes.UserData, "{AccountId: 1}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 2}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 3}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 4}"), 
       new Claim(ClaimTypes.UserData, "{AccountId: 5}"), 
      }); 

      const int iterations = 1000000; 
      var stopwatch = Stopwatch.StartNew(); 
      for (var i = 0; i < iterations; ++i) 
      { 
       identity.GetUserInformationClaims(withConcurrentDictionary: true).ToList(); 
      } 
      Console.WriteLine($"With ConcurrentDictionary: {stopwatch.Elapsed}"); 

      stopwatch = Stopwatch.StartNew(); 
      for (var i = 0; i < iterations; ++i) 
      { 
       identity.GetUserInformationClaims(withConcurrentDictionary: false).ToList(); 
      } 
      Console.WriteLine($"Without ConcurrentDictionary: {stopwatch.Elapsed}"); 
     } 
    } 
} 

uscita:

With ConcurrentDictionary: 00:00:00.8731377 
Without ConcurrentDictionary: 00:00:05.5883120 

un modo rapido per sapere se la deserializzazione delle istanze Userinformation è la causa dei cicli sospettosamente alti di CPU, a commentare la e spegnendo qualsiasi validazione contro UserInformation e vedere se i cicli sono ancora alti.

0

Poiché ogni GET restituisce risultati diversi, è probabile che sia necessario implementare il tuo possedere il caching, che non è terribilmente difficile. È possibile utilizzare MemoryCache o HttpRuntime.Cache per memorizzare qualsiasi dato desiderato. C'è un semplice esempio in fondo alla documentazione.

Una cache esiste per ogni processo, quindi se IIS è configurato per più di un processo di lavoro, ogni processo conserverà la propria cache.

In questo modo, è possibile conservare i dati desiderati nella cache. Quindi recuperarlo e manipolarlo comunque è necessario prima di restituire i dati al client.

È necessario implementare un tipo di blocco per assicurarsi che lo stesso elemento memorizzato nella cache non venga scritto da più thread contemporaneamente. Vedi here per alcune idee a riguardo.


Vecchia risposta:

Se ogni utente vede gli stessi dati, quindi è possibile utilizzare Strathweb.CacheOutput.WebApi2, che è disponibile in NuGet. Potrebbe adattarsi alle tue esigenze.

Memorizzerà i risultati in base all'URL inviato. Pertanto, se i dati vengono restituiti per /api/getmydata, la chiamata successiva a /api/getmydata riceverà i dati dalla cache. Hai impostato la scadenza della cache.

decorare le vostre azioni con la CacheOutputAttribute:

[CacheOutput(ServerTimeSpan = 100)] 
public List<string> GetMyData() { 
    ... 
} 

Ma se uno azione può restituire i dati diversi a seconda di chi è l'utente, allora questo non funzionerà così facilmente.

+0

Sfortunatamente le azioni GET restituiscono sempre risultati diversi e la maggior parte delle chiamate sono comunque POST. Grazie! – Igorek

+0

Probabilmente avrai bisogno di implementare qualcosa da te, in modo da poter memorizzare solo i dati che desideri. Ho aggiornato la mia risposta. –

+0

Questo è quello che stavo chiedendo riguardo a come migliorare il mio. MemoryCache esegue anche la serializzazione, quindi non sarà utile.Penso di aver bisogno di una buona strategia di ricerca delle chiavi – Igorek