一个简单通用的缓存






4.90/5 (19投票s)
一篇关于实现自己的简单缓存的文章。
引言
在我作为程序员多年的职业生涯中,我知道缓存是什么以及它的用途,但我从未遇到过缓存成为关键必需品的情况。我们都知道缓存如何在某些情况下显著提高性能,但更多的时候,开发人员并没有在这些情况下使用缓存。
在本文中,我将带领你通过一些代码来实现一个简单但通用的缓存对象。缓存中的每个项都存储为object
。缓存将使用最近最少使用 (LRU) 替换方法来使缓存项失效。我计划写一篇后续文章,介绍一个更高级的缓存,支持更多功能和更多替换方法。
过程快速概览
总的来说,一个用户(通常是一段软件)需要某种资源。这种资源可以是任何东西,包括大文件、数据集/数据读取器或网络上的资源等等。每当获取此类资源是一项耗时操作(在时间和/或 CPU 周期方面),该资源就成为缓存的候选。
用户通常会首先从缓存请求此资源,以避免耗费大量时间获取它。如果资源在缓存中,则将其返回给请求者;否则,将在其他地方获取该资源并将其交给缓存以供将来检索。
本文介绍的缓存就是基于这个模型工作的。用户或请求者是一个应用程序或服务。它将引用 GenericCache 类库。通用缓存提供了一个回调机制,用于从用户处获取资源并将其添加到缓存中。当用户想要获取资源时,会调用Fetch()
方法并检索该项。如果该项不在缓存中,GenericCache
对象将使用用户的回调实现来获取资源,将其添加到缓存中,然后返回缓存的资源。
使用代码
下载部分提供的源代码是一个完整的 Visual Studio 2003 项目,包含解决方案文件。
注意:假设读者熟悉Dictionary
数据类型以及字典所遵循的键值对模型。该数据类型是此缓存库的基础。
包含缓存项
在我寻找合适的缓存项容器类时,我发现了 .NET Framework 在System.Collections.Specialized
命名空间中提供的NameObjectCollectionBase
抽象类。该类提供了以哈希表方式存储项以及以索引ArrayList
方式存储项的功能。这符合我快速以 FIFO(先进先出)方式访问项并使用唯一键访问项的需求。哈希表的键本质上不是唯一的,但在派生类中,我不允许使用重复的键。
让我们看一下这个类
using System;
using System.Collections.Specialized;
namespace GenericCache
{
// Inherits from the NameObjectCollectionBase abstract class.
// This class will act as the content container and is also
// a wrapper for the functionality of the
// NameObjectCollectionBase class.
internal class Dictionary : NameObjectCollectionBase
{
// Nothing special is done in the constructor and
// the base class' constructor is called.
public Dictionary( int p_initialCapacity ) : base( p_initialCapacity )
{
}
// Removes the oldest item from the cache.
public object Remove( out string p_key )
{
object toReturn;
if ( this.Count < 1 )
{
// Nothing to remove.
p_key = null;
return null;
}
// Get the oldest cache item.
toReturn = this.BaseGet( 0 );
//Get the oldest item key.
p_key = this.BaseGetKey( 0 );
// Remove the oldest item.
this.BaseRemoveAt( 0 );
return toReturn;
}
// Indexer to get a cached item.
public object this[ string p_key ]
{
get
{
return ResetItem( p_key );
}
}
// Add a cache item.
public void Add( string p_key, ref object p_item )
{
// The cache item will automatically be
// added to the end of the underlying ArrayList.
this.BaseAdd( p_key, p_item );
}
// Retrieves a cached item from the NameObjectCollectionBase
// and returns it. Also, the retrieved item is removed and
// then added again to ensure its age is reset.
object ResetItem( string p_key )
{
object tempItem;
tempItem = this.BaseGet( p_key );
// If the retrieved item is null,
// it isn't reset.
if ( tempItem != null )
{
this.BaseRemove( p_key );
this.BaseAdd( p_key, tempItem );
}
return tempItem;
}
// Clears the entire contents of the cache.
public void Clear()
{
this.BaseClear();
}
}
}
代码大部分是自明的,但为了清晰起见,我将解释其中一些方法的用途
public object Remove( out string p_key );
用于过期缓存项的替换方法是最近最少使用方法。此方法按照从很久以前访问的项到最近访问的项的顺序使缓存项失效。
NameObjectCollectionBase
类内部使用ArrayList
,这为我们提供了先移除最旧项的功能。稍后我们会看到,当访问一个缓存项时,它会从容器中移除,然后再次添加,以重置其“年龄”。此方法首先检查容器是否包含项。如果没有,它会简单地返回
null
,并且out
参数也会设置为null
。如果它确实包含项,则会移除底层ArrayList
中索引为 0 的项。这确保了最不常使用的项首先被移除。该方法返回已移除的项,并将out
参数p_key
设置为已移除项的键。object ResetItem( string p_key );
此私有方法通过从底层
ArrayList
(从特定索引)移除一项并将其再次添加到底层ArrayList
的末尾来重置项的“年龄”。
定义了两个事件
在我们主要的Cache
对象(稍后讨论)中,定义了两个事件和两个委托
public delegate void CacheItemExpiredEventHandler( object p_source, CacheItemExpiredEventArgs p_e );
public event CacheItemExpiredEventHandler CacheItemExpired;
CacheItemExpiredEventHandler
是一个处理缓存项过期的事件的委托。每当由于容量已满而从缓存中移除一项时,都会触发此事件。以下是CacheItemExpiredEventArgs
类的定义using System; namespace GenericCache { // Holds the CacheItemExpired event arguments. public class CacheItemExpiredEventArgs { string key; object item; public CacheItemExpiredEventArgs( string p_key, ref object p_item ) { key = p_key; item = p_item; } public object Item { get { return item; } } public string Key { get { return key; } } } }
它只包含对已过期对象的引用以及用于访问该对象的键。
public delegate object FetchItemEventHandler( object p_source, FetchItemEventArgs p_e );
public event FetchItemEventHandler FetchItem;
FetchItemEventHandler
是一个处理从用户处获取缓存项以供缓存的事件的委托。每当请求缓存中的某项但未找到时,都会触发此事件。然后,缓存会要求用户在事件实现中提供该项作为返回对象。以下是FetchItemEventArgs
类的定义using System; namespace GenericCache { // Holds the FetchItem event arguments. public class FetchItemEventArgs { string key; public FetchItemEventArgs( string p_key ) { key = p_key; } public string Key { get { return key; } } } }
它只包含用于获取该项的键的引用。
此事件的返回类型为
object
。处理此事件的用户通过从事件实现中返回请求的项来提供该项。
Cache 类
这是代码以及后面的一些方法的说明
using System;
using System.Threading;
namespace GenericCache
{
// This delegate will be used as a definition for the event
// to notify the caller that an item has expired.
public delegate void CacheItemExpiredEventHandler( object p_source,
CacheItemExpiredEventArgs p_e );
// This delegate will be used as a definition to get an
// item if it does not exist in the cache.
public delegate object FetchItemEventHandler( object p_source,
FetchItemEventArgs p_e );
// Cache class. All the members of this class is thread safe.
public class Cache
{
// Notifies the user that a cache item has expired.
public event CacheItemExpiredEventHandler CacheItemExpired;
// Gets an item to cache if it doesn't exist in the cache.
public event FetchItemEventHandler FetchItem;
// Holds the instance of the dictionary container.
Dictionary dictionary;
// The maximum size of the cache.
int capacity;
// A ReaderWriterLock to synchronize access to the dictionary.
ReaderWriterLock dictionaryLock;
public Cache( int p_initialSize, int p_capacity ) : base()
{
// Initial size cannot be smaller than 1.
if ( p_initialSize < 1 )
p_initialSize = 1;
// Capacity cannot be smaller than 0.
// If capacity is 0, there is no maximum size for
// the cache and it will always grow.
if ( p_capacity < 0 )
p_capacity = 0;
dictionary = new Dictionary( p_initialSize );
capacity = p_capacity;
// Instantiate the lock.
dictionaryLock = new ReaderWriterLock();
}
// Allows the user to retrieve a cached item from the
// cache. If it doesn't exist in the cache, it is
// retrieved with the FetchItem event and entered into
// the cache.
public object Fetch( string p_key )
{
object tempItem;
dictionaryLock.AcquireReaderLock( -1 );
try
{
// Get the item from the cache dictionary.
tempItem = dictionary[ p_key ];
}
finally
{
dictionaryLock.ReleaseReaderLock();
}
if ( tempItem != null )
{
// If the item exists, return it to the user.
return tempItem;
}
// The item is not in the cache.
// If no user has bound the FetchItem event,
// then the correct item cannot be retrieved.
// So null is simply returned.
if ( FetchItem == null )
return null;
// Fetch the correct item from the user by
// raising the FetchItem event.
tempItem = FetchItem( this, new FetchItemEventArgs( p_key ) );
// Nulls are not inserted into the cache.
if ( tempItem == null )
return null;
// The fetched item is not null. Call the
// RemoveItems method to remove items that
// are too old.
RemoveItems();
dictionaryLock.AcquireWriterLock( -1 );
try
{
// Add the new item to the cache.
dictionary.Add( p_key, ref tempItem);
}
finally
{
dictionaryLock.ReleaseWriterLock();
}
return tempItem;
}
// Gets the size of the cache.
public int Count
{
get
{
dictionaryLock.AcquireReaderLock( -1 );
try
{
return dictionary.Count;
}
finally
{
dictionaryLock.ReleaseReaderLock();
}
}
}
// Clears the entire content of the cache.
public void Clear()
{
dictionaryLock.AcquireWriterLock( -1 );
dictionary.Clear();
dictionaryLock.ReleaseWriterLock();
}
// Removes oldest items until the number of items in the
// cache is below capacity.
void RemoveItems()
{
string tempKey;
object tempItem;
dictionaryLock.AcquireWriterLock( -1 );
try
{
if ( capacity == 0 )
return;
while ( capacity - 1 < dictionary.Count )
{
tempItem = dictionary.Remove( out tempKey );
if ( CacheItemExpired != null )
CacheItemExpired( this,
new CacheItemExpiredEventArgs( tempKey,
ref tempItem ) );
}
}
finally
{
dictionaryLock.ReleaseWriterLock();
}
}
// Gets or sets the capacity of the cache.
public int Capacity
{
get
{
return capacity;
}
set
{
capacity = value;
if ( capacity < 0 )
capacity = 0;
}
}
}
}
此类是唯一将向用户公开的类。到目前为止,所有其他类都是internal
类,因为它们只被Cache
类使用。Cache
是完全线程安全的。
Cache
类仅包含前面讨论过的两个事件和两个私有变量,即
Dictionary dictionary;
int capacity;
dictionary
变量保存一个Dictionary
类的实例(前面也讨论过)。capacity
变量保存缓存可以持有的最大项数,在此之前它将开始使缓存项失效。此容量可以设置为 0,表示用户不希望项永远失效。
讨论以下方法
public Cache( int p_initialSize, int p_capacity ) : base()
- 唯一的构造函数。两个
if
语句强制p_initialSize
至少为 1,并且p_capacity
至少为 0。局部变量capacity
被设置为p_capacity
,并且创建了一个Dictionary
实例并存储在dictionary
中。void RemoveItems();
私有方法
RemoveItems
将从字典中移除项,直到项的数量比缓存的容量少 1。在将项插入字典之前调用它,并确保可以插入该项而无需进一步的容量检查。每次移除一项时,都会引发CacheItemExpired
事件,以通知用户某项已过期。public object Fetch( string p_key );
用户将使用此公共方法从缓存中获取缓存项。通过
p_key
参数提供所需对象的键。dictionary
索引器由tempItem = dictionary[ p_key ];
调用。在此方法中,如果项存在于字典中,其“年龄”会自动重置。如果项在缓存中找到,则直接返回。如果未找到该项,
tempItem
将为 null,并且必须从用户处获取该项。将引发FetchItem
事件,并返回要缓存的项。由于缓存不存储 null,如果事件返回 null 值,则返回null
并且不将任何内容添加到缓存。调用RemoveItems()
以确保字典中有空间添加新项。新获取的项将被添加到缓存中并返回供用户使用。
结论
尽管可以在此类缓存实现中添加许多(更高级的)功能,但我认为本文讨论的内容是提高昂贵的资源获取效率所需的最低限度。如果读者对本文有任何疑问或评论,请在下面的论坛消息板上联系我。
历史
- 2005-10-17 - 使用了
ReaderWriterLock
而不是普通锁。 - 2005-10-06 - 文章提交。