在软件开发中,缓存是一种常见的技术,用于提高数据访问速度和减轻服务器压力。然而,在某些情况下,传统的缓存机制可能不足以满足需求,尤其是当数据获取成本高昂时。本文将探讨在这种情况下,如何通过改进缓存机制来提升性能。
缓存机制的基本概念是将数据存储在内存中,以便快速访问。然而,如果数据获取成本高昂,如需要通过第三方系统提供的Web服务或进行复杂的计算,那么每次缓存失效时,应用程序都会阻塞等待新数据,这可能导致性能问题。
一个好的缓存机制应该具备以下特点:
使用System.Runtime.Caching.MemoryCache等工具,可以获得这些优势以及其他一些好处,如通过监视器自动使缓存失效。
在某些情况下,填充缓存的成本非常高,例如第三方系统提供的数据需要超过两分钟才能获取。在这种情况下,每当缓存失效时,应用程序都会阻塞等待新数据,这会影响用户体验。
在缓存的初始填充阶段,没有太多可以做的,数据必须以任何代价获取。最好的解决方案是在应用程序启动时加载所有缓存数据,如果可能的话,使用并行线程。但在运行时,当缓存失效时,为什么不在后台线程重新填充缓存数据,然后将数据检索的成本隐藏起来呢?
标准的缓存机制要求开发者填充缓存,然后它在内容过期后自动失效。改进后的缓存机制将自动填充和自动失效。从编码者的角度来看,这意味着需要提供一个获取数据的方法和其生命周期。换句话说,机制可以转化为一个包含以下抽象方法的抽象类:
protected abstract T GetData();
protected virtual TimeSpan GetLifetime();
然后可以通过一个静态属性访问缓存数据:
public static T Data { get; }
可以通过以下静态方法强制使缓存失效:
public static void Invalidate();
在提供这种模式的实现时,需要注意以下几点:
这里是一个基本的实现示例,它将数据存储在内存中,并在每次请求数据时检查生命周期是否已过期。如果缓存过期时间设置为10分钟,而在一小时内没有请求,那么数据将在缓存中至少保留1小时10分钟。
public abstract class Cache where U : Cache, 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:
var timeSpentInCache = (DateTime.UtcNow - RefreshedOn);
if (timeSpentInCache > Instance.GetLifetime())
{
lock (StateLock)
{
if (CurrentState == State.OnLine) CurrentState = State.Expired;
}
}
break;
case State.Empty:
lock (DataLock)
{
lock (StateLock)
{
if (CurrentState == State.Empty)
{
InMemoryData = Instance.GetData();
RefreshedOn = DateTime.UtcNow;
CurrentState = State.OnLine;
}
}
}
break;
case State.Expired:
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();
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>
{
protected override List GetData()
{
System.Diagnostics.Trace.WriteLine("Getting fresh data...");
Thread.Sleep(3000);
List result = new List();
for (int i = 0; i < 10000; i++)
{
result.Add("Data - " + i.ToString());
}
return result;
}
protected override TimeSpan GetLifetime()
{
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();
}
}
}
从用户的角度来看,使用稍微过时的数据可能听起来很奇怪。但是,如果从更宏观的角度来看,用户实际上是在使用稍微过时的数据,程序并不关心这一点。
进一步思考:无论以何种方式缓存数据,如果用户需要最新数据,而数据加载需要两分钟,那么用户将不得不等待两分钟。