lock/ConcurrentDictionary)導致延遲抖動;
+----------------+ 1. 統計熱點 Key +------------------+
| 熱點提取任務 | -------------------------> | 版本庫 (MySQL) | (ArticleId, Version)
| (每小時跑批) | +------------------+
+----------------+ ^
| |
| 2. FTP 上傳緩存文件 + 通知 | 3. 更新文章時 Version++
v |
+------------------+ 4. 載入新緩存替換舊緩存 |
| 緩存服務器 | <-----------------------------------+
| (Redis/內存) | 5. 用戶請求
+------------------+ |
^ |
| 6. 讀緩存 + 版本比對 |
| v
+------------------+ 7. 版本不符/無緩存 -> 查 DB + 更新緩存
| 應用服務器 | -------------------------------->
| (Web API) |
+------------------+
public abstract class CacheableEntity { public int Version { get; set; } }
public class Article : CacheableEntity { public int Id { get; set; } public string Content { get; set; } }
public class CacheItem {
public Article Article { get; set; }
}
public interface IVersionManager {
int GetArticleVersion(int articleId);
void UpdateArticleVersion(int articleId); // DB 更新文章時同步調用
}
public static class ArticleCacheManager {
private static volatile bool _cacheReady = false;
private static volatile Dictionary _cache = new();
public static bool CacheReady => _cacheReady;
public static void InitCache() {
_cache = new Dictionary();
_cacheReady = true;
}
// 後台任務調用:原子替換整個字典引用,無需鎖
public static void LoadCache(Dictionary newCache) {
_cache = newCache; // volatile 保證可見性
}
// 讀取:先拷貝引用,防止替換過程中讀到 null
public static Article GetArticle(int articleId) {
if (!_cacheReady || articleId <= 0) return null;
var cache = _cache; // 臨時變量捕獲當前引用
return cache.TryGetValue(articleId, out var item) ? item.Article : null;
}
// 版本驗證後的更新入口(極少用,僅版本不符時回寫)
internal static void UpdateArticle(int articleId, Article article) {
if (!_cacheReady) return;
var cache = _cache;
if (cache.TryGetValue(articleId, out var item)) {
item.Article = article; // 引用替換,線程安全
}
}
internal static IVersionManager GetVersionManager() => /* 實現 */ null;
}
public class ArticleItem {
public int ArticleId { get; }
public Article Article { get; private set; }
public ArticleItem(int id) => ArticleId = id;
public void Load() {
// 1. 緩存切換期直接查 DB
if (!ArticleCacheManager.CacheReady) {
Article = DBAdapter.GetArticle(ArticleId, false);
return;
}
var versionMgr = ArticleCacheManager.GetVersionManager();
var cached = ArticleCacheManager.GetArticle(ArticleId);
// 2. 版本一致 -> 命中緩存
if (cached != null && cached.Version == versionMgr.GetArticleVersion(ArticleId)) {
Article = cached;
return;
}
// 3. 未命中或版本不符 -> 查 DB 並可選更新緩存
Article = DBAdapter.GetArticle(ArticleId, cached != null);
}
}
internal static class DBAdapter {
internal static Article GetArticle(int id, bool updateCache) {
var article = /* 從 DB 查詢 */ new Article { Id = id, Version = /* 當前版本 */ 1 };
if (updateCache) ArticleCacheManager.UpdateArticle(id, article);
return article;
}
}
public class CacheWarmupService : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
try {
if (await HasNewCacheFilesAsync()) {
var newCache = await LoadCacheFromDiskAsync();
ArticleCacheManager.LoadCache(newCache);
_logger.LogInformation("Cache reloaded, count={Count}", newCache.Count);
}
}
catch (Exception ex) { _logger.LogError(ex, "Cache warmup failed"); }
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
| 技術點 | 實現細節 | 優勢 |
|---|---|---|
| volatile + 引用替換 | _cache = newCache 原子替換字典引用,讀取端捕獲局部變量 | 讀取完全無鎖,寫入僅發生於後台單線程,極高吞吐 |
| 版本號窄表 | 僅含 (Id, Version) 兩列,索引極小,查詢微秒級 | 將一致性檢查開銷降至最低,避免業務表鎖 |
| 熱點主動預熱 | 統計近 1 小時訪問 Top 10k,FTP 推送緩存文件 | 命中率預期 > 95%,冷啟動穿透 DB 極少 |
| 容忍短暫不一致 | 版本檢查與緩存讀取非原子,極端並發下可能返回舊版 | 用戶刷新即可修復,體驗影響微小,換取系統穩定性 |
ReaderWriterLockSlim 保護關鍵區(犧牲部分無鎖優勢)。本方案通過 熱點主動預熱 + 版本號驗證 + 無鎖引用替換 三大招式,將緩存系統從「鎖爭用、低命中、強一致」轉型為「無鎖讀、高命中、最終一致」。適合「讀多寫少、熱點明顯、容忍秒級不一致」的內容型、資訊型、電商商品詳情等場景。核心代碼不足 100 行,可直接集成至現有 .NET 微服務或單體應用中。