65.9K
CodeProject 正在变化。 阅读更多。
Home

异步刷新缓存数据

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2012 年 10 月 1 日

CPOL

5分钟阅读

viewsIcon

100172

使用异步刷新实现始终可用的缓存模式。

引言

Microsoft .NET 提供了一些出色的数据缓存机制。它们相对容易在您的应用程序中实现和集成。在本文中,我们将讨论一些特定的场景,在这种场景下,由于您要缓存的数据检索成本非常高,因此仅缓存机制是不够的。

背景

如果您想全面深入了解缓存,可以点击此处开始:https://codeproject.org.cn/search.aspx?q=Caching&doctypeid=1%3b2%3b3 

什么构成一个好的缓存机制?

  • 它必须快速:听起来很傻,但这是绝对的首要目标。
  • 它必须可清除:用户必须能够按需清除缓存。当知道有新数据可用时,谁想处理过时的数据?
  • 它必须有时间限制:缓存的数据不太可能永远不变。因此,好的缓存机制必须提供一种设置内容过期的方法。

通过使用System.Runtime.Caching.MemoryCache ,您可以获得这些好处以及更多好处,例如通过监视器自动失效。

为什么它(有时)不够好?

在某些情况下,填充缓存的成本非常高(数据由第三方系统通过 Web 服务提供,或计算量巨大的计算过程的结果等)。

在这种情况下,每当缓存失效(通过自动过期或手动失效)时,您的应用程序在获取新数据时就会被阻塞。我有一些生产环境中的 Web 应用程序,第三方需要两个多分钟才能提供数据!

如何改善这种情况?

在初始填充缓存时,没有什么太多可做的。数据必须不惜一切代价检索。您可以提供的最佳解决方案是在应用程序启动时加载所有缓存数据,如果可能,则使用并行线程。

但是,在运行时,当缓存失效时,**为什么不将其缓存数据在后台线程中重新填充**,然后隐藏数据检索对最终用户的成本呢?

一种略有不同的数据缓存模式

标准的缓存机制要求开发人员填充它,然后它会在内容过期后自动失效。通过后续方法,缓存机制将**自动填充**以及自动失效。

从编码员的角度来看,使用此模式时,您将能够提供一种检索数据及其生命周期的方法。换句话说,该机制被翻译为一个具有以下抽象方法的抽象类:

protected abstract T GetData();// where T is the type of data you want to cache
protected virtual TimeSpan GetLifetime();

然后,您将通过具有以下签名的静态属性访问缓存数据:

public static T Data { get ;}

您可以通过以下静态方法强制失效:

public static void Invalidate();

请仔细阅读

我将向您介绍此模式的实现,但请阅读有关此模式**固有局限性**的重要说明。

缓存失效后,您可能无法立即获得“最新”数据!

原因很简单:该模式规定数据检索耗时很长,并且我们将要在后台检索它以防止 UI 阻塞。

如果调用 Invalidate() 方法,则会在后台异步调用 GetData() 方法。这意味着,在数据重新填充期间,只要您没有获得新数据,您的唯一选择就是提供“旧”数据。

话虽如此,您在为该模式提供实现时需要注意几件事:

  1. 首次填充缓存时:所有客户端线程都必须阻塞,直到数据完全就绪为止。
  2. 防止阻塞:当缓存的数据过期或失效时,只要新数据不可用,缓存仍应提供旧内容。
  3. 您必须确保只启动一个线程在后台刷新数据。

此模式的一种可能实现

这是您可以实现的最基本的一种,它将数据存储在内存中,每次请求数据时,它都会检查生命周期是否已过期。如果缓存过期时间设置为 10 分钟,并且您有一个小时内没有调用,那么数据至少将在缓存中保留一小时又 10 分钟。

读者可以自行提供不同的实现,这些实现可能涉及计时器或其他任何内容。

这对我来说非常有用,因为我不缓存很少使用的数据(您呢?)。

public abstract class Cache<U,T>
    where U : Cache<U,T>, new()
    where T : class
{

    protected abstract T GetData();
    protected virtual TimeSpan GetLifetime() { return TimeSpan.FromMinutes(10); }

    protected Cache() { }

    enum State
    {
        Empty,
        OnLine,
        Expired,
        Refreshing
    }

    static U Instance = new U();
    static T InMemoryData { get; set; }
    static volatile State CurrentState = State.Empty;
    static volatile object StateLock = new object();
    static volatile object DataLock = new object();
    static DateTime RefreshedOn = DateTime.MinValue;

    public static T Data
    {
        get
        {
            switch (CurrentState)
            {
                case State.OnLine: // Simple check on time spent in cache vs lifetime
                    var timeSpentInCache = (DateTime.UtcNow - RefreshedOn);
                    if (timeSpentInCache > Instance.GetLifetime())
                    {
                        lock (StateLock)
                        {
                            if (CurrentState == State.OnLine) CurrentState = State.Expired;
                        }
                    }
                    break;

                case State.Empty: // Initial load : blocking to all callers
                    lock (DataLock)
                    {
                        lock (StateLock)
                        {
                            if (CurrentState == State.Empty)
                            {
                                InMemoryData = Instance.GetData(); // actually retrieve data from inheritor
                                RefreshedOn = DateTime.UtcNow;
                                CurrentState = State.OnLine;
                            }
                        }
                    }
                    break;

                case State.Expired: // The first thread getting here launches an asynchronous refresh
                    lock (StateLock)
                    {
                        if (CurrentState == State.Expired)
                        {
                            CurrentState = State.Refreshing;
                            Task.Factory.StartNew(() => Refresh());
                        }
                    }
                    break;

            }

            lock (DataLock)
            {
                if (InMemoryData != null) return InMemoryData;
            }

            return Data;
        }
    }

    static void Refresh()
    {
        if (CurrentState == State.Refreshing)
        {
            var dt = Instance.GetData(); // actually retrieve data from inheritor
            lock (StateLock)
            {
                lock (DataLock)
                {
                    RefreshedOn = DateTime.UtcNow;
                    CurrentState = State.OnLine;
                    InMemoryData = dt;
                }
            }
        }
    }

    public static void Invalidate()
    {
        lock (StateLock)
        {
            RefreshedOn = DateTime.MinValue;
            CurrentState = State.Expired;
        }
    }
}

用法示例

一个持有非常昂贵的字符串列表的缓存,该列表需要三秒钟才能构建。

public class MyExpensiveListOfStrings : Cache<MyExpensiveListOfStrings, List<string>>
{
    protected override List<string> GetData()
    {
        System.Diagnostics.Trace.WriteLine("Getting fresh data...");
        
        // Make it a really expensive list of string...
        Thread.Sleep(3000);
        
        List<string> result = new List<string>();
        for (int i = 0; i < 10000; i++)
        {
            result.Add("Data  - " + i.ToString());
        }

        return result;
    }

    protected override TimeSpan GetLifetime()
    {
        // Refreshed asynchronously when has spent more than 30 seconds in memory
        return TimeSpan.FromSeconds(30);
    }
}

使用缓存的程序示例

该程序将创建 10 个线程来访问缓存的字符串列表。

首次填充缓存时,所有线程都将被阻塞。

然后,缓存将在每 30 秒后在后台刷新,或者在您按下空格键时失效。将不再有任何线程被阻塞。

class Program
{
    static void Main(string[] args)
    {
       for (int i =0;i<10; i++) 
       {
            var t = Task.Factory.StartNew(() =>
            {
                while (true)
                {
                    System.Diagnostics.Trace.WriteLine("Looping " + 
                      Thread.CurrentThread.ManagedThreadId + " -> " + 
                      MyExpensiveListOfStrings.Data.Count);
                    Thread.Sleep(50);
                }
            });
        }

        ConsoleKeyInfo key = Console.ReadKey();
        while (key.Key == ConsoleKey.Spacebar)
        {
            MyExpensiveListOfStrings.Invalidate();
            key = Console.ReadKey();
        }
    }
}

观点的不同

我知道构建一个处理并非最新数据的程序听起来很奇怪……

现在从全局角度来看,您会发现是最终用户在使用略微过时的数据,程序并不在意……再进一步看:无论您如何缓存数据,如果用户需要最新数据而数据需要两分钟才能加载,那么您的用户**将不得不等待**两分钟。

现在的问题是:**当一个需要最新版本数据的用户正在等待时,是否有任何好理由阻塞应用程序中所有依赖于缓存数据的线程?**

如果您的答案是

  • 毫无疑问地是!那么请忽略本文 Smile | <img src=
  • 这取决于数据的类型!那么我希望您在这里读到了一些有趣的内容。

伙计们,非常欢迎您提供反馈。

© . All rights reserved.