对象缓存技术
上次修改时间: 2010年1月17日
适用范围: SharePoint Foundation 2010
许多开发人员都使用 Microsoft .NET Framework 缓存对象(例如 System.Web.Caching.Cache)帮助更好地利用内存并提高总体系统性能。但是,许多对象都不是"线程安全的",缓存这些对象会导致应用程序失败,并导致意外或无关的用户错误。
备注
本节中讨论的缓存技术与自定义缓存概述中讨论的用于 Web 内容管理的自定义缓存选项不同。
缓存数据和对象
缓存是提高系统性能的一种很好的方法。但是,您必须根据线程安全性的需要权衡缓存的好处,因为有些 SharePoint 对象不是线程安全的,缓存会导致它们行为异常。
缓存非线程安全的 SharePoint 对象
您可能会尝试通过缓存从查询返回的 SPListItemCollection 对象来提高性能和内存利用率。一般来说,这是一种不错的做法;但是,SPListItemCollection 对象包含嵌入的 SPWeb 对象,后者不是线程安全的,不应该进行缓存。
例如,假定 SPListItemCollection 对象缓存在线程中。当其他线程尝试读取该对象时,应用程序会失败或行为异常,因为嵌入的 SPWeb 对象不是线程安全的。有关 SPWeb 对象和线程安全性的详细信息,请参阅 Microsoft.SharePoint.SPWeb 类。
下面一节中的指导介绍在多线程环境中缓存非线程安全的 SharePoint 对象时如何阻止出现问题。
了解线程同步的潜在缺点
您可能不知道代码在多线程环境中运行(默认情况下,Internet Information Services(即 IIS)是多线程的)或如何管理该环境。下面的示例演示有时用于缓存非线程安全的 Microsoft.SharePoint.SPListItemCollection 对象的代码。
不良的编码实践
缓存多个线程可能读取的对象
public void CacheData()
{
SPListItemCollection oListItems;
oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
if(oListItems == null)
{
oListItems = DoQueryToReturnItems();
Cache.Add("ListItemCacheName", oListItems, ..);
}
}
Public Sub CacheData()
Dim oListItems As SPListItemCollection
oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
If oListItems Is Nothing Then
oListItems = DoQueryToReturnItems()
Cache.Add("ListItemCacheName", oListItems,..)
End If
End Sub
上例中的缓存用法在功能上是正确的;不过,因为 ASP.NET 缓存对象是线程安全的,所以它引入了潜在的性能问题。(有关 ASP.NET 缓存的详细信息,请参阅 Cache 类。)如果上例中的查询需要 10 秒钟才能完成,则这段时间内可能会有许多用户同时尝试访问该页面。在这种情况下,所有用户会运行同一查询,这会尝试更新同一缓存对象。如果该同一查询运行 10 次、50 次或 100 次,并且多个线程尝试同时更新同一对象,尤其在多重处理的超线程计算机上,性能问题将变得尤其严重。
要防止多个查询同时访问相同对象,必须按如下所示更改代码。
应用锁
检查是否为空
private static object _lock = new object();
public void CacheData()
{
SPListItemCollection oListItems;
lock(_lock)
{
oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
if(oListItems == null)
{
oListItems = DoQueryToReturnItems();
Cache.Add("ListItemCacheName", oListItems, ..);
}
}
}
Private Shared _lock As New Object()
Public Sub CacheData()
Dim oListItems As SPListItemCollection
SyncLock _lock
oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
If oListItems Is Nothing Then
oListItems = DoQueryToReturnItems()
Cache.Add("ListItemCacheName", oListItems,..)
End If
End SyncLock
End Sub
可以通过将锁放在 if(oListItems == null) 代码块中来稍微提高性能。执行此操作时,无需挂起所有线程便可检查数据是否已缓存。根据查询返回数据所需的时间,仍可能有多个用户在同时运行查询。如果在多处理器计算机上运行,尤其会存在这种情况。请记住,运行的处理器越多,查询运行的时间越长,将锁放在 if() 代码块中引发问题的可能性就越大。要确保另一个线程没有在当前线程有机会进行处理之前创建 oListItems,可以使用以下模式。
应用锁
重新检查是否为空
private static object _lock = new object();
public void CacheData()
{
SPListItemCollection oListItems;
oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
if(oListItems == null)
{
lock (_lock)
{
// Ensure that the data was not loaded by a concurrent thread
// while waiting for lock.
oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
if (oListItems == null)
{
oListItems = DoQueryToReturnItems();
Cache.Add("ListItemCacheName", oListItems, ..);
}
}
}
}
Private Shared _lock As New Object()
Public Sub CacheData()
Dim oListItems As SPListItemCollection
oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
If oListItems Is Nothing Then
SyncLock _lock
' Ensure that the data was not loaded by a concurrent thread
' while waiting for lock.
oListItems = CType(Cache("ListItemCacheName"), SPListItemCollection)
If oListItems Is Nothing Then
oListItems = DoQueryToReturnItems()
Cache.Add("ListItemCacheName", oListItems,..)
End If
End SyncLock
End If
End Sub
如果缓存已经填充,则上述示例的效果会与初始实现一样好。如果缓存尚未填充,并且系统的负载很轻,则获取锁会导致性能稍微下降。当系统的负载很重时,此方法应该能够显著提高性能,因为查询只执行一次而不是多次,并且与同步开销相比,查询的开销通常更昂贵。
这些示例中的代码会挂起 IIS 中运行的关键部分中的所有其他线程,并阻止其他线程访问缓存对象,直到已完全生成该缓存对象。这解决了线程同步问题;但是,代码仍然不正确,因为它缓存的是非线程安全的对象。
要解决线程安全性问题,可以缓存从 SPListItemCollection 对象创建的 DataTable 对象。您可以如下所示修改前面的示例,以便代码从 DataTable 对象获取数据。
良好的编码实践
缓存 DataTable 对象
private static object _lock = new object();
public void CacheData()
{
DataTable oDataTable;
SPListItemCollection oListItems;
lock(_lock)
{
oDataTable = (DataTable)Cache["ListItemCacheName"];
if(oDataTable == null)
{
oListItems = DoQueryToReturnItems();
oDataTable = oListItems.GetDataTable();
Cache.Add("ListItemCacheName", oDataTable, ..);
}
}
}
Private Shared _lock As New Object()
Public Sub CacheData()
Dim oDataTable As DataTable
Dim oListItems As SPListItemCollection
SyncLock _lock
oDataTable = CType(Cache("ListItemCacheName"), DataTable)
If oDataTable Is Nothing Then
oListItems = DoQueryToReturnItems()
oDataTable = oListItems.GetDataTable()
Cache.Add("ListItemCacheName", oDataTable,..)
End If
End SyncLock
End Sub
有关使用 DataTable 对象的详细信息和示例,以及开发 SharePoint 应用程序的其他好的想法,请参阅 DataTable 类的参考主题。