一个对.NET
平台下缓存解决方案的探索关于:
- MemoryCache:
.NET
自定义的内存缓存实现。 - CacheManager:一个开源的
.NET
内存缓存实现。
MemoryCache
是.NET唯一实现了ObjectCache
的类,可以使用它来在内存中缓存任意类型对象。MemoryCache.Default
是默认的MemoryCache
实例,.NET建议使用它而不是创建任意多个缓存对象。
Add
方法会在已存在相同Key
的缓存时返回false
,所以不会重复添加。
MemoryCache.Default.Add(Warehouse.CACHE_KEY, Warehouse.Warehouses, DateTimeOffset.Now.AddSeconds(2));
var warehousesFromCache = MemoryCache.Default.Get(Warehouse.CACHE_KEY) as List<Warehouse>;
Assert.That(warehousesFromCache, Is.Not.Null);
Assert.That(warehousesFromCache.Count, Is.EqualTo(2));
返回新添加或已存在相同Key
的缓存Value。
MemoryCache.Default.Add(Warehouse.CACHE_KEY, Warehouse.Warehouses, MemoryCache.InfiniteAbsoluteExpiration);
var caches = MemoryCache.Default.AddOrGetExisting(Warehouse.CACHE_KEY, Warehouse.Warehouses.Where(o => o.WarehouseNumber == "07").ToList(), MemoryCache.InfiniteAbsoluteExpiration) as List<Warehouse>;
Assert.That(caches.Count, Is.Not.EqualTo(1));
Assert.That(caches.Count, Is.EqualTo(2));
更新(替换)缓存。
MemoryCache.Default.Add(Warehouse.CACHE_KEY, Warehouse.Warehouses, MemoryCache.InfiniteAbsoluteExpiration);
var specialWarehouse = Warehouse.Warehouses.Where(o => o.WarehouseNumber == "07").ToList();
MemoryCache.Default.Set(Warehouse.CACHE_KEY, specialWarehouse, MemoryCache.InfiniteAbsoluteExpiration);
var caches = MemoryCache.Default.Get(Warehouse.CACHE_KEY) as List<Warehouse>;
Assert.That(caches, Is.Not.Null);
Assert.That(caches.Count, Is.EqualTo(1));
Assert.That(caches[0].WarehouseNumber == "07");
有两种缓存过期策略:
- AbsoluteExpiration:绝对时间过期。在指定的时间间隔后过期,缓存移除、丢失。
- SlidingExpiration:滑动时间过期。在指定的时间间隔后若未命中过该缓存,则移除、丢失;否则,则顺延指定的时间间隔,缓存不移除。
MemoryCache.Default.Add(Warehouse.CACHE_KEY+"1", Warehouse.Warehouses, DateTimeOffset.Now.AddSeconds(2));
Thread.Sleep(2000);
var warehousesFromCache = MemoryCache.Default.Get(Warehouse.CACHE_KEY+"1") as List<Warehouse>;
Assert.That(warehousesFromCache, Is.Null);
当Cache丢失前,MemoryCache会回掉一个Callback委托进行通知,此时,为了让应用程序没有该缓存真空期,我们应该立即刷新缓存,保持最新的同时防止客户端出现缓存未命中(缓存丢失)的现象发生。
[Test]
public void CacheShouldRefreshSuccessAfterExpired()
{
MemoryCache.Default.Set(Warehouse.CACHE_KEY, Warehouse.Warehouses, new CacheItemPolicy()
{
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(2),
UpdateCallback = this.CacheRefreshCallback
});
Thread.Sleep(5000);
var warehousesFromCache = MemoryCache.Default.Get(Warehouse.CACHE_KEY) as List<Warehouse>;
Assert.That(warehousesFromCache, Is.Not.Null);
}
private void CacheRefreshCallback(CacheEntryUpdateArguments args)
{
var cacheItem = MemoryCache.Default.GetCacheItem(args.Key);
var cacheObj = cacheItem.Value;
cacheItem.Value = cacheObj;
args.UpdatedCacheItem = cacheItem;
var policy = new CacheItemPolicy
{
UpdateCallback = new CacheEntryUpdateCallback(CacheRefreshCallback),
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(2)
};
args.UpdatedCacheItemPolicy = policy;
}
在需要缓存文件内容时适用;且当文件内容更改时自动刷新缓存。
var configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "warehouse.txt");
var contents = MemoryCache.Default.Get("WarehouseConfig");
if (contents == null)
{
var policy = new CacheItemPolicy()
{
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30),
};
policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string>() { configFilePath }));
MemoryCache.Default.Set("WarehouseConfig", File.ReadAllText(configFilePath), policy);
}
Thread.Sleep(5000);
var contentsFromCache = MemoryCache.Default.Get("WarehouseConfig");
Assert.That(contentsFromCache, Is.Not.Null);
Assert.That(contentsFromCache, Is.EqualTo("01,02,03"));
CacheManager
是GitHub
上一个开源的专注于.NET Cache
领域项目,其有许多新的思想和概念是.NET MemoryCache
所不具备的;相对于MemoryCache
,其有以下特有feature
:
- 代码实现、写法上更符合现如今的可读性、连续性的友好风格
- 缓存抽象出
Layer
的概念,同一份缓存数据可以同时缓存到多个Layer
(CacheManager称之为'Handle')上,有利于防止某Layer
缓存意外丢失,降低客户端缓存未命中的概率,其中DictionaryHandle
性能最好 - 每层可单独定义过期策略,并指定层与层之间当发现缓存不一致时(有的丢失了,有的还在)的更新策略
- 支持通过配置文件(.config)形式定义缓存策略(过期策略、缓存
Handle
等),就可以不修改代码来随时更改 - 有针对每个
Layer
缓存的Add、Remove、Update、Hit统计信息输出 - 有提供Logging接口,以记录缓存在Add、Remove、Update、Hit时的详细信息输入
- 有提供分布式缓存不一致的解决方案(ClientA、ClientB共享同一份缓存,同时各自缓存一份In-Process缓存在本地,当ClientA更新了本地缓存时,通过消息的方式通知ClientB更新本地缓存)
添加缓存。已存在时返回false
var cacheManager = CacheFactory.Build<List<Warehouse>>(setting =>
{
setting.WithDictionaryHandle();
setting.WithSystemRuntimeCacheHandle();
});
cacheManager.Add(Warehouse.CACHE_KEY, Warehouse.Warehouses);
var caches = cacheManager.Get(Warehouse.CACHE_KEY);
Assert.That(caches.Count, Is.EqualTo(2));
Assert.That(cacheManager.CacheHandles.Count, Is.EqualTo(2));
返回新添加的或已存在相同Key
的缓存。
var cacheManager = CacheFactory.Build<List<Warehouse>>(settings =>
{
settings.WithDictionaryHandle();
});
var caches = cacheManager.GetOrAdd(Warehouse.CACHE_KEY, Warehouse.Warehouses);
Assert.That(caches.Count, Is.EqualTo(2));
不存在就添加,存在就更新。
var manager = CacheFactory.Build<List<Company>>(settings =>
{
settings.WithDictionaryHandle();
});
manager.Add(Company.CACHE_KEY, Company.MovieCompanies);
manager.AddOrUpdate(Company.CACHE_KEY, Company.ITCompanies, o => Company.ITCompanies);
var caches = manager.Get(Company.CACHE_KEY);
Assert.That(caches, Is.Not.Null);
Assert.That(caches.Count, Is.EqualTo(2));
Assert.That(caches[0].CompanyType, Is.EqualTo(CompanyType.ITCompany));
更新缓存。
var manager = CacheFactory.Build(settings =>
{
settings.WithDictionaryHandle();
});
manager.Add(Company.CACHE_KEY, Company.MovieCompanies);
manager.Update(Company.CACHE_KEY, o => Company.ITCompanies.Union(o as List<Company>).ToList());
var caches = manager.Get<List<Company>>(Company.CACHE_KEY);
Assert.That(caches.Count, Is.EqualTo(4));
从配置文件加载缓存策略等信息。
-
Caches.config
var cfg = ConfigurationBuilder.LoadConfigurationFile("Caches.config", "ITCompaniesCacheConfig");
var manager = CacheFactory.FromConfiguration<List<Company>>(cfg);
manager.Add(Company.CACHE_KEY, Company.ITCompanies);
var caches = manager.Get(Company.CACHE_KEY);
Assert.That(caches, Is.Not.Null);
Assert.That(caches.Count, Is.EqualTo(2));
将同一份数据缓存到多个Layer
,会按Layer
(Handle)的添加顺序去命中缓存(返回第一个命中的)。
var manager = CacheFactory.Build<List<Company>>(settings =>
{
settings.WithDictionaryHandle().WithExpiration(ExpirationMode.Absolute, TimeSpan.FromSeconds(1));
settings.WithSystemRuntimeCacheHandle();
settings.WithUpdateMode(CacheUpdateMode.None);
});
// all cache layer hold the ITCompanies.
manager.Add(Company.CACHE_KEY, Company.ITCompanies);
Thread.Sleep(1000);
var caches = manager.Get(Company.CACHE_KEY); // ONLY When GET: If find some layer was difference from others, the "Update" strategy will be executed.
Assert.That(caches, Is.Not.Null);
Assert.That(caches.Count, Is.EqualTo(2));
Assert.That(manager.CacheHandles.Count, Is.EqualTo(2));
Assert.That(manager.CacheHandles.First().Count, Is.EqualTo(0));
Assert.That(manager.CacheHandles.Last().Count, Is.GreaterThanOrEqualTo(1));
缓存过期是针对每个Layer
的,可以用连续调用的方式来初始化一个带有过期策略的CacheManager
。
var manager = CacheFactory.Build<List<Company>>(settings =>
{
settings.WithDictionaryHandle().WithExpiration(ExpirationMode.Absolute, TimeSpan.FromSeconds(1));
settings.WithSystemRuntimeCacheHandle();
settings.WithUpdateMode(CacheUpdateMode.None);
});
注意这里的WithUpdateMode
,CacheUpdateMode
有三种值,分别代表:
- CacheUpdateMode.None: 无论缓存首先被命中在哪一个的
Layer
,各个Layer
存储的缓存不做任何更新即使它们彼此不一样; - CacheUpdateMode.Full: 无论缓存首先被命中在哪一个的
Layer
,各个Layer
存储的缓存将被更新一致当它们彼此不一样时; - CacheUpdateMode.UP: 更新缓存首先被命中的那个
Layer
添加顺序之上的所有Layer
的缓存以保持一致。
Cache
在各种Operation
下的次数统计信息输出。
var manager = CacheFactory.Build<List<Company>>(settings =>
{
settings.WithDictionaryHandle().WithExpiration(ExpirationMode.Absolute, TimeSpan.FromSeconds(1)).EnableStatistics().EnablePerformanceCounters();
settings.WithSystemRuntimeCacheHandle().EnableStatistics();
settings.WithUpdateMode(CacheUpdateMode.None);
});
// all cache layer hold the ITCompanies.
manager.Add(Company.CACHE_KEY, Company.ITCompanies);
manager.Put(Company.CACHE_KEY, Company.MovieCompanies);
manager.Remove(Company.CACHE_KEY);
manager.Add(Company.CACHE_KEY, Company.ITCompanies);
var cachesFinal = manager.Get(Company.CACHE_KEY);
foreach (var handle in manager.CacheHandles)
{
var stats = handle.Stats;
Console.WriteLine(string.Format(
"Items: {0}, Hits: {1}, Miss: {2}, Remove: {3}, ClearRegion: {4}, Clear: {5}, Adds: {6}, Puts: {7}, Gets: {8}",
stats.GetStatistic(CacheStatsCounterType.Items),
stats.GetStatistic(CacheStatsCounterType.Hits),
stats.GetStatistic(CacheStatsCounterType.Misses),
stats.GetStatistic(CacheStatsCounterType.RemoveCalls),
stats.GetStatistic(CacheStatsCounterType.ClearRegionCalls),
stats.GetStatistic(CacheStatsCounterType.ClearCalls),
stats.GetStatistic(CacheStatsCounterType.AddCalls),
stats.GetStatistic(CacheStatsCounterType.PutCalls),
stats.GetStatistic(CacheStatsCounterType.GetCalls)
));
}
支持Cache
在各种Operation
下的日志记录。
var manager = CacheFactory.Build<List<Company>>(settings =>
{
settings.WithDictionaryHandle().WithExpiration(ExpirationMode.Absolute, TimeSpan.FromSeconds(1)).EnableStatistics().EnablePerformanceCounters();
settings.WithSystemRuntimeCacheHandle().EnableStatistics();
settings.WithUpdateMode(CacheUpdateMode.None);
settings.WithLogging(typeof(CustomerLogFactory));
});
// all cache layer hold the ITCompanies.
manager.Add(Company.CACHE_KEY, Company.ITCompanies);
manager.Put(Company.CACHE_KEY, Company.MovieCompanies);
manager.Remove(Company.CACHE_KEY);
manager.Add(Company.CACHE_KEY, Company.ITCompanies);
var caches = manager.Get(Company.CACHE_KEY);
Assert.That(caches, Is.Not.Null);
OutPut Like Below:
CacheManager.Core.BaseCacheManager<object>: Trace: Add or update: key .
CacheManager.Core.BaseCacheManager<object>: Trace: Add: key
CacheManager.Core.BaseCacheManager<object>: Trace: Add: key to handle redis FAILED. Evicting items from other handles.
CacheManager.Core.BaseCacheManager<object>: Trace: Evict from other handles: key : excluding handle 1.
CacheManager.Core.BaseCacheManager<object>: Trace: Evict from handle: key : on handle default.
CacheManager.Core.BaseCacheManager<object>: Trace: Add or update: key : add failed, trying to update...
CacheManager.Core.BaseCacheManager<object>: Trace: Update: key .
CacheManager.Core.BaseCacheManager<object>: Trace: Update: key : tried on handle redis: result: Success.
CacheManager.Core.BaseCacheManager<object>: Trace: Evict from handles above: key : above handle 1.
CacheManager.Core.BaseCacheManager<object>: Trace: Evict from handle: key : on handle default.
CacheManager.Core.BaseCacheManager<object>: Trace: Add to handles below: key : below handle 1.
CacheManager.Core.BaseCacheManager<object>: Trace: Add or update: key : successfully updated.