在 Web 系統中,有些資料會被頻繁查詢,但本身並不會頻繁變動。IMemoryCache
一段時間內只從資料庫讀取一次完整資料,後續查詢直接從 Web Server 記憶體取得。
在 Web 系統中,有些資料會被頻繁查詢,但本身並不會頻繁變動。
例如:
- 使用者清單
- 部門資料
- 系統設定
- 權限選項
- 下拉選單資料
- 國家、地區、廠區
- 商品分類
- 組織架構
- 基礎主檔
假設系統有 5,000 名員工,使用者每輸入一次關鍵字搜尋,就執行一次:
SELECT *
FROM [USER]
WHERE ENABLE = 'Y'
即使資料庫查詢只需要 100ms,只要同時有很多使用者操作,資料庫仍會不斷處理幾乎相同的查詢。
這種情況非常適合使用:IMemoryCache
核心概念很簡單:
一段時間內只從資料庫讀取一次完整資料,後續查詢直接從 Web Server 記憶體取得。
1. 沒有 Cache 時會發生什麼?
假設畫面有一個使用者搜尋功能。
使用者輸入:bob
後端執行:
Request
↓
Controller
↓
Service
↓
Database
↓
取得全部使用者
↓
篩選 bob
下一個人搜尋:john
又再執行:
Request
↓
Controller
↓
Service
↓
Database
即使 [USER] 資料根本沒有改變。
假設:100 個 Request,就可能產生:100 次 Database Query,然而這些查詢大部分其實都是重複工作。
2. 加入 Cache 後的概念
加入快取後,流程變成:
第一次 Request
↓
檢查 Cache
↓
Cache 不存在
↓
Database
↓
取得完整資料
↓
放入 Cache
↓
回傳
第二次 Request
↓
檢查 Cache
↓
Cache 存在
↓
直接讀 Memory
↓
回傳
假設 Cache 設定:10 分鐘
那麼可能變成:
10 分鐘內 500 個 Request
↓
Database Query
↓
1 次
剩下的:499 次直接從記憶體讀取。
3. ASP.NET Core 的 IMemoryCache
ASP.NET Core 本身提供:IMemoryCache
它會將資料保存於目前 Web Server Process 的記憶體中。
先在 Program.cs 註冊:builder.Services.AddMemoryCache();
Service 中透過 DI 注入:
private readonly IMemoryCache _memoryCache;
public UserService(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
之後就可以:
_memoryCache.Set(
"SelectUser",
userList,
TimeSpan.FromMinutes(10)
);
取得資料:
if (_memoryCache.TryGetValue(
"SelectUser",
out List<SelectUserModel>? users))
{
return users;
}
4. 最基本的 Cache 寫法
最簡單的實作:
/// <summary>
/// 使用者快取鍵值。
/// </summary>
private const string SelectUserCacheKey =
"UserService.SelectUser";
/// <summary>
/// 使用者快取時間。
/// </summary>
private static readonly TimeSpan SelectUserCacheExpiration =
TimeSpan.FromMinutes(10);
/// <summary>
/// 取得完整使用者清單。
/// </summary>
private async Task<List<SelectUserModel>> GetUsersAsync()
{
/*
* 快取存在:
* 直接使用 Cache,不查詢資料庫。
*/
if (_memoryCache.TryGetValue(
SelectUserCacheKey,
out List<SelectUserModel>? cachedUsers) &&
cachedUsers != null)
{
return cachedUsers;
}
/*
* Cache 不存在:
* 從資料庫取得資料。
*/
List<SelectUserModel> users =
await GetSelectUser();
/*
* 保存 10 分鐘。
*/
_memoryCache.Set(
SelectUserCacheKey,
users,
SelectUserCacheExpiration
);
return users;
}
概念沒有錯。
但正式環境還有一個很重要的問題。
5. Cache Stampede 是什麼?
假設 Cache 在:10:10:00 剛好過期。
同一瞬間有 20 個 Request 進來。
Request A:Cache 不存在
Request B:Cache 不存在
Request C:Cache 不存在
一直到 Request T:Cache 不存在
結果:
Request A → DB
Request B → DB
Request C → DB
Request D → DB
...
Request T → DB
原本希望:10 分鐘只查一次 DB
結果 Cache 一過期:瞬間查 DB 20 次
這種問題通常稱為:Cache Stampede 或:Thundering Herd
6. 使用 SemaphoreSlim 解決 Cache Stampede
可以使用:SemaphoreSlim 控制同一時間只允許一個 Request 重新載入 Cache。
private static readonly SemaphoreSlim SelectUserCacheLock =
new SemaphoreSlim(1, 1);
當 Cache 不存在:
Request A
↓
取得 Lock
↓
查 Database
Request B
↓
等待 Lock
Request C
↓
等待 Lock
A 完成後:
Request A
↓
建立 Cache
↓
Release Lock
B 取得 Lock 後,再檢查一次:Cache 已存在
所以:不需要查 Database
7. 正式版 IMemoryCache 實作
以下是一個比較完整的版本。
/// <summary>
/// 完整使用者清單快取鍵值。
/// </summary>
private const string SelectUserCacheKey =
"UserService.SelectUser";
/// <summary>
/// 使用者清單快取有效時間。
/// </summary>
private static readonly TimeSpan SelectUserCacheExpiration =
TimeSpan.FromMinutes(10);
/// <summary>
/// 防止 Cache Stampede。
///
/// Cache 過期時,只允許一個 Request
/// 重新從資料庫載入完整使用者資料。
/// </summary>
private static readonly SemaphoreSlim SelectUserCacheLock =
new SemaphoreSlim(1, 1);
/// <summary>
/// ASP.NET Core Memory Cache。
/// </summary>
private readonly IMemoryCache _memoryCache;
/// <summary>
/// 取得完整使用者清單。
///
/// 行為:
/// 1. Cache 存在 → 直接回傳。
/// 2. Cache 不存在 → 等待載入鎖。
/// 3. 取得鎖後再次確認 Cache。
/// 4. 確認仍不存在才查詢 Database。
/// 5. 建立 Cache。
/// </summary>
private async Task<IReadOnlyList<SelectUserModel>>
GetFilteredUsersAsync()
{
/*
* 第一次 Cache 檢查。
*
* 正常情況大部分 Request 都會在這裡直接 return。
*/
if (_memoryCache.TryGetValue(
SelectUserCacheKey,
out IReadOnlyList<SelectUserModel>? cachedUsers
) &&
cachedUsers != null)
{
return cachedUsers;
}
/*
* Cache 不存在。
*
* 等待重新載入權限。
*/
await SelectUserCacheLock.WaitAsync();
try
{
/*
* 第二次 Cache 檢查。
*
* 為什麼要檢查兩次?
*
* 因為目前 Request 等待 Lock 的期間,
* 前一個 Request 很可能已經:
*
* 1. 查詢 Database
* 2. 建立 Cache
* 3. Release Lock
*
* 所以取得 Lock 後一定要再次確認。
*/
if (_memoryCache.TryGetValue(
SelectUserCacheKey,
out cachedUsers
) &&
cachedUsers != null)
{
return cachedUsers;
}
/*
* 真正查詢資料庫。
*
* 同一個 Process 中,
* 只有取得 SemaphoreSlim 的 Request
* 可以執行到這裡。
*/
List<SelectUserModel> databaseUsers =
await GetSelectUser();
/*
* 建立資料快照。
*
* 避免後續程式直接修改原始 List。
*/
List<SelectUserModel> snapshot =
databaseUsers?
.Where(user => user != null)
.ToList()
?? new List<SelectUserModel>();
/*
* 使用 IReadOnlyList,
* 避免呼叫端對快取執行:
*
* Add
* Remove
* Clear
*/
IReadOnlyList<SelectUserModel> readOnlyUsers =
snapshot.AsReadOnly();
/*
* Cache 設定。
*/
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
SelectUserCacheExpiration,
Priority =
CacheItemPriority.Normal
};
/*
* 寫入 Cache。
*/
_memoryCache.Set(
SelectUserCacheKey,
readOnlyUsers,
options
);
return readOnlyUsers;
}
finally
{
/*
* 無論成功、失敗、發生 Exception,
* 都必須釋放 SemaphoreSlim。
*/
SelectUserCacheLock.Release();
}
}
這種設計又稱為:Double Check Locking
流程:
Cache ?
│
├─ 有 → return
│
└─ 沒有
↓
Lock
↓
再檢查 Cache
│
├─ 有 → return
│
└─ 沒有
↓
DB
↓
Cache
8. 為什麼要使用 IReadOnlyList?
一般寫法:List<SelectUserModel> 這有一個風險。
假設 Cache 裡面存:List<SelectUserModel> users ,某個方法取得後執行:users.Clear();
因為這個 List 很可能就是 Cache 裡的同一個 Object。
結果:整個 Cache 被清空,而另一段程式:users.RemoveAt(0); 也可能直接修改 Cache。
因此:IReadOnlyList<SelectUserModel> 可降低這類問題。
例如:
IReadOnlyList<SelectUserModel> users =
await GetFilteredUsersAsync();
使用端主要只能:
Where
Select
OrderBy
FirstOrDefault
Any
Count
而不能直接:
Add
Remove
Clear
9. 實際搜尋時不要再查 Database
完整清單已經 Cache 後:
public async Task<List<SelectUserModel>> SearchUsersAsync(
string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
{
return new List<SelectUserModel>();
}
keyword = keyword.Trim();
IReadOnlyList<SelectUserModel> users =
await GetFilteredUsersAsync();
return users
.Where(user =>
ContainsIgnoreCase(
user.LogonID,
keyword
) ||
ContainsIgnoreCase(
user.UserName,
keyword
) ||
ContainsIgnoreCase(
user.EmployeeNO,
keyword
)
)
.OrderBy(user => user.UserName)
.Take(10)
.ToList();
}
共用字串搜尋:
/// <summary>
/// 不區分英文字母大小寫搜尋字串。
/// </summary>
private static bool ContainsIgnoreCase(
string? source,
string keyword)
{
if (string.IsNullOrWhiteSpace(source) ||
string.IsNullOrWhiteSpace(keyword))
{
return false;
}
return source.Contains(
keyword,
StringComparison.OrdinalIgnoreCase
);
}
這樣:
搜尋 Bob
搜尋 John
搜尋 Mary
搜尋 FQ123456
全部都不需要重新查 Database。
10. 多個功能可以共用同一份 Cache
例如系統裡有:
SelectUser
GetChangeUserList
GetApproverList
GetUserSelector
GetSecurityUserList
如果它們底層都需要完整使用者清單,可以共用:
GetFilteredUsersAsync()
例如:
public async Task<List<SelectUserModel>>
GetChangeUserList()
{
IReadOnlyList<SelectUserModel> users =
await GetFilteredUsersAsync();
HashSet<string> allowedLogonIds =
new HashSet<string>(
GetAllowedLogonIds(),
StringComparer.OrdinalIgnoreCase
);
return users
.Where(user =>
!string.IsNullOrWhiteSpace(user.LogonID) &&
allowedLogonIds.Contains(user.LogonID)
)
.ToList();
}
因此:
SelectUser
│
│
GetChangeUserList
│
│
GetApproverList
│
▼
GetFilteredUsersAsync
│
▼
IMemoryCache
│
▼
Database
只有 Cache 不存在時才會到底層 Database。
11. Absolute Expiration
前面的範例使用:
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(10)
意思是:
不論這筆 Cache 有沒有被使用,建立 10 分鐘後一定過期。
例如:
10:00 建立 Cache
10:02 使用
10:05 使用
10:08 使用
10:09 使用
10:10 Cache 過期
即使一直有人使用:10:10 ,仍然會過期。
適合:
- 員工資料
- 組織資料
- 系統設定
- 權限資料
- 定期允許同步的主檔
12. Sliding Expiration
另一種方式:
SlidingExpiration =
TimeSpan.FromMinutes(10)
代表:
最後一次使用後 10 分鐘沒有再被使用,才過期。
例如:
10:00 建立
10:05 使用
→ 延長到 10:15
10:12 使用
→ 延長到 10:22
10:20 使用
→ 延長到 10:30
只要一直有人使用:Cache 可能永遠存在
適合:
- 不常變動資料
- 希望熱門資料長時間保留
- Session-like Cache
但使用者、權限等資料通常不建議單獨使用 Sliding Expiration。
因為資料可能已經修改:DB 已停用某帳號,但 Cache 因為一直有人使用而永遠不過期。
13. Absolute + Sliding 同時使用
可以兩種一起設定:
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions
{
SlidingExpiration =
TimeSpan.FromMinutes(5),
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(30)
};
意思是:5 分鐘沒使用 → 過期
但是:即使一直使用 → 最長 30 分鐘仍一定過期,這很常見的設計。
14. Absolute 與 Sliding 比較
| 類型 | Absolute | Sliding |
|---|---|---|
| 固定時間過期 | 是 | 否 |
| 使用後延長 | 否 | 是 |
| 可保證資料定期刷新 | 是 | 否 |
| 熱門資料可能永久存在 | 否 | 是 |
| 適合權限資料 | 較適合 | 不建議單獨使用 |
| 適合一般主檔 | 適合 | 視情況 |
如果是:使用者清單,我通常建議:
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(10)
簡單而且可預期。
15. 主動清除 Cache
不一定要等 10 分鐘。如果系統知道資料已經修改,可以主動清除:
public void ClearSelectUserCache()
{
_memoryCache.Remove(
SelectUserCacheKey
);
}
例如新增使用者:
public async Task UpdateUserAsync(
UserModel model)
{
/*
* 更新 Database。
*/
await UpdateUserDatabaseAsync(model);
/*
* 更新成功後,
* 立即使 Cache 失效。
*/
ClearSelectUserCache();
}
下一次 Request:
Cache 不存在
↓
重新讀 Database
↓
產生新 Cache
這是一個非常實用的模式:
Time-based expiration
+
Event-based invalidation
也就是:
平常 10 分鐘自動刷新;如果系統知道資料被修改,就立即刷新。
16. 為什麼不是修改 Cache 裡的資料?
例如一名員工名字修改:Bob → Robert
理論上可以:cachedUser.UserName = "Robert";
但實務上通常不推薦。因為你很容易漏掉:
- 其他 Cache
- 其他欄位
- 其他 Server
- 權限資料
- 關聯資料
比較安全的是:
Database 更新成功
↓
Remove Cache
↓
下一次重新建立
也就是:
_memoryCache.Remove(
SelectUserCacheKey
);
17. IMemoryCache 的最大限制:每台 Server 各自一份
這是非常重要的一點。假設系統只有:Web Server A 那:IMemoryCache 非常適合。
但是如果部署:
Load Balancer
│
┌────┴────┐
▼ ▼
Server A Server B
Server A 有自己的:Memory Cache A 與 Server B 有自己的:Memory Cache B 是兩份 Cache 完全獨立。
例如:
Server A
10:00 查 DB
→ Cache A
Server B
10:01 查 DB
→ Cache B
因此:不是整個系統 10 分鐘查一次 DB,而是:每個 Web Server 最多 10 分鐘查一次!
如果有:4 台 Web Server,可能變成:10 分鐘最多 4 次! 通常仍然很少,所以不一定是問題。
18. 多 Server 時可以使用 Distributed Cache
當系統需要:所有 Web Server 共用同一份 Cache
可以使用:Distributed Cache
常見方案:
Redis
SQL Server Distributed Cache
架構:
Redis
▲
│
┌──────┴──────┐
│ │
Server A Server B
│ │
└──────┬──────┘
▼
Database
Server A 與 Server B 都讀同一份 Redis。
19. IMemoryCache vs Distributed Cache
| 項目 | IMemoryCache | Distributed Cache |
|---|---|---|
| 速度 | 非常快 | 快 |
| 是否需要外部系統 | 不需要 | 通常需要 Redis / SQL |
| 部署簡單 | 非常簡單 | 較複雜 |
| 多 Server 共用 | 不行 | 可以 |
| 序列化 | 通常不用 | 通常需要 |
| 網路延遲 | 無 | 有 |
| Server Restart | Cache 消失 | 通常仍存在 |
| 小型系統 | 非常適合 | 可能過度設計 |
| 大型多節點系統 | 視需求 | 適合 |
20. IMemoryCache 最大優點
優點一:速度非常快
Database:
Network
+
SQL Parsing
+
Query Execution
+
Result Transfer
IMemoryCache:直接讀 Process Memory
速度差距很大。
優點二:程式簡單
只需要:builder.Services.AddMemoryCache();
不需要:
Redis Server
Connection String
Redis Cluster
Network
Authentication
Monitoring
優點三:非常適合小型主檔
例如:
5,000 員工
500 部門
50 廠區
100 權限項目
這種資料很適合放 Memory。
21. IMemoryCache 缺點
缺點一:吃 Web Server RAM
Cache 本質上就是:RAM,如果亂放大型資料:500MB、1GB、2GB
可能增加:GC 壓力、Memory Pressure、OOM 的壓力,因此不能把所有資料都 Cache。
缺點二:Server Restart 就消失
例如:
Deploy
IIS Recycle
Application Restart
Container Restart
Cache 都會消失。
不過 Cache 本來就不應該被當成永久資料來源。
正確觀念:
Database = Source of Truth
Cache = 加速用副本
Cache 消失後:重新從 DB 建立即可
缺點三:多 Server 不共享
這是前面提到最大的架構限制。
22. 哪些資料適合 Cache?
非常適合:
使用者主檔
部門
廠區
Category
系統設定
角色定義
國家
語言
下拉選單
產品分類
共同特色:大量讀取+低頻率更新! 這類資料就是 Cache 最理想的使用情境。
23. 哪些資料不適合長時間 Cache?
例如:
銀行餘額
庫存數量
付款狀態
即時訂單
即時設備狀態
登入失敗次數
安全驗證 Token
即時審批結果
因為這些資料:正確性! 通常比:查詢速度重要。如果 Cache:庫存 = 10 實際 DB:庫存 = 0,就可能發生超賣。
24. 權限資料尤其要小心
權限資料可以 Cache,但時間不宜太長。
例如:使用者 Bob 原本是 Admin 在 10:00 管理員取消 Bob 權限。
但 Bob 的 Cache:10:00 ~ 10:10 仍可能保持:Admin = true
因此權限 Cache 建議:
短效 Cache
+
權限修改時立即 Clear Cache
例如:1~5 分鐘 或更新時:
_memoryCache.Remove(
GetPermissionCacheKey(logonId)
);
25. Cache Key 設計非常重要
不推薦:
"User"
"Data"
"List"
"Cache"
很容易撞名。
推薦:
UserService.SelectUser
UserService.Profile.Bob
PermissionService.Bob
DepartmentService.All
CategoryService.Hardware
例如:
private const string SelectUserCacheKey =
"UserService.SelectUser";
如果是每個使用者一份:
private static string GetUserCacheKey(
string logonId)
{
return $"UserService.User:{logonId.ToLowerInvariant()}";
}
26. 不同類型 Cache 要分開
例如:完整員工主檔 和:Bob 的權限,不要放成同一個 Cache。因為它們的生命週期不同。
應該:
UserService.Users.All
PermissionService.User:Bob
完整人員:10~30 分鐘 給予權限:1~5 分鐘,這樣比較設置合理。
27. Cache 時間不是越久越好
很多人一看到 Cache 就設定:24 小時!這通常不是好主意。
Cache 時間應該根據:
1.資料多久會改變?
2.錯誤資料可以接受多久?
3.Database Query 成本是多少?
例如:
| 資料 | 建議概念 |
|---|---|
| 國家清單 | 幾小時 |
| 系統分類 | 30~60 分鐘 |
| 員工主檔 | 5~30 分鐘 |
| 部門 | 5~30 分鐘 |
| 使用者權限 | 1~5 分鐘 |
| 即時庫存 | 不建議普通 Cache |
| Session Token | 不應使用一般 Cache 邏輯 |
這不是絕對值,而是設計思考方向。
28. MemoryCache Priority
可設定:Priority = CacheItemPriority.Normal
ASP.NET Core 支援:
Low
Normal
High
NeverRemove
例如:
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions
{
Priority =
CacheItemPriority.Normal
};
一般業務 Cache 建議:Normal
不要動不動使用:NeverRemove
否則記憶體壓力高時,Cache 不容易被清除。
29. 不要假設 Cache 一定存在 10 分鐘
即使設定:TimeSpan.FromMinutes(10)
並不代表一定存活整整 10 分鐘。在 Memory Pressure 等情況下,系統可能提早移除 Cache。
因此程式永遠要能處理:Cache Missing ,並重新建立。不能寫成:Cache 不可能不存在
30. Cache 不是 Database
最重要的設計觀念:
Database
=
Source of Truth
Cache:
Cache
=
Temporary Copy
所以系統應該符合:Cache 全部清空,依然可以正常運作。
頂多:第一次查詢比較慢,不能因為 Cache 消失:系統資料就不見
31. Cache Aside Pattern
目前介紹的設計其實就是非常常見的:
Cache-Aside Pattern
流程:
Application
│
▼
Check Cache
│
┌───┴───┐
│ │
Hit Miss
│ │
▼ ▼
Return DB
│
▼
Set Cache
│
▼
Return
Application 自己負責:
先查 Cache
Cache 沒有才查 DB
DB 查完放 Cache
這是 ASP.NET Core 最常見、也最好理解的 Cache Pattern。
32. Cache Hit 與 Cache Miss
Cache 領域常看到兩個術語。
Cache Hit
Cache 有資料
例如:
_memoryCache.TryGetValue(...)
取得成功。稱為:
Cache Hit
Cache Miss
Cache 沒有資料:
第一次查詢
Cache 過期
Cache 被 Remove
Server Restart
稱為:
Cache Miss
理想系統通常希望:
Cache Hit Rate
越高越好。
33. GetOrCreateAsync 可以嗎?
ASP.NET Core 也可以寫:
List<SelectUserModel> users =
await _memoryCache.GetOrCreateAsync(
SelectUserCacheKey,
async entry =>
{
entry.AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(10);
return await GetSelectUser();
}
);
程式非常漂亮。
完整版本:
private async Task<List<SelectUserModel>>
GetUsersAsync()
{
return await _memoryCache.GetOrCreateAsync(
SelectUserCacheKey,
async entry =>
{
entry.AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(10);
entry.Priority =
CacheItemPriority.Normal;
return await GetSelectUser();
}
) ?? new List<SelectUserModel>();
}
34. GetOrCreateAsync 優缺點
優點
非常簡潔:
少量程式碼
容易閱讀
容易維護
適合:
一般 Cache
低併發系統
DB Query 成本不高
缺點
如果非常在意:Cache 過期瞬間只能查 DB 一次,仍建議自行搭配:SemaphoreSlim,因為多 Request 同時 Cache Miss 時,需要明確控制資料載入行為。
所以:
一般系統
→ GetOrCreateAsync
高併發 + Query 很重
→ Double Check + SemaphoreSlim
35. 三種常見做法比較
方法一:每次查 Database
Request
↓
DB
優點
資料永遠最新
程式簡單
缺點
Database 壓力大
重複 Query 多
延遲較高
適合:
即時性非常重要、查詢頻率低
方法二:IMemoryCache
Request
↓
Memory
↓
必要時 DB
優點
最快
簡單
不需要額外 Infrastructure
缺點
Server 不共享
Restart 後消失
使用 Server RAM
適合:
單機
小型至中型企業系統
低變動主檔
方法三:Redis / Distributed Cache
Request
↓
Redis
↓
必要時 DB
優點
多台 Server 共用
Cache 集中管理
可支援大型系統
缺點
Infrastructure 複雜
有 Network latency
需要 Redis 維運
需要 Serialization
適合:
多 Server
Container / Kubernetes
大型系統
跨 Service
36. 實際架構選擇
如果系統是:ASP.NET Core + 一台 IIS+SQL Server + 5,000 員工
建議:IMemoryCache 就夠了。
沒必要為了 Cache 特別建立 Redis。
如果系統未來變成:
Load Balancer
│
┌──────┼──────┐
│ │ │
Web1 Web2 Web3
再考慮:
Redis
通常比較合理。
不要在系統還只有:
一台 Server
就先導入:
Redis Cluster
Distributed Lock
Message Queue
這很可能只是增加維護成本。
37. 完整實務架構建議
以使用者搜尋功能來說,我會設計成:
Controller
│
▼
UserService
│
▼
GetFilteredUsersAsync
│
├──────── Cache Hit
│ │
│ ▼
│ Memory
│
└──────── Cache Miss
│
▼
SemaphoreSlim
│
▼
Database
│
▼
IMemoryCache
然後:
SelectUser
GetChangeUserList
GetApproverList
全部共用:GetFilteredUsersAsync
這樣才真正做到:
完整使用者清單在一段時間內,只從資料庫讀取一次。
38. 最後整理
Cache 不只是:_memoryCache.Set(...)
真正需要考慮的是:
資料什麼時候載入?
資料保存多久?
Cache Miss 怎麼處理?
多 Request 同時 Miss 怎麼辦?
資料修改後如何失效?
多台 Server 是否共用?
資料是否允許短暫過期?
Cache 是否可能太大?
對一般企業內部 ASP.NET Core 系統而言,如果資料具有:
讀取非常頻繁
+
修改頻率低
+
資料量可放入 RAM
那:
IMemoryCache
+
Absolute Expiration
+
Cache Aside
+
SemaphoreSlim
+
主動 Cache Invalidation
是一套非常實用又不會過度複雜的設計。
39. 一句話理解整套設計
可以把 Cache 想像成辦公桌上的影印資料。
Database 是:檔案室裡的正式文件
IMemoryCache 是:桌上的影本
每次有人問資料,不需要跑去檔案室,直接看桌上的影本即可。
但過一段時間:重新去檔案室拿最新版,如果有人通知:正式文件剛剛修改了,就立刻:丟掉桌上的舊影本! 下一次再重新影印一份。
而 SemaphoreSlim 解決的是:
文件過期時,不要 20 個人一起衝去檔案室拿同一份文件,只需要一個人去拿,其他人等他把新影本放回桌上。
這就是這套快取設計最核心的概念。