在内存中缓存json.net序列化结果的最佳方法是什么?

时间:2016-05-03 15:11:37

标签: c# asp.net-web-api json.net

Project是基于MVC WebAPI的。

我们将客户端的权限上下文作为请求的声明标头中的序列化JSON对象传递给我们的API服务器。这不是一个大对象:6个属性和一个基于枚举的键值对的集合(这里最多6个项目)

绝大多数API请求都是从同一组客户端每分钟(更频繁地)发生的。可能是700-900个客户(并且还在增长),每个客户每分钟都会反复发送相同的声明。

对于每个请求,代码的各个组件可能会反序列化此对象5-6次。这种反序列化会导致服务器上的CPU耗尽。

在内存中缓存这些反序列化的最佳方法是什么?具有键的静态Dictionary对象是序列化的JSON字符串,运行良好还是通过它搜索太慢,因为这些字符串的大小会相当大?

编辑: 每个控制器的每个Action都通过此属性进行过滤,以确保调用具有适当的权限

    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));
        }
    }
}

在基本控制器中也有调用询问声明的调用,但AccountResolve可能会将第一次反序列化的结果缓存到控制器中,以便这些调用不会再次尝试反序列化。但是,声明一遍又一遍,我只是想找到一种优化方法,不要反复反序列化相同的字符串。我已经尝试将序列化字符串作为键和结果对象缓存到全局静态ConcurrentDictionary的内存中,但它似乎没有帮助

2 个答案:

答案 0 :(得分:1)

这个问题似乎有两个方面:

  1. 标题是什么
  2. 有些东西正在耗尽CPU周期;假设是由于UserInformation实例的反序列化而导致的。
  3. 对于1.,似乎ConcurrentDictionary符合该法案,假设确实存在合理有限数量的UserInformation可能性(你在问题中提到这一点);否则,你不仅会继续采用序列化成本,而且基本上会有一些看起来像内存泄漏的东西。

    如果您可以安全地做出假设,这是一个例子:

    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>));
        }
    }
    

    您曾提到您尝试使用ConcerrentDictionary,但它没有帮助。如果反序列化对象的性能击败ConcurrentDictionary中的查找(再次,做出上述假设),即使键是“长”字符串,我也会感到震惊。如果没有UserInformation类的示例,很难从我们的结果中100%确定...但是,这里有一个示例,表明给定具有AccountId属性的UserInformation,ConcurrentDictionary方法通过以下方式击败了强制 - 反序列化方法一个数量级:

    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}");
            }
        }
    }
    

    输出:

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

    一种快速的方法可以知道UserInformation实例的反序列化是否是造成可疑的高CPU周期的原因,请尝试注释掉并根据UserInformation删除任何验证并查看周期是否仍然很高。

答案 1 :(得分:0)

由于每个GET都会返回不同的结果,因此您可能需要实现自己的缓存,这并不是非常困难。您可以使用MemoryCacheHttpRuntime.Cache来存储您想要的任何数据。文档底部有一个简单的例子。

每个进程都有一个缓存,因此如果为多个工作进程配置了IIS,则每个进程都将拥有自己的缓存。

但是这样,你可以在缓存中保存你想要的任何数据。然后在将数据返回给客户端之前检索它并操作它。

您只需要实现某种锁定,以确保多个线程不会同时写入相同的缓存项。有关这方面的一些想法,请参阅here

旧答案:

如果每个用户都看到相同的数据,那么您可以使用NuGet中提供的Strathweb.CacheOutput.WebApi2。它可能符合您的需求。

它将根据发送的URL缓存结果。因此,如果为/api/getmydata返回数据,则对/api/getmydata的下一次调用将从缓存中获取数据。您设置缓存过期。

使用CacheOutputAttribute:

装饰您的操作
[CacheOutput(ServerTimeSpan = 100)]
public List<string> GetMyData() {
    ...
}

但是,如果一个动作可以根据用户的身份返回不同的数据,那么这将不会那么容易。