傳統 Cache.Insert(key, value, CacheDependency) 僅支援**檔案/目錄/鍵值**相依。當資料來源為 SQL Server 資料表,且需**「資料異動即時讓快取失效」**時,ASP.NET 2.0+ 內建 **SqlCacheDependency** 機制,透過 **Service Broker / 查詢通知 / 輪詢** 三種模式實現。
| 模式 | 原理 | SQL Server 版本 | 延遲 | 權限需求 | 適用場景 |
|---|---|---|---|---|---|
| Service Broker (SqlDependency) | 資料庫推送通知到佇列,.NET 客戶端監聽 | 2005+ (需啟用 Broker) | 近乎即時 (秒級) | SUBSCRIBE QUERY NOTIFICATIONS | 高即時性、低頻異動、單一資料庫 |
| 查詢通知 | 類似 SqlDependency,但較底層 | 2005+ | 秒級 | 同 SqlDependency | 特殊查詢需求 |
| 輪詢 | 定期查詢系統資料表 sys.dm_qn_subscriptions 或自訂觸發器更新檔案 | 2000+ | 輪詢間隔 (預設 60s) | 建立觸發器/資料表權限 | 舊版 SQL、跨資料庫、無 Broker 環境 |
-- 1. 啟用 Service Broker
ALTER DATABASE Northwind SET ENABLE_BROKER WITH ROLLBACK IMMEDIATE;
-- 2. 授權
GRANT SUBSCRIBE QUERY NOTIFICATIONS TO [AppUser];
GRANT RECEIVE ON QueryNotificationErrorsQueue TO [AppUser];
-- 3. 確認資料表符合限制:需有主鍵、不含 *、不含非確定性函數
SELECT EmployeeID, LastName, FirstName FROM dbo.Employees; -- 合法查詢
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web.Caching;
public class EmployeeRepository
{
private static readonly string ConnStr = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
private const string CacheKey = "Employees_All";
private const string Sql = "SELECT EmployeeID, LastName, FirstName FROM dbo.Employees";
public static DataTable GetEmployees()
{
// 1. 嘗試從快取取得
if (HttpRuntime.Cache[CacheKey] is DataTable cached)
return cached;
// 2. 快取未命中,查詢資料庫並建立相依性
using (var conn = new SqlConnection(ConnStr))
using (var cmd = new SqlCommand(Sql, conn))
{
// 關鍵:SqlCommand 必須關聯 SqlDependency
var dep = new SqlCacheDependency(cmd);
conn.Open();
var dt = new DataTable();
using (var adapter = new SqlDataAdapter(cmd))
adapter.Fill(dt);
// 3. 插入快取,附加 SqlCacheDependency
HttpRuntime.Cache.Insert(CacheKey, dt, dep,
Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
CacheItemPriority.Normal, OnCacheRemoved);
return dt;
}
}
// 4. 快取移除回呼:記錄日誌、預熱、通知其他節點
private static void OnCacheRemoved(string key, object value, CacheItemRemovedReason reason)
{
_logger.Info($"Cache {key} removed: {reason}");
if (reason == CacheItemRemovedReason.DependencyChanged)
{
// 非同步預熱下一筆,避免下一請求穿透 DB
Task.Run(() => GetEmployees());
}
}
}
protected void Application_Start()
{
// 啟動 SqlDependency 監聽器 (每個連線字串僅需啟動一次)
SqlDependency.Start(ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString);
}
protected void Application_End()
{
SqlDependency.Stop(ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString);
}
此為文章原始方案現代化改良版,核心思想:**資料庫觸發器更新「信號檔案」 → ASP.NET 以 CacheDependency 監視該檔案**。
-- 1. 建立共享資料夾 \\CacheServer\SqlSignals\ 並授予 SQL Server 服務帳號寫入權限
-- 2. 建立空白信號檔案
EXEC xp_cmdshell 'type nul > \\CacheServer\SqlSignals\Employees.sig';
-- 3. 建立觸發器
CREATE TRIGGER trg_Employees_NotifyCache
ON dbo.Employees
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
-- 更新檔案時間戳記即可觸發 CacheDependency
EXEC xp_cmdshell 'copy /b \\CacheServer\SqlSignals\Employees.sig+,, > nul';
END;
<system.web>
<caching>
<sqlCacheDependency pollTime="5000" /> <!-- 5 秒輪詢一次 -->
</caching>
</system.web>
public static DataTable GetEmployeesPolling()
{
const string key = "Employees_Polling";
if (HttpRuntime.Cache[key] is DataTable cached) return cached;
using (var conn = new SqlConnection(ConnStr))
using (var cmd = new SqlCommand("SELECT EmployeeID, LastName, FirstName FROM dbo.Employees", conn))
{
// 建立檔案相依性
var dep = new CacheDependency(@"\\CacheServer\SqlSignals\Employees.sig");
conn.Open();
var dt = new DataTable();
new SqlDataAdapter(cmd).Fill(dt);
HttpRuntime.Cache.Insert(key, dt, dep, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration);
return dt;
}
}
SqlDependency.Start,Service Broker 會將通知**廣播**至所有訂閱者,天然支援多節點。| 指標 | 建議閾值 | 優化手段 |
|---|---|---|
| SqlDependency 佇列積壓 | < 100 | 縮小查詢範圍、增加索引、拆分熱點表 |
| 輪詢 CPU/IO | 無感知 | 調大 pollTime (10-30s)、改用 Service Broker |
| 快取命中率 | > 95% | 預熱、調整 SlidingExpiration、熱資料永不過期 |
| 記憶體壓力 | Gen 2 GC < 1/min | 設定 CacheItemPriority、限制快取總大小 |
| 錯誤 | 原因 | 解決 |
|---|---|---|
| SqlDependency 不觸發 | 查詢不符合限制 (含 *、JOIN、非確定函數、無主鍵) | 簡化查詢、加主鍵、避免 * |
| Service Broker 無法啟動 | 資料庫未啟用 Broker、或單一使用者模式 | ALTER DATABASE SET ENABLE_BROKER |
| 權限不足 | 缺少 SUBSCRIBE QUERY NOTIFICATIONS | GRANT 相關權限 |
| 檔案相依性不生效 | UNC 路徑權限、SQL Server 服務帳號無寫入權 | 配置共享資料夾權限、改用本地檔案 + 複寫 |
對於高併發、微服務架構,建議棄用進程內 HttpRuntime.Cache,改用:
// 伪代码:CDC 事件處理器
public class SqlChangeHandler : INotificationHandler
{
private readonly IDatabase _redis;
public async Task Handle(SqlChangedEvent evt, CancellationToken ct)
{
var keys = _redis.SearchKeys($"cache:{evt.Table}:*");
await _redis.KeyDeleteAsync(keys);
// 可選:發布失效事件供其他微服務
await _redis.PublishAsync("cache:invalidate", JsonSerializer.Serialize(new { evt.Table, Keys = keys }));
}
}