## [.NET 筆記 021] FusionCache:L1+L2 混合快取、Fail-safe、Backplane
###### 📅 2026-05-06
---
### 📌 介紹-1. 為什麼需要 FusionCache?
[上一篇 (020)](https://hackmd.io/@dada00321/S1wWC4v0bx) 用 `IDistributedCache` 把資料存進 Redis,每次存取都要經過網路,速度還是有限制
FusionCache 在 `IDistributedCache` 之上多了一層 **L1 Memory 快取**,讓最熱門的資料直接在記憶體裡拿,不需要每次都連 Redis
```
IDistributedCache (020) :
每次請求 → Redis (網路, ~1ms)
FusionCache (021) :
每次請求 → L1 Memory (本機記憶體, ~0.01ms)
→ L2 Redis (網路, ~1ms,L1 沒有才查)
→ 資料庫 (最慢,L2 也沒有才查)
```
---
### 📌 介紹-2. FusionCache 四大核心功能
#### 1. Hybrid Cache (L1 + L2)
同時管理 L1 (Memory) 和 L2 (Redis),自動決定從哪一層取資料
#### 2. Fail-safe (失效保護)
資料庫或 Redis 掛掉時,FusionCache 不直接回錯誤,而是把**過期的舊資料先拿來用**,讓服務維持可用狀態
```
正常情況:
快取過期 → 查資料庫 → 更新快取 → 回傳新資料
Fail-safe 啟動 (資料庫掛掉) :
快取過期 → 查資料庫 → 無回應 → 拿過期的舊資料先回傳
(服務還活著,只是資料稍舊)
```
#### 3. Backplane (多節點同步)
有多台伺服器時,某一台更新了 L1 快取,其他台不會知道,資料會不一致
Backplane 透過 Redis 的 Pub/Sub 機制,讓所有節點的 L1 快取自動同步失效
```
節點 A 更新資料 → 通知 Backplane (Redis Pub/Sub)
→ 節點 B, C 的 L1 快取自動失效
→ 下次讀取時重新載入
```
#### 4. Cache Stampede 保護
同一瞬間大量請求同時發現快取失效,全部都去打資料庫,造成資料庫過載
FusionCache 會讓只有一個請求去查資料庫,其他請求等待結果,避免雪崩
---
### 📌 介紹-3. FusionCache vs IDistributedCache vs 進階範例專案
| | IDistributedCache (020) | FusionCache (021) | 進階範例專案 |
|:--|:----------------------|:-----------------|:-----------|
| L1 Memory | ❌ | ✅ | ✅ |
| L2 Redis | ✅ | ✅ | ✅ |
| Fail-safe | ❌ | ✅ | ✅ |
| Backplane | ❌ | ✅ | ✅ |
| Cache Profiles | ❌ | ✅ (自訂) | ✅ (Static, Default, Realtime, Volatile, and List) |
| Tags (批次失效) | ❌ | ✅ | ✅ |
> 💡 進階範例專案直接使用 FusionCache,020 的 `IDistributedCache` 是作為理解底層的前置知識
---
### 📌 介紹-4. 進階範例專案的 Cache Profiles
進階範例專案定義了 5 種快取設定檔,根據資料的變動頻率選不同的設定:
| Profile | 適用資料 | L1 時間 | L2 時間 | Fail-safe |
|:--------|:--------|:--------|:--------|:----------|
| `Static` | 系統設定、分類 | 10 分鐘 | 30 分鐘 | ✅ 4 小時 |
| `Default` | 商品、會員資料 | 1 分鐘 | 5 分鐘 | ✅ 2 小時 |
| `Realtime` | 餘額、庫存 | 30 秒 | 1 分鐘 | ✅ 30 分鐘 |
| `Volatile` | OTP, 驗證碼 | 15 秒 | 30 秒 | ❌ 不使用 |
| `List` | 分頁查詢結果 | 30 秒 | 2 分鐘 | ✅ 30 分鐘 |
> 💡 `Volatile` 不使用 Fail-safe,確保過期就是真的過期,不會拿舊的驗證碼來用
---
### 📌 套件版本
| 套件 | 版本 | 說明 |
|:----|:----|:----|
| `ZiggyCreatures.FusionCache` | 2.6.0 | FusionCache 核心 |
| `ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis` | 2.6.0 | 用 Redis 做 Backplane |
| `ZiggyCreatures.FusionCache.Serialization.SystemTextJson` | 2.5.0 | 序列化,把物件存入 Redis |
| `StackExchange.Redis` | 已安裝 2.12.14 | Redis 連線驅動,不需重複安裝 |
| `Microsoft.Extensions.Caching.StackExchangeRedis` | 已安裝 8.0.22 | L2 快取後端,不需重複安裝 |
---
### 📌 Step-0. 開啟專案
繼上一篇 [[.NET 筆記 020]](https://hackmd.io/@dada00321/S1wWC4v0bx)
➡️ 專案: API_test_260504
---
### 📌 Step-1. 安裝套件
鑒於範例專案指定 .NET 版本為 8.0,在終端機依序執行:
```
dotnet add package ZiggyCreatures.FusionCache --version 2.6.0
dotnet add package ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis --version 2.6.0
dotnet add package ZiggyCreatures.FusionCache.Serialization.SystemTextJson --version 2.5.0
```
> ℹ️ 安裝完後可在終端機: `dotnet list package` (或: 相依性 > 套件) 確認已經安裝
---
### 📌 Step-2. 更新 CacheSettings
打開 `Settings/CacheSettings.cs`,補上 FusionCache 需要的設定欄位 (仿照進階範例專案) :
```csharp
namespace API_test_260504.Settings
{
public class CacheSettings
{
public const string SectionName = "CacheSettings";
/// <summary>Redis Key 前綴,避免不同專案的 Key 衝突</summary>
public string KeyPrefix { get; set; } = "api_test";
// ── L1 Memory 快取 ─────────────────────────────────────
/// <summary>L1 預設快取時間 (分鐘)</summary>
public int MemoryCacheDurationMinutes { get; set; } = 1;
// ── L2 Redis 快取 ──────────────────────────────────────
/// <summary>L2 預設快取時間 (分鐘),通常比 L1 長</summary>
public int DistributedCacheDurationMinutes { get; set; } = 5;
// ── Fail-safe ──────────────────────────────────────────
/// <summary>Fail-safe 最長可使用舊資料的時間 (小時)</summary>
public double FailSafeMaxDurationHours { get; set; } = 2;
/// <summary>Fail-safe 觸發後的節流時間 (秒),避免頻繁觸發</summary>
public int FailSafeThrottleDurationSeconds { get; set; } = 30;
// ── 斷路器 ────────────────────────────────────────────
/// <summary>Redis 掛掉時暫停嘗試連線的時間 (秒)</summary>
public int DistributedCacheCircuitBreakerDurationSeconds { get; set; } = 10;
// ── 向下相容 (020 筆記的欄位保留) ────────────────────
/// <summary>020 筆記用的欄位,FusionCache 改用 KeyPrefix</summary>
public string InstanceName { get; set; } = "api_test:";
/// <summary>預設絕對過期時間 (分鐘),020 向下相容</summary>
public int DefaultAbsoluteExpirationMinutes { get; set; } = 5;
/// <summary>預設滑動過期時間 (分鐘),020 向下相容</summary>
public int DefaultSlidingExpirationMinutes { get; set; } = 2;
}
}
```
---
### 📌 Step-3. 更新 appsettings.json
在 `appsettings.json` 的 `CacheSettings` 補上新欄位:
```json
"CacheSettings": {
"KeyPrefix": "api_test",
"MemoryCacheDurationMinutes": 1,
"DistributedCacheDurationMinutes": 5,
"FailSafeMaxDurationHours": 2,
"FailSafeThrottleDurationSeconds": 30,
"DistributedCacheCircuitBreakerDurationSeconds": 10,
"InstanceName": "api_test:",
"DefaultAbsoluteExpirationMinutes": 5,
"DefaultSlidingExpirationMinutes": 2
}
```
---
### 📌 Step-4. 建立 Cache Profiles
新增 `Caching/CacheProfiles.cs` (仿照進階範例專案的五種設定檔) :
```csharp
using ZiggyCreatures.Caching.Fusion;
namespace API_test_260504.Caching
{
/// <summary>
/// 不同資料類型的快取設定檔
/// 根據資料的變動頻率選擇不同的設定
/// </summary>
public static class CacheProfiles
{
/// <summary>靜態資料 — 變動極少 (系統設定、分類)</summary>
public static FusionCacheEntryOptions Static => new()
{
Duration = TimeSpan.FromMinutes(10),
DistributedCacheDuration = TimeSpan.FromMinutes(30),
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(4),
FailSafeThrottleDuration = TimeSpan.FromMinutes(1)
};
/// <summary>一般資料 — 中等變動頻率 (商品資訊、裝置列表)</summary>
public static FusionCacheEntryOptions Default => new()
{
Duration = TimeSpan.FromMinutes(1),
DistributedCacheDuration = TimeSpan.FromMinutes(5),
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(2),
FailSafeThrottleDuration = TimeSpan.FromSeconds(30),
FactorySoftTimeout = TimeSpan.FromMilliseconds(500),
FactoryHardTimeout = TimeSpan.FromSeconds(5),
AllowTimedOutFactoryBackgroundCompletion = true
};
/// <summary>即時資料 — 高變動頻率 (庫存、狀態)</summary>
public static FusionCacheEntryOptions Realtime => new()
{
Duration = TimeSpan.FromSeconds(30),
DistributedCacheDuration = TimeSpan.FromMinutes(1),
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromMinutes(30),
FailSafeThrottleDuration = TimeSpan.FromSeconds(10)
};
/// <summary>短暫資料 — 極高變動或敏感資料 (OTP, 驗證碼)</summary>
public static FusionCacheEntryOptions Volatile => new()
{
Duration = TimeSpan.FromSeconds(15),
DistributedCacheDuration = TimeSpan.FromSeconds(30),
IsFailSafeEnabled = false // 敏感資料不使用 Fail-safe
};
/// <summary>列表資料 — 分頁查詢結果</summary>
public static FusionCacheEntryOptions List => new()
{
Duration = TimeSpan.FromSeconds(30),
DistributedCacheDuration = TimeSpan.FromMinutes(2),
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromMinutes(30),
FailSafeThrottleDuration = TimeSpan.FromSeconds(15)
};
}
/// <summary>快取資料類型 Enum,對應 CacheProfiles</summary>
public enum CacheDataType
{
Static,
Default,
Realtime,
Volatile,
List
}
}
```
---
### 📌 Step-5. 更新 ICacheService 介面
打開 `Caching/ICacheService.cs`,改成和進階範例專案一致,加入 `CacheDataType`, `tags`, and `CancellationToken`:
```csharp
namespace API_test_260504.Caching
{
public interface ICacheService
{
/// <summary>取得快取,沒有就執行 factory 查詢並存入快取</summary>
Task<T?> GetOrSetAsync<T>(
string key,
Func<CancellationToken, Task<T?>> factory,
CacheDataType dataType = CacheDataType.Default,
string[]? tags = null,
CancellationToken cancellationToken = default);
/// <summary>直接取得快取</summary>
Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
/// <summary>直接設定快取</summary>
Task SetAsync<T>(
string key,
T value,
CacheDataType dataType = CacheDataType.Default,
string[]? tags = null,
CancellationToken cancellationToken = default);
/// <summary>刪除快取</summary>
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
/// <summary>標記快取為過期 (Fail-safe 仍可使用舊資料)</summary>
Task ExpireAsync(string key, CancellationToken cancellationToken = default);
/// <summary>根據 Tags 批次讓快取失效</summary>
Task RemoveByTagsAsync(string[] tags, CancellationToken cancellationToken = default);
}
}
```
---
### 📌 Step-6. 建立 FusionCacheService
新增 `Caching/FusionCacheService.cs` (取代 020 的 `DistributedCacheService`) :
```csharp
using API_test_260504.Abstractions;
using API_test_260504.Settings;
using Microsoft.Extensions.Options;
using ZiggyCreatures.Caching.Fusion;
namespace API_test_260504.Caching
{
/// <summary>
/// FusionCache 實作
/// 封裝 FusionCache 並提供統一的快取操作介面
/// 仿照進階範例專案的 FusionCacheService 架構
/// </summary>
public sealed class FusionCacheService : ICacheService, IScopedService
{
private readonly IFusionCache _cache;
private readonly CacheSettings _settings;
private readonly ILogger<FusionCacheService> _logger;
public FusionCacheService(
IFusionCache cache,
IOptions<CacheSettings> settings,
ILogger<FusionCacheService> logger)
{
_cache = cache;
_settings = settings.Value;
_logger = logger;
}
public async Task<T?> GetOrSetAsync<T>(
string key,
Func<CancellationToken, Task<T?>> factory,
CacheDataType dataType = CacheDataType.Default,
string[]? tags = null,
CancellationToken cancellationToken = default)
{
var fullKey = BuildKey(key);
var options = GetOptions(dataType);
try
{
return await _cache.GetOrSetAsync<T?>(
key: fullKey,
factory: async (ctx, ct) =>
{
_logger.LogInformation(
"Cache miss: {Key}, HasStaleValue: {HasStale}",
fullKey, ctx.HasStaleValue);
return await factory(ct);
},
options: options,
tags: tags,
token: cancellationToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Cache GetOrSet error for key {Key}", fullKey);
// 快取完全失效時,直接執行 factory 確保服務不中斷
return await factory(cancellationToken);
}
}
public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
{
var fullKey = BuildKey(key);
try
{
var result = await _cache.TryGetAsync<T>(fullKey, token: cancellationToken);
if (result.HasValue)
_logger.LogInformation("Cache hit: {Key}", fullKey);
return result.HasValue ? result.Value : default;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Cache get error for key {Key}", fullKey);
return default;
}
}
public async Task SetAsync<T>(
string key,
T value,
CacheDataType dataType = CacheDataType.Default,
string[]? tags = null,
CancellationToken cancellationToken = default)
{
var fullKey = BuildKey(key);
var options = GetOptions(dataType);
try
{
await _cache.SetAsync(
key: fullKey,
value: value,
options: options,
tags: tags,
token: cancellationToken);
_logger.LogInformation("Cache set: {Key}, Profile: {Profile}", fullKey, dataType);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Cache set error for key {Key}", fullKey);
}
}
public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
{
var fullKey = BuildKey(key);
try
{
await _cache.RemoveAsync(fullKey, token: cancellationToken);
_logger.LogInformation("Cache removed: {Key}", fullKey);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Cache remove error for key {Key}", fullKey);
}
}
public async Task ExpireAsync(string key, CancellationToken cancellationToken = default)
{
var fullKey = BuildKey(key);
try
{
// Expire 標記為過期,但 Fail-safe 仍可使用舊資料
// Remove 則是完全刪除,Fail-safe 也無法使用
await _cache.ExpireAsync(fullKey, token: cancellationToken);
_logger.LogInformation("Cache expired: {Key}", fullKey);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Cache expire error for key {Key}", fullKey);
}
}
public async Task RemoveByTagsAsync(string[] tags, CancellationToken cancellationToken = default)
{
try
{
await _cache.RemoveByTagAsync(tags, token: cancellationToken);
_logger.LogInformation("Cache removed by tags: {Tags}", string.Join(", ", tags));
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Cache remove by tags error: {Tags}", string.Join(", ", tags));
}
}
// 仿照進階範例專案:前綴由 Service 層統一管理
private string BuildKey(string key) =>
string.IsNullOrWhiteSpace(_settings.KeyPrefix)
? key
: $"{_settings.KeyPrefix}:{key}";
private static FusionCacheEntryOptions GetOptions(CacheDataType dataType) => dataType switch
{
CacheDataType.Static => CacheProfiles.Static,
CacheDataType.Realtime => CacheProfiles.Realtime,
CacheDataType.Volatile => CacheProfiles.Volatile,
CacheDataType.List => CacheProfiles.List,
_ => CacheProfiles.Default
};
}
}
```
---
### 📌 Step-7. 建立 FusionCacheExtensions
新增 `Caching/FusionCacheExtensions.cs`,把 FusionCache 的 DI 註冊邏輯包成擴充方法 (仿照進階範例專案) :
```csharp
using API_test_260504.Settings;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using StackExchange.Redis;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;
namespace API_test_260504.Caching
{
public static class FusionCacheExtensions
{
/// <summary>
/// 註冊 FusionCache 服務
/// 包含 L1 (Memory) + L2 (Redis) + Backplane
/// </summary>
public static IServiceCollection AddFusionCacheServices(
this IServiceCollection services,
IConfiguration configuration)
{
var cacheSettings = configuration
.GetSection(CacheSettings.SectionName)
.Get<CacheSettings>() ?? new CacheSettings();
// 1. 綁定 CacheSettings
services.Configure<CacheSettings>(
configuration.GetSection(CacheSettings.SectionName));
// 2. 建立 Redis 連線 (Singleton,全程共用一個連線)
services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var logger = sp.GetRequiredService<ILogger<IConnectionMultiplexer>>();
var redisConn = configuration.GetConnectionString("Redis") ?? "localhost:6379";
var options = ConfigurationOptions.Parse(redisConn);
options.AbortOnConnectFail = false; // 連線失敗不立即中止,讓 Fail-safe 接管
options.ConnectRetry = 3;
options.ConnectTimeout = 5000;
var multiplexer = ConnectionMultiplexer.Connect(options);
// 訂閱連線事件,記錄 Log
multiplexer.ConnectionFailed += (_, args) =>
logger.LogWarning("Redis connection failed: {EndPoint}, {FailureType}",
args.EndPoint, args.FailureType);
multiplexer.ConnectionRestored += (_, args) =>
logger.LogInformation("Redis connection restored: {EndPoint}", args.EndPoint);
return multiplexer;
});
// 3. 註冊 FusionCache (L1 + L2 + Backplane)
services.AddFusionCache()
.WithDefaultEntryOptions(options =>
{
// 預設使用 Default Profile 的設定
options.Duration =
TimeSpan.FromMinutes(cacheSettings.MemoryCacheDurationMinutes);
options.DistributedCacheDuration =
TimeSpan.FromMinutes(cacheSettings.DistributedCacheDurationMinutes);
options.IsFailSafeEnabled = true;
options.FailSafeMaxDuration =
TimeSpan.FromHours(cacheSettings.FailSafeMaxDurationHours);
options.FailSafeThrottleDuration =
TimeSpan.FromSeconds(cacheSettings.FailSafeThrottleDurationSeconds);
})
// JSON 序列化,把物件存進 Redis
.WithSerializer(new FusionCacheSystemTextJsonSerializer(
new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
}))
// L2:Redis 分散式快取
.WithDistributedCache(sp =>
{
var multiplexer = sp.GetRequiredService<IConnectionMultiplexer>();
return new RedisCache(new RedisCacheOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(multiplexer)
});
})
// Backplane:多節點 L1 同步
.WithBackplane(sp =>
{
var multiplexer = sp.GetRequiredService<IConnectionMultiplexer>();
return new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(multiplexer)
});
});
return services;
}
}
}
```
---
### 📌 Step-8. 更新 Program.cs
移除 020 的 `AddStackExchangeRedisCache`,改用 FusionCache:
```csharp
using API_test_260504.Caching;
// [.NET 筆記 021] FusionCache 取代 020 的 IDistributedCache 設定
// 020 的設定已被取代,改在 AddFusionCacheServices 裡統一處理
// [已移除] builder.Services.Configure<CacheSettings>(...);
// [已移除] builder.Services.AddStackExchangeRedisCache(...);
services.AddFusionCacheServices(builder.Configuration);
```
完整的 Program.cs 修改段落位置 (放在 Scrutor 掃描之前) :
```csharp
// [.NET 筆記 021] FusionCache:L1 + L2 (Redis) + Backplane
builder.Services.AddFusionCacheServices(builder.Configuration);
// Scrutor 自動掃描
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(c => c.AssignableTo<IScopedService>())
.AsImplementedInterfaces()
.AsSelf()
.WithScopedLifetime());
```
> 💡 `FusionCacheService` 掛上了 `IScopedService`,Scrutor 會自動登記,不需要額外寫 `AddScoped`
---
### 📌 Step-9. 更新 DeviceService
`ICacheService` 的 `GetOrSetAsync` 簽名改了 (新增 `CancellationToken` 參數),需要更新呼叫方式:
```csharp
public async Task<List<DeviceDto>> GetAllAsync()
{
return await _cache.GetOrSetAsync(
key: AllDevicesCacheKey,
factory: async ct =>
{
_logger.LogInformation("Cache miss,查詢資料庫取得所有裝置");
using var conn = _db.CreateConnection();
var devices = await conn.QueryAsync<Device>("SELECT * FROM Devices");
return devices.Select(d => d.Adapt<DeviceDto>()).ToList();
},
dataType: CacheDataType.List) // 指定 Cache Profile
?? [];
}
public async Task<DeviceDto?> GetByIdAsync(int deviceId)
{
return await _cache.GetOrSetAsync(
key: DeviceCacheKey(deviceId),
factory: async ct =>
{
using var conn = _db.CreateConnection();
var device = await conn.QueryFirstOrDefaultAsync<Device>(
"SELECT * FROM Devices WHERE Id = @Id", new { Id = deviceId });
return device?.Adapt<DeviceDto>();
},
dataType: CacheDataType.Default); // 指定 Cache Profile
}
```
再修改 Log 對應的 CacheService 介面 (下面兩處)
(1) 將 `private readonly ILogger<DistributedCacheService> _logger;`
改為 `private readonly ILogger<FusionCacheService> _logger;`
(2) 將 `ILogger<DistributedCacheService> logger,`
改為 `ILogger<FusionCacheService> logger,`
---
### 📌 Step-10. 驗證結果
確認 Redis 已啟動,執行 `https`,用 Swagger 測試:
- **第一次** `GET /api/Device`
- ➡️ Result

- ➡️ Explanation
124
- **第二次 (1 分鐘內,L1 快取有效)** `GET /api/Device`
- ➡️ Result

- ➡️ Explanation
資料從 L1 Memory 拿,不走網路,速度最快
- **第 3 次及以後 (超過 1 分鐘後)** `GET /api/Device` (L1 過期,L2 還有效)
- 
- ➡️ Explanation
FusionCache 自動去 L2 (Redis) 查,並重新暖起 L1,無需開發者手動處理
- **用 redis-cli 驗證 Key**:
➡️ Result
- 
- 補充:[FusionCache Log 解讀:三次呼叫的完整流程](https://hackmd.io/@dada00321/ryABfYu0Wx)
---
### 📌 總結:020 vs 021 對比
| | IDistributedCache (020) | FusionCache (021) |
|:--|:----------------------|:-----------------|
| 快取層級 | 只有 L2 Redis | L1 Memory + L2 Redis |
| Fail-safe | ❌ | ✅ |
| Backplane | ❌ | ✅ |
| Cache Profile | ❌ (只有固定過期時間) | ✅ (Static, Default, Realtime, Volatile, List) |
| Tags 批次失效 | ❌ | ✅ |
| 使用難度 | 較低 | 較高 |
> 💡 020 的 `DistributedCacheService` 和 `ICacheService` 介面已被 `FusionCacheService` 取代,但架構概念相同 (都有 `BuildKey`, `GetOrSetAsync`, `RemoveAsync`)
> 進階範例專案還有 `FusionCacheAlertingService` (告警服務) 和 Cache Tags 的進階應用,這些屬於進階用法,熟悉基本操作後可以參考進階範例專案的實作