FusionCache 快取套件介紹

近期同事介紹了 FusionCache 這個快取套件,試玩了一下發現裡面的功能非常完整,說明文件也很詳細。FusionCache 解決了很多使用多層快取時會遇到的問題,也比微軟在 .NET 9 新出的 HybridCache 完整和好用很多。

FusionCache 除了提供基本的 in-memory 快取之外,還可以疊加更多層次的快取,例如 Redis, Memcached 等。還有提供各種 timeout 的處理、快取同步、重新連線、熔斷等功能,減少了各種連線問題的處理。

FusionCache 的 GitHub:
GitHub - ZiggyCreatures/FusionCache: FusionCache is an easy to use, fast and robust hybrid cache with advanced resiliency features.

這篇「Step by Step」的官方說明簡潔有力,直接說明了各功能的用途和效果,還提供了一些維持服務穩定的調整方向:
FusionCache/docs/StepByStep.md at main · ZiggyCreatures/FusionCache · GitHub

這裡就挑「Step by Step」裡幾個有趣的功能簡單說明。

基本使用

只要安裝 FusionCache 的套件,裡面實作了 IMemoryCache,使用 in-memory 的方式在本機建立快取。只要 DI 註冊、調整資料存取相關程式就能馬上使用,相當方便。

必裝套件:

  • ZiggyCreatures.FusionCache

直接使用

1
services.AddFusionCache();

自訂快取有效期

設定說明:

  • Duration: In-memory cache 資料的有效期。
1
2
3
4
5
6
services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions {
// In-memory cache 的到期時間
Duration = TimeSpan.FromMinutes(1)
})
;

調整資料存取的程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
public class ProductRepository : IProductRepository
{
private readonly EcShopContext _context;
private readonly IFusionCache _fusionCache;
private readonly ILogger _logger;

public ProductRepository(EcShopContext context, IFusionCache fusionCache, ILoggerFactory loggerFactory)
{
_context = context;
_fusionCache = fusionCache;
_logger = loggerFactory.CreateLogger<ProductRepository>();
}

private string GetCacheKey(string prefix, string identifier = "")
{
return $"{prefix}:{identifier}";
}

// Create
public async Task<Product> CreateAsync(Product product)
{
_context.Product.Add(product);
await _context.SaveChangesAsync();

// 建立快取
await _fusionCache.SetAsync(
GetCacheKey("Product", product.Id.ToString()),
product,
options =>
{
// 客製化設定,用來方便測試,實務上並不會這樣設定,請勿照抄。
// 這裡的設定只會針對這筆資料,會覆蓋 DI 內的設定。
options.DistributedCacheDuration = TimeSpan.FromMinutes(10);
options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10);
options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500);
options.Duration = TimeSpan.FromSeconds(5);
}
);

return product;
}

// Read - By Id
public async Task<Product> GetByIdAsync(int id)
{
var product = await _fusionCache.GetOrSetAsync(
GetCacheKey("Product", id.ToString()),
async ct => // 這段就是 factory,先記得
{
_logger.LogError("Getting product {id} from the database.", id);
return await _context.Product.FindAsync(id, ct);
},
options =>
{
// 客製化設定,用來方便測試,實務上並不會這樣設定,請勿照抄。
// 這裡的設定只會針對這筆資料,會覆蓋 DI 內的設定。
options.DistributedCacheDuration = TimeSpan.FromMinutes(10);
options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10);
options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500);
options.Duration = TimeSpan.FromSeconds(5);
}
);

return product;
}

// Update
public async Task<bool> UpdateAsync(Product product)
{
var existing = await _context.Product.FindAsync(product.Id);
if (existing == null)
return false;

_context.Entry(existing).CurrentValues.SetValues(product);
await _context.SaveChangesAsync();

// 更新相關快取
await _fusionCache.SetAsync(
GetCacheKey("Product", product.Id.ToString()),
product,
options =>
{
// 客製化設定,用來方便測試,實務上並不會這樣設定,請勿照抄。
// 這裡的設定只會針對這筆資料,會覆蓋 DI 內的設定。
options.DistributedCacheDuration = TimeSpan.FromMinutes(10);
options.DistributedCacheSoftTimeout = TimeSpan.FromMilliseconds(10);
options.DistributedCacheHardTimeout = TimeSpan.FromMilliseconds(500);
options.Duration = TimeSpan.FromSeconds(5);
}
);

return true;
}

// Delete
public async Task<bool> DeleteAsync(int id)
{
var product = await _context.Product.FindAsync(id);
if (product == null) return false;

_context.Product.Remove(product);
await _context.SaveChangesAsync();

// 刪除相關快取
await _fusionCache.RemoveAsync(GetCacheKey("Product", id.ToString()));

return true;
}

Fail-Safe

當 factory (也就是 factory 裡面的實作,存取資料庫) 壞掉時,就會觸發這裡。當資料庫發生錯誤時,仍然可以持續提供服務,會去讀取已經過期的快取資料。搭配 Factory Timeouts,效果更佳,縮短 timeouts 時間,儘早回傳資料。

設定說明:

  • IsFailSafeEnabled: 是否啟用。
  • FailSafeMaxDuration: 一個已經過期的快取資料,最多還可以被使用多久(從原本過期時間開始算)。
  • FailSafeThrottleDuration: 每次有請求來時,會讓這筆過期資料在這段時間內暫時當作還沒過期,可以繼續讀取,避免每次都嘗試連資料庫。
1
2
3
4
5
6
7
8
9
10
services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions {
Duration = TimeSpan.FromMinutes(1),

// Fail-Safe 的設定
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(2),
FailSafeThrottleDuration = TimeSpan.FromSeconds(30)
})
;

Factory Timeouts

當遇到 factory (也就是 factory 裡面的實作,存取資料庫) 的回傳時間比較久,甚至等滿了 timeout 時間才回傳資料庫壞掉。這時,使用 Fatory Timeouts 就可以設定較短的時間,縮短 timeout,繼續下一步進入到 Fail-Safe

設定說明:

  • FactorySoftTimeout: 軟性 timeout。超過這個時間後,FusionCache 會先放棄等待結果,但 factory 還是會在背景繼續執行。若在 hard timeout 之前能收到資料,還是會在背景建立快取。
  • FactoryHardTimeout: 硬性 timeout。超過這個時間後,FusionCache 會強制取消執行(例如透過 CancellationToken)。

流程:

  • 先到 soft timeout 時 → FusionCache 嘗試用 fail-safe 或回傳 null,但 factory 還會繼續執行(可補快取)。
  • 再到 hard timeout 時 → 就會真的取消 factory 的執行。
1
2
3
4
5
6
7
8
9
10
11
12
13
services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions {
Duration = TimeSpan.FromMinutes(1),

IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(2),
FailSafeThrottleDuration = TimeSpan.FromSeconds(30),

// Factory timeout 的設定
FactorySoftTimeout = TimeSpan.FromMilliseconds(100),
FactoryHardTimeout = TimeSpan.FromMilliseconds(1500)
})
;

Distributed cache

單純使用 FusionCache 的話,只會使用 in-memory 的快取 (實作 IMemoryCache),多節點的話還是需要個分散式快取。FusionCache 實作了 IDistributedCache,所以可以使用各種服務來儲存 (Redis, Memcached, MongoDB 等)。這裡以 Redis 作為範例。

IMemoryCacheIDistributedCache 可以看這裡:

使用 Distributed cache

必裝套件:

  • Microsoft.Extensions.Caching.StackExchangeRedis

擇一安裝:

  • ZiggyCreatures.FusionCache.Serialization.SystemTextJson
  • ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson
  • ZiggyCreatures.FusionCache.Serialization.NeueccMessagePack

上面的序列化套件依需求擇一安裝。這裡選 MessagePack,因為資料佔用空間小。除了上面那三種,還有這些(要搭配支援的分散式快取服務):

  • ZiggyCreatures.FusionCache.Serialization.CysharpMemoryPack
  • ZiggyCreatures.FusionCache.Serialization.ServiceStackJson
  • ZiggyCreatures.FusionCache.Serialization.ProtoBufNet
1
2
3
4
5
6
7
8
9
10
11
12
13
services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions {
Duration = TimeSpan.FromMinutes(1),
})
// 使用 Message Pack 作為序列化工具
.WithSerializer(
new FusionCacheNeueccMessagePackSerializer()
)
// 使用 Redis 作為分散式快取的服務
.WithDistributedCache(
new RedisCache(new RedisCacheOptions() { Configuration = "CONNECTION STRING" })
)
;

Distributed cache 的設定

設定說明:

  • DistributedCacheCircuitBreakerDuration: 斷路器的持續時間。如果分散式快取出現硬性錯誤 (如壞掉拋例外),就會觸發啟動斷路器,在指定的時間內停止使用分散式快取。
  • DistributedCacheSoftTimeout: 軟性 timeout。超過這個時間後,FusionCache 會先放棄等待結果,進入下一步,但還是會在背景繼續執行。
  • DistributedCacheHardTimeout: 硬性 timeout。超過這時間後,會直接取消分散式快取操作(例如用 CancellationToken)。
  • AllowBackgroundDistributedCacheOperations: 允許在背景操作分散式快取,加快回應速度。讀取不會有影響,新增、修改、刪除才會影響。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
services.AddFusionCache()
.WithOptions(options => {
// 分散式快取斷路器的設定
options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2);
})
.WithDefaultEntryOptions(new FusionCacheEntryOptions {
Duration = TimeSpan.FromMinutes(1),

// 分散式快取的設定
DistributedCacheSoftTimeout = TimeSpan.FromSeconds(1),
DistributedCacheHardTimeout = TimeSpan.FromSeconds(2),
AllowBackgroundDistributedCacheOperations = true
})
.WithSerializer(
new FusionCacheNeueccMessagePackSerializer()
)
.WithDistributedCache(
new RedisCache(new RedisCacheOptions() { Configuration = "CONNECTION STRING" })
)
;

Backplane

有了分散式快取,多節點都可以使用了,但是要處理同步的問題。當 A 節點更新資料後,A 自己的記憶體快取更新了,可是其他節點不知道資料已經修改,各節點裡面的記憶體快取依舊是舊資料,這時就產生資料不一致的問題。所以需要主動通知其他節點要更新記憶體快取內的資料,不用等到快取記憶體過期才更新,加快資料更新的時間,消除資料不一致的問題。Backplane 就是在處理這件事的功能。

這裡以使用 Redis 為例,FusionCache 背後的實作是利用 Redis 的 Pub/Sub 功能來達成。

必裝套件:

  • ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
services.AddFusionCache()
.WithOptions(options => {
options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2);
})
.WithDefaultEntryOptions(new FusionCacheEntryOptions {
Duration = TimeSpan.FromMinutes(1),

DistributedCacheSoftTimeout = TimeSpan.FromSeconds(1),
DistributedCacheHardTimeout = TimeSpan.FromSeconds(2),
AllowBackgroundDistributedCacheOperations = true,
})
.WithSerializer(
new FusionCacheNeueccMessagePackSerializer()
)
.WithDistributedCache(
new RedisCache(new RedisCacheOptions() { Configuration = "CONNECTION STRING" })
)
// 使用 Redis 作為 Backplane
.WithBackplane(
new RedisBackplane(new RedisBackplaneOptions() { Configuration = "CONNECTION STRING" })
)
;

最後

除了上述的功能之外,還有功能和套件的使用概念在官方文件中都有寫到,建議去裡面看看。對於快取的概念,其實也可以從裡面學到很多各種問題的處理方式。原本以為裡面的連線問題或重試會使用 Polly,結果並沒有使用,看來可以再仔細研究裡面的做法。

參考

延伸閱讀