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

来源:https://www.cnblogs.com/myzony/archive/2018/07/27/9377492.html
-Advertisement-
Play Games

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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ...
  • 1.今天在寫12306查詢餘票時,想給定字典(dict)的值,從而得到字典(dict)的鍵,但好像字典(dict)方法中沒有與此相關的方法,只能退而求其次,反轉字典(dict),將原字典(dict)的鍵作為值,值值作為鍵。 下附12306的車站與其對應的簡稱: {'北京北': 'VAP', '北京東 ...
  • 眾所周知,電腦底層是二進位。而java作為一門電腦編程語言,也對二進位的位運算提供了完整的支持。 在java中,int是32位的,也就是說可以用來實現32位的位運算。方便起見,我們一般用16進位對它賦值,比如: 0011表示成16進位是 0x3, 110111表示成16進位是 0x37。 那麼什 ...
  • 最近把我之前學SpringCloud所涉及到的知識以及我寫的博客進行了比較系統的整理,目錄如下。 1. 單體架構和微服務架構的比較 2.微服務所要解決的主要問題 3.SOA和微服務的比較 初識微服務 4. 服務拆分 微服務的服務拆分 5. 數據一致性 微服務的數據一致性 6.服務間的通信—RestT ...
  • Given an array of integers, return indices of the two numbers such that they add up to a specific target. You may assume that each input would have ex ...
  • 最近有童鞋有這種需求,說實話我不知道這個Panel怎麼起名字。 效果連接https://tuchong.com/tags/風光/ 下麵是我做成的效果,可以規定每個Row的Items個數 2個 3個 4個 代碼在:GitHub 下麵我來說一下我的思路 其實很早之前就寫過這種可變大小的控制項,但這次的跟這 ...
  • .NET 性能優化小技巧 Intro 之前做了簡訊發送速度的提升,在大師的指導下,發送簡訊的速度有了極大的提升,學到了一些提升 .NET 性能的一些小技巧 HttpClient 優化 關於使用 ,大概很多人都知道儘量使用單例以提升 的性能。 由於 在發送請求時需要進行功能變數名稱解析,使用的時候第一次一般來 ...
  • 對稱加密(向量) DES加密 倒序加1加密解密 Base64編解碼 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...