ABP

2023-06-02 15:00:43

1. 與 .NET Core 快取的關係和差異

ABP 框架中的快取系統核心包是 Volo.Abp.Caching ,而對於分散式快取的支援,abp 官方提供了基於 Redis 的方案,需要安裝 Volo.Abp.Caching.StackExchangeRedis 整合包。預設的情況下,在我們使用 ABP CLI 建立 ABP 框架模板專案的時候已經整合了這個包,我們不需要手動進行安裝。

ABP 框架中的快取系統在 ASP.NET Core的分散式快取系統上進行擴充套件,從而為了使分散式快取使用起來更加方便。理所當然的,ABP 框架的快取系統相容 ASP.NET Core 原生的分散式快取使用方式,關於 ASP.NET Core 分散式快取的使用,可以參考我之前的文章 ASP.NET Core - 快取之分散式快取,也可以看下官方檔案 ASP.NET Core 中的分散式快取

那麼 ABP 框架中的快取系統擴充套件了什麼呢?ASP.NET Core 的分散式快取又有哪些不便之處呢 ?ASP.NET Core 通過 IDistributedCache 抽象了分散式快取的存取操作,但是有這個兩個問題:

  • 它對快取值的存取是基於 byte 陣列的,而不是物件。

    這使得我們在使用分散式快取的時候需要先將範例物件進行序列化/反序列化,之後再轉碼為 byte 陣列。如果每個使用分散式快取的地方都這麼做將會有很多的重複冗餘的程式碼,這是需要抽取封裝的。

  • 為了能實現多個應用公用快取,達成分散式快取的左右,它將所有的快取項存放在同一個 Key 池之中。

    這就可能會有問題,我們要特別注意快取鍵的設定,如果開發人員不注意,很可能將一些快取相互覆蓋了,特別是共用快取的應用比較多,或者多租戶的情況下,這造成的問題可能很嚴重,而且很難排查。

ABP 框架的快取系統擴充套件了通用的泛型介面 IDistributedCache<TCacheItem> ,泛型型別就是快取值的型別,用於解決上面提到的問題:

  • 該介面內部實現了對快取物件 序列化/反序列化 的邏輯。 預設使用 JSON 序列化, 我們如果有需要可以替換 依賴注入 系統中 IDistributedCacheSerializer 服務的實現來覆蓋預設的方式。

  • 該介面會根據快取中物件型別自動向快取key新增 快取名稱 字首。 預設快取名是快取物件的全類名(如果類名以CacheItem 結尾, 那麼CacheItem 會被忽略,不應用到快取名稱上),開發人員也可以在快取類上使用 CacheName 特性 設定快取的名稱.

  • 如果是多租戶應用的話,它會自動將當前的租戶id新增到快取鍵中, 以區分不同租戶的快取項 。 如果租戶之間需要共用快取物件, 我們可以在快取類打上 IgnoreMultiTenancy 特性,宣告當前快取不區分組合。

  • 允許為每個應用程式定義 全域性快取鍵字首 , 不同的應用程式可以在共用的分散式快取中擁有自己的隔離池.

  • 它提供了 錯誤容忍 機制,對分散式快取存取過程中的異常進行了處理,例如分散式快取服務連線失敗等,避免因快取問題導致應用出錯。
    因為快取本身就只是一種用於提升應用效能的策略,如果因為分散式快取的問題導致應用出錯,業務邏輯無法繼續執行,那就得不償失了。快取系統應該是,就算將其從應用中摘除,也不影響應用業務邏輯正常執行的東西,只是效能可能差了點。

  • 它額外提供了 GetManyAsync 和 SetManyAsync 等方法, 支援對快取的批次操作,可以顯著提高批次處理的效能。

2. Abp 快取的使用

這裡還是以控制檯應用作為演示,Web 應用中使用比控制檯更簡單,因為 Web 應用中已經整合了快取模組,不需要再自行引入。首先,通過以下命令生成一個 ABP 的控制檯啟動模板:

abp new AbpCacheSample -t console

之後,如果是使用基於記憶體的分散式快取的話,只需要安裝 Volo.Abp.Caching 包即可。

Install-package  Volo.Abp.Caching

之後,在專案的模組檔案中新增模組依賴:

這裡不需要再進行依賴關係的設定,因為在 AbpCacheModule模組的初始化之中已經設定好了相關的內容,通過原始碼可以看到:

當然,你也可以更高效地在專案所在的資料夾使用以下命令,省掉對模組依賴的設定。

abp add-package  Volo.Abp.Caching

如果是使用 Redis 分散式快取的話,需要安裝 Volo.Abp.Caching.StackExchangeRedis 整合包,這個包對 Microsoft.Extensions.Caching.StackExchangeRedis 進行了擴充套件,它簡化了 Redis 快取的設定,也是前面提到的 GetManyAsync 和SetManyAsync 等更加效能的方法的實現所在。

install-package Volo.Abp.Caching.StackExchangeRedis

同時,模組依賴改成以下這樣:

同樣的,模組初始化的時候也已經設定好 Redis 快取使用的內容:

通過原始碼我們也可以看到,Redis 相應的設定是從組態檔中的 "Redis" 節點讀取的,我們可以在appsettings.json 中新增以下內容啟用 Redis 快取:

{
  "Redis": {
	"IsEnabed": true, // 控制 Redis 分散式快取是否啟用
	"Configuration": "xxx.xxx.xxx.xxx:6379,password=123456" // Redis 連線字串
  }
}

這裡其實就是通過設定資訊中的 "Redis:Configuration" 節點設定 RedisCacheOpions 選項,這個選項是微軟標準的 Redi s快取支援包中的,如果有需要,我們可以在程式碼中通過以下方式對該選項進行設定:

[DependsOn(
	typeof(AbpAutofacModule),
	typeof(AbpCachingStackExchangeRedisModule)
)]
public class AbpCacheSampleModule : AbpModule
{
	public override void ConfigureServices(ServiceConfigurationContext context)
	{
		Configure<RedisCacheOptions>(option =>
		{
			// ...
		});
	}
}

2.1 常規使用

首先我們定義快取類:

/// <summary>
/// 快取中,會以快取類的全類名作為建,如果類名以 CacheItem 結尾,CacheItem 會被忽略
/// </summary>
// [CacheName("DateTime")] 也可以通過 CacheName 特性設定快取鍵
public class DateTimeCacheItem
{
	public DateTime Now { get; set; }

	public string Name { get; set; }
}

之後,只需要在要用到的服務中注入 IDistributedCache 泛型介面即可:

public class HelloWorldService : ITransientDependency
{
	public const string CacheKey = nameof(HelloWorldService);
	private readonly IDistributedCache<DateTimeCacheItem> _distributedCache;

	public HelloWorldService(IDistributedCache<DateTimeCacheItem> distributedCache)
	{
		_distributedCache = distributedCache;
	}

	public async Task SayHelloAsync()
	{
		// 常規的 IDisctributedCache 的同名方法,不過 ABP 框架中進行了擴充套件
		// 使其支援泛型,可以直接存取物件
		//var cacheValue = await _distributedCache.GetAsync(CacheKey);
		//if (cacheValue == null)
		//{
		//    cacheValue = new DateTimeCacheItem
		//    {
		//        Name = CacheKey,
		//        Now = DateTime.Now,
		//    };
		//    await _distributedCache.SetAsync(
		//        CacheKey, // 快取鍵,最終的鍵會是 快取名稱(快取類全類名去除CacheItem,或CacheName特性設定的名稱) + 這裡的鍵
		//        cacheValue, // 直接存取物件,而不用自己序列化/反序列化 以及轉碼 
		//        new DistributedCacheEntryOptions // 一樣可以通過選項設定快取策略
		//    {
		//        SlidingExpiration = TimeSpan.FromMinutes(1)
		//    });
		//}

		// ABP 框架新增的方法
		var cacheValue = await _distributedCache.GetOrAddAsync(
			CacheKey,
			async () =>
			{
				return await Task.FromResult(new DateTimeCacheItem()
				{
					Name = CacheKey,
					Now = DateTime.Now
				});
			},
			() => new DistributedCacheEntryOptions
			{
				SlidingExpiration= TimeSpan.FromMinutes(1)
			});

		Console.WriteLine(JsonSerializer.Serialize(cacheValue));
		Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
	}
}

2.2 非字串型別的 Key

除了可以使用 string 型別作為快取鍵之外,我們還可以通過 IDistributedCache<CacheKey, CacheItem> 介面使用其他型別,甚至複雜型別作為快取鍵,使用複雜型別作為快取鍵的時候需要重寫 ToString() 方法。

public class MyCacheKey
	{
		public int Id { get; set; }

		public string Name { get; set; }

		public override string ToString()
		{
			return Id + Name;
		}
	}

public class HelloWorldService : ITransientDependency
{
	private readonly IDistributedCache<DateTimeCacheItem, int> _distributedCacheKeyInt;
	private readonly IDistributedCache<DateTimeCacheItem, MyCacheKey> _distiributedCacheKey;

	public HelloWorldService(
		IDistributedCache<DateTimeCacheItem, int> distributedCacheKeyInt,
		IDistributedCache<DateTimeCacheItem, MyCacheKey> distiributedCacheKey)
	{
		_distributedCache = distributedCache;
	}

	public async Task SayHelloAsync()
	{
		_ = await _distributedCacheKeyInt.GetOrAddAsync(
			1,
			async () =>
			{
				return await Task.FromResult(new DateTimeCacheItem
				{
					Name = "1",
					Now = DateTime.Now
				});
			},
			() => new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(10) }
			);

		_ = await _distiributedCacheKey.GetOrAddAsync(
			new MyCacheKey { Id = 1, Name = "MyKey" },
			async () =>
			{
				return await Task.FromResult(new DateTimeCacheItem
				{
					Name = "1",
					Now = DateTime.Now
				});
			},
			() => new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(10) }
			);
	}
}

2.3 批次操作

批次進行快取存取操作的方式如下:

_ = await _distributedCache.GetOrAddManyAsync(new List<string> { "Key1", "Key2" }, async keys =>
{
	return await Task.FromResult(
		keys.Select(k => new KeyValuePair<string, DateTimeCacheItem>(k, new DateTimeCacheItem { Name = k, Now = DateTime.Now }))
		.ToList()
		);
},
() => new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(10) });

ABP的分散式快取介面定義了以下批次操作方法,當你需要在一個方法中呼叫多次快取操作時,這些方法可以提高效能

  • SetManyAsync 和 SetMany 方法可以用來向快取中設定多個值.
  • GetManyAsync 和 GetMany 方法可以用來從快取中獲取多個值.
  • GetOrAddManyAsync 和 GetOrAddMany 方法可以用來從快取中獲取並新增缺少的值.
  • RefreshManyAsync 和 RefreshMany 方法可以來用重置多個值的捲動過期時間.
  • RemoveManyAsync 和 RemoveMany 方法可以用來從快取中刪除多個值.

這些不是標準的ASP.NET Core快取方法, 所以其他的分散式快取方案可能不支援。而 ABP 通過 Volo.Abp.Caching.StackExchangeRedis 實現了 Redis 分散式快取下的批次操作。如果採用了其他方案實現分散式快取,而提供程式不支援的情況下,會回退到迴圈呼叫 SetAsync 和 GetAsync 方法。

以上就是 ABP 框架中快取系統的基本使用。除此之外,ABP 框架提供了 AbpDistributedCacheOptions 選項用於設定一些快取策略,可用屬性:

  • HideErrors (bool, 預設: true): 啟用/禁用隱藏從快取伺服器寫入/讀取值時的錯誤.
  • KeyPrefix (string, 預設: null): 如果你的快取伺服器由多個應用程式共同使用, 則可以為應用程式的快取鍵設定一個字首. 在這種情況下, 不同的應用程式不能覆蓋彼此的快取內容.
  • GlobalCacheEntryOptions (DistributedCacheEntryOptions): 用於設定儲存緩內容卻沒有指定選項時, 預設的分散式快取選項 (例如 AbsoluteExpiration 和 SlidingExpiration). SlidingExpiration的預設值設定為20分鐘.

3. 額外功能

另外,還有ABP 框架下的快取系統還有以下一些功能:

  • 錯誤處理

    當為你的物件設計快取時, 通常會首先嚐試從快取中獲取值. 如果在快取中找不到該值, 則從來源查詢物件. 它可能在資料庫中, 或者可能需要通過HTTP呼叫遠端伺服器.

    在大多數情況下, 你希望容忍快取錯誤; 如果快取伺服器出現錯誤, 也不希望取消該操作. 相反, 你可以默默地隱藏(並記錄)錯誤並從來源查詢. 這就是ABP框架預設的功能.

    ABP的分散式快取例外處理, 預設記錄並隱藏錯誤,有一個全域性修改該功能的選項.所有的IDistributedCache (和 IDistributedCache<TCacheItem, TCacheKey>)方法都有一個可選的引數hideErrors, 預設值為null. 如果此引數設定為null, 則全域性生效, 否則你可以選擇單個方法呼叫時隱藏或者丟擲異常.

  • 工作單元級別的快取

    分散式快取服務提供了一個有趣的功能. 假設你已經更新了資料庫中某本書的價格, 然後將新價格設定到快取中, 以便以後使用快取的值. 如果設定快取後出現異常, 並且更新圖書價格的事務被回滾了, 該怎麼辦?在這種情況下, 快取值是錯誤的.

    IDistributedCache<..>方法提供一個可選引數, considerUow, 預設為false. 如果將其設定為true, 則你對快取所做的更改不會應用於真正的快取儲存, 而是與當前的工作單元關聯. 你將獲得在同一工作單元中設定的快取值, 但僅當前工作單元成功時更改才會生效.

  • 可替換的鍵、值處理方式

    • IDistributedCacheSerializer

      IDistributedCacheSerializer 服務用於序列化和反序列化快取內容. 預設實現是 Utf8JsonDistributedCacheSerializer 類, 它使用 IJsonSerializer 服務將物件轉換為 JSON, 反之亦然. 然後, 它使用 UTC8 編碼將 JSON 字串轉換為分散式快取接受的位元組陣列.

      如果你想實現自己的序列化邏輯, 可以自己實現並替換此服務.

    • IDistributedCacheKeyNormalizer

      預設情況下, IDistributedCacheKeyNormalizer是由DistributedCacheKeyNormalizer類實現的. 它將快取名稱、應用程式快取字首和當前租戶id新增到快取鍵中. 如果需要更高階的鍵規範化, 可以自己實現並替換此服務.



參考文章:
ABP 官方檔案 - 快取



ABP 系列總結:
目錄:ABP 系列總結
上一篇:ABP - 依賴注入(2)