适用于长时间运行的流程的Web服务缓存设计理念

时间:2019-01-16 18:15:35

标签: c# .net web-services caching

我正在寻找一些设计思路

我有一个网站使用的ASP.Net Web服务。其中一项调用大约需要13秒才能检索到大约70000行。由于每条记录上都有一个处理,因此在数据库上需要4秒钟,而在Web服务器上需要9秒钟来处理。据我所知,此功能已进行了优化,而最初的42秒已将其降低。

数据不会经常更改,因此我的想法是在Web服务上创建缓存,并按计时器轮询以每30秒左右更新该缓存。然后,Webservice调用从缓存中检索已处理的记录

我正在寻找设计思想,以寻求最佳方法。我知道ASP.Net具有输入缓存字典,但是那不能解决轮询问题,因此我也不需要单例,那么就有可能出现线程问题。

感到非常困惑,不确定Im是否在正确的行上,或者我是否应该计算数据并将其存储在DB表中,因此不胜感激

更新

作为对某些评论的反馈。该网站旨在与客户站点上的ERP Dynamics AX进行交互,因此,即使我对数据库层有一定控制权,但它还是有限的(我可以添加一些Select SP和一些索引,但是更改的触发器和通知者可能不是否)

Dynamics AX的最新升级是在Azure中进行的,无法访问数据库层,因此我可能也必须将Azure服务器托管在Azure中。如果是这种情况,并且由于我需要支持所有版本,则似乎只能使用Redis或另一个NoSQL DB,或者将结果写入我自己的DB表并从那里调用。肯定是Azure的情况吗?

5 个答案:

答案 0 :(得分:0)

您可以使用Redis缓存数据,如果数据发生更改,则可以使用sql依赖项更新缓存。我认为您只需要redis和sql依赖项即可。

答案 1 :(得分:0)

如果要实现您的方案,我将不希望使用轮询,因为重复调用服务和保持服务/网络繁忙的意义不大。此外,如果要实现新的客户端,则还必须再次实现轮询。

请在静态类中使用基于字典的缓存。您使用现有的库,例如CacheManager。通常的想法是使用用于进行服务调用的参数来创建密钥。然后,将处理后获得的结果存储在ConcurrentDictionary中,该{@ 1}负责多个线程本身的访问。

仅当基础数据库表(?)已更新或过于复杂时,才每30秒清除一次存储结果。

此外,您还可以在数据访问层上实现类似的缓存机制,以减少当前的4秒钟。在基础数据更改(添加,更新,删除,插入操作)之后刷新缓存的数据!

答案 2 :(得分:0)

我们在ASP.NET中实现了一种轮询模式,该模式可能适用于您的用例。

在我们的Global.ashx中,我们有:

protected void Application_Start(object sender, EventArgs e)
{
  ConfigurationMonitor.Start();
}

其中ConfiguraitonMonitor看起来像这样:

public static class ConfigurationMonitor
{
    private static readonly Timer timer = new Timer(PollingInterval);
    public static bool MonitoringEnabled
    {
        get
        {
            return ((timer.Enabled || Working) ? true : false);
        }
    }
    private static int _PollingInterval;
    public static int PollingInterval
    {
        get
        {
            if (_PollingInterval == 0)
            {
                _PollingInterval = (Properties.Settings.Default.ConfigurationPollingIntervalMS > 0) ? Properties.Settings.Default.ConfigurationPollingIntervalMS : 5000;
            }
            return (_PollingInterval);

        }
        set { _PollingInterval = value; }
    }


    private static bool _Working = false;
    public static bool Working
    {
        get { return (_Working); }
    }
    public static void Start()
    {
        Start(PollingInterval);
    }

    /// <summary>
    /// Scans each DLL in a folder, building a list of the ConfigurationMonitor methods to call.
    /// </summary>
    private static List<ConfigurationMonitorAttribute> _MonitorMethods;
    private static List<ConfigurationMonitorAttribute> MonitorMethods
    {
        get
        {
            if (_MonitorMethods == null)
            {
                _MonitorMethods = new List<ConfigurationMonitorAttribute>();
                MonitorMethodsMessage = string.Empty;
                foreach (var assemblyFile in Directory.GetFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin"), Properties.Settings.Default.ConfigurtionMonitorDLLPath))
                {
                    var assembly = Assembly.LoadFrom(assemblyFile);
                    foreach (ConfigurationMonitorAttribute monitor in assembly.GetCustomAttributes(typeof(ConfigurationMonitorAttribute), inherit: false))
                    {
                        _MonitorMethods.Add(monitor);
                    }
                }
            }
            return (_MonitorMethods);
        }
    }

    /// <summary>
    /// Resets and instanciates MonitorMethods property to refresh dlls being monitored
    /// </summary>
    public static void LoadMonitoringMethods()
    {
        _MonitorMethods = null;
        List<ConfigurationMonitorAttribute> monitorMethods = MonitorMethods;
    }

    /// <summary>
    /// Initiates a timer to monitor for configuration changes.
    /// This method is invoke on web application startup.
    /// </summary>
    /// <param name="pollingIntervalMS"></param>
    public static void Start(int pollingIntervalMS)
    {
        if (Properties.Settings.Default.ConfigurationMonitoring)
        {
            if (!timer.Enabled)
            {
                LoadMonitoringMethods();
                timer.Interval = pollingIntervalMS;
                timer.Enabled = true;
                timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
                timer.Start();
            }
            else
            {
                timer.Interval = pollingIntervalMS;
            }
        }
    }
    public static void Stop()
    {
        if (Properties.Settings.Default.ConfigurationMonitoring)
        {
            if (timer.Enabled)
            {
                timer.Stop();
            }
        }
    }

    /// <summary>
    /// Monitors CE table for changes
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private static void OnTimerElapsed(object sender, ElapsedEventArgs e)
    {
        timer.Enabled = false;
        PollForChanges();
        timer.Enabled = true;
    }
    public static DateTime PollForChanges()
    {
        LastPoll = PollForChanges(LastPoll);
        return (LastPoll);
    }
    public static DateTime PollForChanges(DateTime lastPollDate)
    {
        try
        {
            _Working = true;
            foreach (ConfigurationMonitorAttribute monitor in MonitorMethods)
            {
                try
                {
                    lastPollDate = monitor.InvokeMethod(lastPollDate);
                    if (lastPollDate > LastRefreshDate)
                        LastRefreshDate = lastPollDate;
                }
                catch (System.Exception ex)
                {
                    // log the exception; my code omitted for brevity
                }
            }
        }
        catch (System.Exception ex)
        {
            // log the exception; my code omitted for brevity

        }
        finally
        {
            _Working = false;
        }
        return (lastPollDate);
    }

    #region Events
    /// <summary>
    /// Event raised when an AppDomain reset should occur
    /// </summary>
    public static event AppDomainChangeEvent AppDomainChanged;
    public static void OnAppDomainChanged(string configFile, IDictionary<string, object> properties)
    {
        if (AppDomainChanged != null) AppDomainChanged(null, new AppDomainArgs(configFile, properties));
    }
    #endregion
}

当我们有一个要“参与”此轮询机制的用例时,我们用属性标记一些方法:

[assembly: ConfigurationMonitorAttribute(typeof(bar), "Monitor")]
namespace foo
{
  public class bar 
  {
    public static DateTime Monitor(DateTime lastPoll)
    {
      // do your expensive work here, setting values in your cache
    }
  }
}
  

我们通过ConfigurationMonitor返回DateTime来触发方法的模式是一个非常奇怪的情况。您当然可以使用void方法。

ConfigurationMonitorAttribute如下所示:

[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class ConfigurationMonitorAttribute : Attribute
{
    private Type _type;
    private string _methodName;

    public ConfigurationMonitorAttribute(Type type, string methodName)
    {
        _type = type;
        _methodName = methodName;
    }

    public Type Type
    {
        get
        {
            return _type;
        }
    }

    public string MethodName
    {
        get
        {
            return _methodName;
        }
    }

    private MethodInfo _Method;
    protected MethodInfo Method
    {
        get
        {
            if (_Method == null)
            {
                _Method = Type.GetMethod(MethodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
                if (_Method == null)
                    throw new ArgumentException(string.Format("The type {0} doesn't have a static method named {1}.", Type, MethodName));

            }
            return _Method;
        }
    }
    public DateTime InvokeMethod(DateTime lastPoll)
    {
        try
        {
            return (DateTime)Method.Invoke(null, new object[] { lastPoll });
        }
        catch (System.Exception err)
        {
            new qbo.Exception.ThirdPartyException(string.Format("Attempting to monitor {0}/{1} raised an error.", _type, _methodName), err);
        }
        return lastPoll;
    }
}

答案 3 :(得分:0)

在这种情况下,除了说“我想添加缓存”以外,还有其他一些事情需要考虑。

  1. 如果您在Azure或Web场中运行,则需要集中式缓存(REDIS等),因为内存缓存将被破坏并与您的站点一起重新创建,并且位于本地一台服务器上农场,因此您不一定会看到性能提升。

  2. 如果您确实设置了REDIS缓存,请确保在配置它时要格外小心。编码必须正确,以解决连接问题,如果操作不正确,最终将导致连接池超载。

  3. 这更多地取决于您的情况,但是即使要花4秒才能返回7万条记录,似乎也很高。您是否已执行了执行计划,以查看是否存在缺少可应用的CTE的索引或优化?

答案 4 :(得分:0)

您可以设置缓存的过期策略并按需加载。您还可以具有一个以上的缓存级别,一个为分布式缓存,一个为本地缓存,因为本地版本总是最快的。我更喜欢按需加载而不是轮询,因为轮询总是刷新数据,即使在没有人收听的时候也是如此。如果您采用多层存储,则可以轮询分布式缓存,并按需加载本地缓存。那将与您获得的效率差不多。