有個開發老鳥專屬的「成功經驗」陷阱:遇到難題,想出一套簡單有效解法,或許有些小缺點,但造成的麻煩在可忍受範圍,於是 日後再遇到同樣狀況,一律照方煎藥,數十年如一日。
但技術會革新、元件會改進,善用一些新特性,小缺點其實可以化為無形。可怕的地方在於:如果每次都能順利解決問題,就不會圖謀改進,直到有天發現洋人船堅砲利,才知自己已成滿清… 老鳥想一直寫程式又不想被時代淘汰,就得提高警覺。有個超簡單的實踐方法-對自己機車一點。當有人反應不方便時,別一句「就多一個動作會死嗎?」頂回去,改成問自己:「連這個動作都省不掉?嫩!」,對自己GY一點才能撐久一點。以下算是個實例:
專案有時會遇到上傳檔案更新資料的機制,每天由排程將當天的資料寫到固定資料夾,網站執行時解析檔案轉為必要的資料格式。由於檔案每天只更新一次,每次重新讀檔解析太沒效率,解析完將資料寫入 Cache 並設定當日有效,隔日再用到時 Cache 已逾時再讀取新資料,如此兼顧效能與資料即時性,看似挺完美。但有個問題,若營運過程發現檔案有誤重新上傳,此時 Cache 仍有效,網站將繼續涗用舊資料。因此得多設計清除 Cache 的 API,而中途更新檔案的 SOP 要改成:1) 上傳檔案 2) 呼叫清除 Cache API。
以下是實作範例:
DataHelper.cs
publicclass ProductItem
{
publicstring Name;
publicdecimal Price;
}
conststring CACHE_KEY = "PriceData";
publicstatic List<ProductItem> GetPriceData()
{
//實務上可寫成共用函式GetCachableData,參考:https://goo.gl/K0IeTb
//這裡為示範原理,直接操作MemoryCache
var cache = MemoryCache.Default;
lock (cache)
{
if (cache[CACHE_KEY] == null)
{
string filePath = HostingEnvironment.MapPath("~/App_Data/Price.txt");
//將文字資料轉為物件陣列
List<ProductItem> list = System.IO.File.ReadAllLines(filePath)
.Select(o =>
{
var p = o.Split(' ');
returnnew ProductItem()
{
Name = p[0],
Price = decimal.Parse(p[1])
};
}).ToList();
//第一筆塞入Cache產生時間
list.Insert(0, new ProductItem() { Name = DateTime.Now.ToString("HH:mm:ss") });
cache.Add(CACHE_KEY, list, new CacheItemPolicy()
{
AbsoluteExpiration = DateTime.Today.AddDays(1)
});
}
return cache[CACHE_KEY] as List<ProductItem>;
}
}
publicstaticvoid ClearPriceData()
{
MemoryCache.Default.Remove(CACHE_KEY);
}
HomeController.cs
public ActionResult ShowDailyPrice()
{
return View(DataHelper.GetPriceDataEx());
}
public ActionResult ClearDailyPrice()
{
DataHelper.ClearPriceData();
return Content("OK");
}
ShowDailyPrice.cshtml
@model List<MyMvc.Models.DataHelper.ProductItem>
@{
Layout = null;
}
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>ShowDailyPrice</title>
<style>
table { width: 200px; font-size: 11pt; }
td,th { text-align: center; padding: 6px; }
tr:nth-child(even) { background-color: #eee; }
tr.hdr { background-color: #0094ff; color: white; }
.name { width: 70%; }
.prz { width: 30%; text-align: right; }
</style>
</head>
<body>
<div>
<table>
<trclass="hdr"><th>品名</th><th>價格</th></tr>
@foreach (MyMvc.Models.DataHelper.ProductItem prod in Model.Skip(1))
{
<tr>
<tdclass="name">@prod.Name</td>
<tdclass="prz">@string.Format("{0:n1}", prod.Price)</td>
</tr>
}
</table>
</div>
<br/>
<small>
快取產生時間:@Model.First().Name
</small>
</body>
</html>
這套寫法我用在很多專案,由於中途更新檔案頻率不高,SOP 多一個動作大家覺得還好。但如果 GY 一點:手動清 Cache 的動作真的不能省嗎?這都搞不定,你有臉說自己是資深程式設計師?
其實,都用了 MemoryCache 做檔案快取卻要手動清 Cache 真的有點 Low。在存入 Cache 時,CacheItemPolicy除了設定保存期限、移除事件外,還可以指定 ChangeMonitor 物件,跟檔案、資料庫建立相依關係,在資料異動時自動清除快取。.NET 提供了幾個現成實作元件,包含:CacheEntryChangeMonitor(綁定另一個 Cache 項目,當其被移除時一併移除)、SqlChangeMonitor(利用 SQL Server 的 SqlDependency在某個 DB 查詢結果改變時自動移除)以及 HostFileChangeMonitor(FileChangeMonitor 是抽象類別,HostFileChangeMonitor 是它的實作,偵測到檔案或資料夾異動時可自動移除快取),而我們的案例即可藉由 HostFileChangeMonitor 實現重傳檔案時自動清除快取,省去手動清除的多餘步驟。
寫法很簡單,CacheItemPolicy.ChangeMonitors.Add(new HostFileChangeMonitor(string[] 檔案或路徑)) 就大功告成!
publicstatic List<ProductItem> GetPriceData()
{
//實務上可寫成共用函式GetCachableData,參考:https://goo.gl/K0IeTb
//這裡為示範原理,直接操作MemoryCache
var cache = MemoryCache.Default;
lock (cache)
{
if (cache[CACHE_KEY] == null)
{
string filePath = HostingEnvironment.MapPath("~/App_Data/Price.txt");
//將文字資料轉為物件陣列
List<ProductItem> list = System.IO.File.ReadAllLines(filePath)
.Select(o =>
{
var p = o.Split(' ');
returnnew ProductItem()
{
Name = p[0],
Price = decimal.Parse(p[1])
};
}).ToList();
//第一筆塞入Cache產生時間
list.Insert(0, new ProductItem() { Name = DateTime.Now.ToString("HH:mm:ss") });
CacheItemPolicy policy = new CacheItemPolicy()
{
AbsoluteExpiration = DateTime.Today.AddDays(1)
};
//指定檔案異動時自動移除Cache内容
policy.ChangeMonitors.Add(new HostFileChangeMonitor(
//HostFileChangeMonitor接受IList<string>,此處用Split小技巧將單一或多檔名轉成IList
filePath.Split('\n')));
cache.Add(CACHE_KEY, list, policy);
}
return cache[CACHE_KEY] as List<ProductItem>;
}
}
實際展示如下,下方的檔案快取時間可用於驗證資料是否來自快取,先重新整理兩次時間未變,代表使用的是快取中的資料;修改檔案後儲存,重新整理網頁價格數字跟快取時間立即更新!
就醬,老狗又學會了新把戲。