【问题标题】:What's the best way to cache results of a json.net serialization in memory?在内存中缓存 json.net 序列化结果的最佳方法是什么?
【发布时间】:2016-05-03 15:11:36
【问题描述】:

项目基于 MVC WebAPI。

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

对 API 的绝大多数请求每分钟(有些更频繁)都来自同一组客户端。可能有 700-900 个客户(并且还在不断增加),每个客户每分钟一遍又一遍地发送相同的声明。

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

在内存中缓存这些反序列化的最佳方法是什么?带有序列化 JSON 字符串的键的静态 Dictionary 对象是否可以正常工作,或者通过它进行搜索会太慢,因为这些字符串的大小会相当大?

编辑: 每个控制器的每个 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 中的内存中,但它似乎没有帮助

【问题讨论】:

  • 你说代码的各个组件反序列化对象。出于好奇,组件首先是如何获取序列化数据的?例如:是否从当前线程的主体获取它,主体是否作为参数传递等等?
  • 除了我刚才问的问题之外的另一个问题:您使用的是 IoC 容器吗?如果是,是哪一个?
  • 是的,我们正在使用 autofac
  • Principal 已针对控制器操作进行验证(它是基本 controllrr 中的一个属性)。在基本控制器的某些地方也使用它来获取默认上下文
  • 您提到的组件如何获取序列化数据?

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


【解决方案1】:

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

  1. 标题在问什么
  2. 有东西占用了 CPU 周期;假设是由于 UserInformation 实例的反序列化

对于 1.,假设确实存在合理有限数量的 UserInformation 可能性,似乎 ConcurrentDictionary 符合要求(您在问题中提到了这一点);否则,您不仅会继续承担序列化成本,而且本质上会出现看起来像内存泄漏的东西。

如果你可以安全地做出假设,这里有一个例子:

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 的任何验证,然后查看周期是否仍然很高。

【讨论】:

    【解决方案2】:

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

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

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

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


    旧答案:

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

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

    您使用 CacheOutputAttribute 装饰您的操作:

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

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

    【讨论】:

    • 不幸的是,GET 操作总是返回不同的结果,而且大多数调用都是 POST。谢谢!
    • 你可能需要自己实现一些东西,所以你可以只缓存你想要的数据。我更新了我的答案。
    • 这就是我要问的,关于如何最好地推出自己的产品。 MemoryCache 也执行序列化,所以不会有用。我想我需要一个好的键查找策略
    • 根据this answerHttpRuntime.Cache 不会序列化任何东西。所以这可能是一个选择。
    • 对了,你从哪里读到 MemoryCache 执行序列化的?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-07-24
    • 2011-01-18
    • 2012-07-31
    • 2011-01-10
    • 1970-01-01
    • 1970-01-01
    • 2015-11-19
    相关资源
    最近更新 更多