对象缓存技术

上次修改时间: 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 类的参考主题。