0.簡介 緩存在一個業務系統中十分重要,常用的場景就是用來儲存調用頻率較高的數據。Abp 也提供了一套緩存機制供用戶使用,在使用 Abp 框架的時候可以通過註入 來新建/設置緩存。 同時 Abp 框架也提供了 Redis 版本的 實現,你也可以很方便的將現有的記憶體緩存替換為 Redis 緩存。 0. ...
0.簡介
緩存在一個業務系統中十分重要,常用的場景就是用來儲存調用頻率較高的數據。Abp 也提供了一套緩存機制供用戶使用,在使用 Abp 框架的時候可以通過註入 ICacheManager
來新建/設置緩存。
同時 Abp 框架也提供了 Redis 版本的 ICacheManager
實現,你也可以很方便的將現有的記憶體緩存替換為 Redis 緩存。
0.1 典型使用方法
public class TestAppService : ApplicationService
{
private readonly ICacheManager _cacheMgr;
private readonly IRepository<TestEntity> _rep;
// 註入緩存管理器與測試實體的倉儲
public TestAppService(ICacheManager cacheMgr, IRepository<TestEntity> rep)
{
_cacheMgr = cacheMgr;
_rep = rep;
}
public void TestMethod()
{
// 獲取/創建一個新的緩存
var cache = _cacheMgr.GetCache("緩存1");
// 轉換為強類型的緩存
var typedCache = cache.AsTyped<int, string>();
// 獲取緩存的數據,如果存在則直接返回。
// 如果不存在則執行工廠方法,將其值存放到
// 緩存項當中,最後返回緩存項數據。
var cacheValue = typedCache.Get(10, id => _rep.Get(id).Name);
Console.WriteLine(cacheValue);
}
}
1.啟動流程
同其他的基礎設施一樣,緩存管理器 ICacheManager
在 Abp 框架啟動的時候就自動被註入到了 Ioc 容器當中,因為他的基類 CacheManagerBase
繼承了 ISingletonDependency
介面。
public abstract class CacheManagerBase : ICacheManager, ISingletonDependency
{
// ... 其他代碼
}
其次就是他的 ICachingConfiguration
緩存配置是在 AbpCoreInstaller
註入到 Ioc 容器,並且同其他基礎設施的配置一起被集成到了 IAbpStartupConfiguration
。
internal class AbpCoreInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(
// 其他被註入的基礎設施配置
Component.For<ICachingConfiguration, CachingConfiguration>().ImplementedBy<CachingConfiguration>().LifestyleSingleton()
// 其他被註入的基礎設施配置
);
}
}
你可以在其他模塊的 PreInitialize()
方法裡面可以直接通過 Configuration.Caching
來配置緩存過期時間等功能。
public override void PreInitialize()
{
Configuration.Caching.ConfigureAll(z=>z.DefaultSlidingExpireTime = TimeSpan.FromHours(1));
}
2. 代碼分析
緩存這塊可能是 Abp 框架實現當中最簡單的一部分了,代碼量不多,但是設計思路還是值得借鑒的。
2.1 緩存管理器
2.1.1 基本定義
緩存管理器即 ICacheManager
,通常它用於管理所有緩存,他的介面定義十分簡單,就兩個方法:
public interface ICacheManager : IDisposable
{
// 獲得所有緩存
IReadOnlyList<ICache> GetAllCaches();
// 根據緩存名稱獲取緩存
[NotNull] ICache GetCache([NotNull] string name);
}
2.1.2 獲取/創建緩存
Abp 實現了一個抽象基類 CacheBase
實現了本介面,在 CacheBase
內部維護了一個 ConcurrentDictionary<string,ICache>
字典,這個字典裡面就是存放的所有緩存。
同時在他的 GetCache(string name)
內部呢,通過傳入的緩存名字來從字典獲取已經存在的緩存,如果不存在呢,執行其工廠方法來創建一個新的緩存。
public virtual ICache GetCache(string name)
{
Check.NotNull(name, nameof(name));
// 從字典根據名稱取得緩存,不存在則使用工廠方法
return Caches.GetOrAdd(name, (cacheName) =>
{
// 得到創建成功的緩存
var cache = CreateCacheImplementation(cacheName);
// 遍歷緩存配置集合,查看當前名字的緩存是否存在配置項
var configurators = Configuration.Configurators.Where(c => c.CacheName == null || c.CacheName == cacheName);
// 遍歷這些配置項執行配置操作,更改緩存的過期時間等參數
foreach (var configurator in configurators)
{
configurator.InitAction?.Invoke(cache);
}
// 返回配置完成的緩存
return cache;
});
}
// 真正創建緩存的方法
protected abstract ICache CreateCacheImplementation(string name);
這裡的 CreateCacheImplementation()
由具體的緩存管理器實現的緩存創建方法,因為 Redis 與 MemoryCache 的實現各不一樣,所以這裡定義了一個抽象方法。
2.1.3 緩存管理器銷毀
當緩存管理器被銷毀的時候,首先是遍歷字典記憶體儲的所有緩存,並通過 IIocManager.Release()
方法來釋放這些緩存,之後則是調用字典的 Clear()
方法清空字典。
public virtual void Dispose()
{
DisposeCaches();
// 清空字典
Caches.Clear();
}
// 遍歷字典,釋放對象
protected virtual void DisposeCaches()
{
foreach (var cache in Caches)
{
IocManager.Release(cache.Value);
}
}
2.1.4 記憶體緩存管理器
Abp 對於緩存管理器的預設實現是 AbpMemoryCacheManager
,其實沒多複雜,就是實現了基類的 CreateCacheImplementation()
返回特定的 ICache
。
public class AbpMemoryCacheManager : CacheManagerBase
{
// ... 忽略了的代碼
protected override ICache CreateCacheImplementation(string name)
{
// 就 new 一個新的記憶體緩存而已,記憶體緩存的實現請看後面的
// 這裡是因為 AbpMemory 沒有註入到 IOC 容器,所以需要手動 new
return new AbpMemoryCache(name)
{
Logger = Logger
};
}
// 重寫了基類的緩存釋放方法
protected override void DisposeCaches()
{
foreach (var cache in Caches.Values)
{
cache.Dispose();
}
}
}
2.1.5 Redis 緩存管理器
如果要使用 Redis 緩存管理器,根據模塊的載入順序,你需要在啟動模塊的 PreInitialize()
調用 Abp.Redis 庫提供的集成方法即可。
這裡先來看看他的實現:
public class AbpRedisCacheManager : CacheManagerBase
{
public AbpRedisCacheManager(IIocManager iocManager, ICachingConfiguration configuration)
: base(iocManager, configuration)
{
// 註冊 Redis 緩存
IocManager.RegisterIfNot<AbpRedisCache>(DependencyLifeStyle.Transient);
}
protected override ICache CreateCacheImplementation(string name)
{
// 解析已經註入的 Redis 緩存
// 這裡可以看到解析的時候如何傳入構造參數
return IocManager.Resolve<AbpRedisCache>(new { name });
}
}
一樣的,非常簡單,沒什麼可以說的。
2.2 緩存
我們從緩存管理器當中拿到具體的緩存之後才能夠進行真正的緩存操作,這裡需要明確的一個概念是緩存是一個緩存項的集合,緩存項裡面的值才是我們真正緩存的結果。
就如同一個用戶表,他擁有多條用戶數據,那麼我們要針對這個用戶表做緩存,就會創建一個緩存名稱叫做 "用戶表" 的緩存,在需要獲得用戶數據的時候,我們拿去數據就直接從這個 "用戶表" 緩存當中取得具體的緩存項,也就是具體的用戶數據。
其實每個緩存項也是幾個 鍵值對 ,鍵就是緩存的鍵,以上面的 "用戶表緩存" 為例子,那麼他緩存項的鍵就是 int
型的 Id ,他的值呢就是一個用戶實體。
2.2.1 基本定義
所有緩存的定義都在 ICache
當中,每個緩存都擁有增刪查改這些基本操作,並且還擁有過期時間與名稱等屬性。
同樣,緩存也有一個抽象基類的實現,名字叫做 CacheBase
。與緩存管理器的抽象基類一樣,CacheBase
內部僅實現了 Get
方法的基本邏輯,其他的都是抽象方法,需要由具體的類型進行實現。
public interface ICache : IDisposable
{
// 緩存名稱
string Name { get; }
// 相對過期時間
TimeSpan DefaultSlidingExpireTime { get; set; }
// 絕對過期時間
TimeSpan? DefaultAbsoluteExpireTime { get; set; }
// 根據緩存項 Key 獲取到緩存的數據,不存在則執行工廠方法
object Get(string key, Func<string, object> factory);
// Get 的非同步實現
Task<object> GetAsync(string key, Func<string, Task<object>> factory);
// 根據緩存項 Key 獲取到緩存的數據,沒有則返回預設值,一般為 null
object GetOrDefault(string key);
// GetOrDefault 的非同步實現
Task<object> GetOrDefaultAsync(string key);
// 設置緩存項值和過期時間等參數
void Set(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null);
// Set 的非同步實現
Task SetAsync(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null);
// 移除指定緩存名稱的緩存項
void Remove(string key);
// Remove 的非同步實現
Task RemoveAsync(string key);
// 清空緩存內所有緩存項
void Clear();
// Clear 的非同步實現
Task ClearAsync();
}
2.2.2 記憶體緩存的實現
這裡我們以 Abp 的預設 MemoryCache 實現為例子來看看裡面是什麼構造:
public class AbpMemoryCache : CacheBase
{
private MemoryCache _memoryCache;
// 初始化 MemoryCahce
public AbpMemoryCache(string name)
: base(name)
{
_memoryCache = new MemoryCache(new OptionsWrapper<MemoryCacheOptions>(new MemoryCacheOptions()));
}
// 從 MemoryCahce 取得緩存
public override object GetOrDefault(string key)
{
return _memoryCache.Get(key);
}
// 設置緩存
public override void Set(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null)
{
// 值為空的時候拋出異常
if (value == null)
{
throw new AbpException("Can not insert null values to the cache!");
}
if (absoluteExpireTime != null)
{
_memoryCache.Set(key, value, DateTimeOffset.Now.Add(absoluteExpireTime.Value));
}
else if (slidingExpireTime != null)
{
_memoryCache.Set(key, value, slidingExpireTime.Value);
}
else if (DefaultAbsoluteExpireTime != null)
{
_memoryCache.Set(key, value, DateTimeOffset.Now.Add(DefaultAbsoluteExpireTime.Value));
}
else
{
_memoryCache.Set(key, value, DefaultSlidingExpireTime);
}
}
// 刪除緩存
public override void Remove(string key)
{
_memoryCache.Remove(key);
}
// 清空緩存
public override void Clear()
{
_memoryCache.Dispose();
_memoryCache = new MemoryCache(new OptionsWrapper<MemoryCacheOptions>(new MemoryCacheOptions()));
}
public override void Dispose()
{
_memoryCache.Dispose();
base.Dispose();
}
}
可以看到在 AbpMemoryCache
內部就是將 MemoryCahce
進行了一個二次包裝而已。
其實可以看到這些緩存超期時間之類的參數 Abp 自己並沒有用到,而是將其傳遞給具體的緩存實現來進行管理。
2.2.3 Redis 緩存的實現
Abp.Redis 庫使用的是 StackExchange.Redis
庫來實現對 Redis 的通訊的,其實現為 AbpRedisCache
,裡面也沒什麼好說的,如同記憶體緩存一樣,實現那些抽象方法就可以了。
public class AbpRedisCache : CacheBase
{
private readonly IDatabase _database;
private readonly IRedisCacheSerializer _serializer;
public AbpRedisCache(
string name,
IAbpRedisCacheDatabaseProvider redisCacheDatabaseProvider,
IRedisCacheSerializer redisCacheSerializer)
: base(name)
{
_database = redisCacheDatabaseProvider.GetDatabase();
_serializer = redisCacheSerializer;
}
// 獲取緩存
public override object GetOrDefault(string key)
{
var objbyte = _database.StringGet(GetLocalizedKey(key));
return objbyte.HasValue ? Deserialize(objbyte) : null;
}
public override void Set(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null)
{
if (value == null)
{
throw new AbpException("Can not insert null values to the cache!");
}
//TODO: 這裡是一個解決實體序列化的方法.
//TODO: 通常實體不應該存儲在緩存當中,目前 Abp.Zero 包是這樣來進行處理的,這個問題將會在未來被修正.
var type = value.GetType();
if (EntityHelper.IsEntity(type) && type.GetAssembly().FullName.Contains("EntityFrameworkDynamicProxies"))
{
type = type.GetTypeInfo().BaseType;
}
_database.StringSet(
GetLocalizedKey(key),
Serialize(value, type),
absoluteExpireTime ?? slidingExpireTime ?? DefaultAbsoluteExpireTime ?? DefaultSlidingExpireTime
);
}
// 移除緩存
public override void Remove(string key)
{
_database.KeyDelete(GetLocalizedKey(key));
}
// 清空緩存
public override void Clear()
{
_database.KeyDeleteWithPrefix(GetLocalizedKey("*"));
}
// 序列化對象
protected virtual string Serialize(object value, Type type)
{
return _serializer.Serialize(value, type);
}
// 反序列化對象
protected virtual object Deserialize(RedisValue objbyte)
{
return _serializer.Deserialize(objbyte);
}
// 獲得緩存的 Key
protected virtual string GetLocalizedKey(string key)
{
return "n:" + Name + ",c:" + key;
}
}
2.3 緩存配置
緩存配置的作用就是可以為每個緩存配置不同的過期時間,我們最開始說過 Abp 是通過 ICachingConfiguration
來配置緩存的,在這個介面裡面呢定義了這樣幾個東西。
public interface ICachingConfiguration
{
// 配置項集合
IReadOnlyList<ICacheConfigurator> Configurators { get; }
// 配置所有緩存
void ConfigureAll(Action<ICache> initAction);
// 配置指定名稱的緩存
void Configure(string cacheName, Action<ICache> initAction);
}
Emmmm,可以看到他有個 Configurators
屬性存了一大堆 ICacheConfigurator
,這個玩意兒呢就是對應到具體緩存的配置項了。
public interface ICacheConfigurator
{
// 關聯的緩存名稱
string CacheName { get; }
// 緩存初始化的時候執行的配置操作
Action<ICache> InitAction { get; }
}
這玩意兒的實現也沒什麼好看的,跟介面差不多,這下我們知道了緩存的配置呢就是存放在 Configurators
裡面的。
然後呢,就在我們最開始的地方,緩存管理器創建緩存的時候不是根據名字去遍歷這個 Configurators
集合麽,在那裡面就直接通過這個 ICacheConfigurator
的 Action<ICache>
來配置緩存的超期時間。
至於 Configure()
和 ConfigureAll()
方法嘛,前者就是根據你傳入的緩存名稱初始化一個 CacheConfigurator
,然後扔到那個列表裡面去。
private readonly List<ICacheConfigurator> _configurators;
public void Configure(string cacheName, Action<ICache> initAction)
{
_configurators.Add(new CacheConfigurator(cacheName, initAction));
}
後者的話則是添加了一個沒有名字的 CacheConfigurator
,正因為沒有名字,所以他的 cacheName 肯定 null,也就是在緩存管理器創建緩存的時候如果該緩存沒有對應的配置,那麼就會使用這個名字為空的 CacheConfigurator
了。
2.4 強類型緩存
在最開始的使用方法裡面可以看到我們通過 AsType<TKey,TValue>()
方法將 ICache
對象轉換為 ITypedCache
,這樣我們就無需再將緩存項手動進行強制類型轉換。
註:雖然這裡是指定了泛型操作,但是呢,在其內部實現還是進行的強制類型轉換,也是會發生裝/拆箱操作的。
Abp 自己則通過 TypedCacheWrapper<TKey, TValue>
來將原有的 ICache
緩存包裝為 ITypedCache<TKey, TValue>
。
看看這個擴展方法的定義,他是放在 CacheExtensions
裡面的:
public static ITypedCache<TKey, TValue> AsTyped<TKey, TValue>(this ICache cache)
{
return new TypedCacheWrapper<TKey, TValue>(cache);
}
Emmm,這裡是 new 了一個 TypedCacheWrapper
來處理的,從方法定義可以看出來 TypedCacheWrapper
是 ITypedCache 的一個預設實現。
ITypedCache<TKey,TValue>
擁有 ICache
的所有方法簽名,所以使用 ITypedCache<TKey,TValue>
與使用 ICache
的方式是一樣的。
TypedCacheWrapper
的各種方法其實就是調用的傳入的 ICache
對象的方法,只不過在返回值得時候他自己進行了強制類型轉換而已,比如說,看看他的 Get 方法。
public class TypedCacheWrapper<TKey, TValue> : ITypedCache<TKey, TValue>
{
// 返回的是內部 ICache 的名稱
public string Name
{
get { return InternalCache.Name; }
}
public TimeSpan DefaultSlidingExpireTime
{
get { return InternalCache.DefaultSlidingExpireTime; }
set { InternalCache.DefaultSlidingExpireTime = value; }
}
public TimeSpan? DefaultAbsoluteExpireTime
{
get { return InternalCache.DefaultAbsoluteExpireTime; }
set { InternalCache.DefaultAbsoluteExpireTime = value; }
}
// 調用 AsTyped() 方法時候傳入的 ICache 對象
public ICache InternalCache { get; private set; }
public TypedCacheWrapper(ICache internalCache)
{
InternalCache = internalCache;
}
// 調用的是一個 ICache 的擴展方法
public TValue Get(TKey key, Func<TKey, TValue> factory)
{
return InternalCache.Get(key, factory);
}
// ..... 忽略了其他方法
}
看看 InternalCache.Get(key, factory);
這個擴展方法的定義吧:
public static TValue Get<TKey, TValue>(this ICache cache, TKey key, Func<TKey, TValue> factory)
{
// 本質上就是調用的 ICache 的 Get 方法,返回的時候進行了強制類型轉換而已
return (TValue)cache.Get(key.ToString(), (k) => (object)factory(key));
}