[Abp 源碼分析]八、緩存管理


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 集合么,在那里面就直接通過這個 ICacheConfiguratorAction<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));
}

3.點此跳轉到總目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM