有個開發老鳥專屬的「成功經驗魔咒」:遇到難題,想出一套簡單有效解法,或許有些小缺點,但造成的麻煩在可忍受範圍,於是 日後再遇到同樣狀況,一律照方煎藥,數十年如一日。

但技術會革新、元件會改進,善用一些新特性,小缺點其實可以化為無形。可怕的地方在於:如果每次都能順利解決問題,就不會圖謀改進,直到有天發現洋人船堅砲利,才知自己已成滿清… 老鳥想一直寫程式又不想被時代淘汰,就得提高警覺。有個超簡單的實踐方法-對自己機車一點。當有人反應不方便時,別一句「就多一個動作會死嗎?」頂回去,改成問自己:「連這個動作都省不掉?嫩!」,對自己GY一點才能撐久一點。以下算是個實例:

專案有時會遇到上傳檔案更新資料的機制,每天由排程將當天的資料寫到固定資料夾,網站執行時解析檔案轉為必要的資料格式。由於檔案每天只更新一次,每次重新讀檔解析太沒效率,解析完將資料寫入 Cache 並設定當日有效,隔日再用到時 Cache 已逾時再讀取新資料,如此兼顧效能與資料即時性,看似挺完美。但有個問題,若營運過程發現檔案有誤重新上傳,此時 Cache 仍有效,網站將繼續沿用舊資料。因此得多設計清除 Cache 的 API,而中途更新檔案的 SOP 要改成:1) 上傳檔案 2) 呼叫清除 Cache API。

以下是實作範例:

DataHelper.cs

public class ProductItem
{
    public string Name;
    public decimal Price;
}
 
const string CACHE_KEY = "PriceData";
 
public static 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(' ');
                    return new 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>;
    }
}
 
public static void 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;
}
 
<!DOCTYPE html>
 
<html>
<head>
    <meta name="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>
            <tr class="hdr"><th>品名</th><th>價格</th></tr>
            @foreach (MyMvc.Models.DataHelper.ProductItem prod in Model.Skip(1))
            {
                <tr>
                    <td class="name">@prod.Name</td>
                    <td class="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[] 檔案或路徑)) 就大功告成!

public static 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(' ');
                    return new 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>;
    }
}

實際展示如下,下方的檔案快取時間可用於驗證資料是否來自快取,先重新整理兩次時間未變,代表使用的是快取中的資料;修改檔案後儲存,重新整理網頁價格數字跟快取時間立即更新!

就醬,老狗又學會了新把戲。


Comments

Be the first to post a comment

Post a comment