關於 ASP.NET 記憶體快取你需要知道的 10 點

leoxu,Tocy,無若發表於2017-06-23

快取機制的主要目的是提高應用程式的效能。作為 ASP.NET 開發人員,你可能會意識到 ASP.NET Web 窗體以及 ASP.NET MVC 可以使用 Cache 物件快取應用程式的資料。這通常被稱為伺服器端資料快取,並且常作為框架的內建功能。雖然 ASP.NET Core 中並沒有這樣的 Cache 物件,但是你可以很容易地實現記憶體快取。本文將向你說明如何實現。

在進一步閱讀之前,你先建立一個基於 Web 應用程式專案模板的新的 ASP.NET Core 應用程式。

然後按照下面提到的步驟逐一構建和測試由記憶體快取提供的各種功能。

1. 記憶體快取需要在啟動類 Startup 中啟用一下

不同於 ASP.NET Web 窗體和 ASP.NET MVC,ASP.NET Core 沒有內建的 Cache 物件,可以拿來在控制器裡面直接使用。 這裡,記憶體快取時通過依賴注入來啟用的,因此第一步就是在 Startup 類中註冊記憶體快取的服務。如此,就得開啟 Startup 類然後定位到 ConfigureServices() 方法,像下面這樣修改 ConfigureServices() 方法:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
        services.AddMemoryCache();
}

為了向你的應用程式加入記憶體快取能力,你需要在服務集合上呼叫 AddMemoryCache() 方法。採用這種辦法就可以讓一個記憶體快取(它是一個 IMemoryCache 物件)的預設實現可以被注入到控制器中去。

2. 記憶體快取使用依賴注入來注入快取物件

然後開啟 HomeController 並對其進行修改,如下所示:

public class HomeController : Controller
{
    private IMemoryCache cache;

    public HomeController(IMemoryCache cache)
    {
        this.cache = cache;
    }
    ....
}

如你所見,上述程式碼宣告瞭一個 ImemoryCache 的私有變數。該變數會被構造器中被賦值。構造器會通過 DI(依賴注入)接收到快取引數,然後被儲存在本地變數總,提供後續使用。

3. 你可以使用 Set() 方法來在快取中存東西

等你有了這個 IMemoryCache 物件,就可以讀取或者向它寫入資料了。向快取寫入資料項是相當直接的。

public IActionResult Index()
{
  cache.Set<string>("timestamp", DateTime.Now.ToString());
  return View();
}

上述程式碼在 Index() 這個 action 中設定了一個快取項。這是通過使用 IMemoryCache 的 Set<T>() 來完成的。Set() 方法的第一個引數是鍵名,用來標識該資料項。第二個引數是鍵的取值。在此例中,我們儲存一個字串的鍵和一個字串的值,而你也可以儲存其它型別 (原生以及自定義的型別) 的鍵值對。

4. 你可以使用 Get 方法來從快取中獲取到一個資料項

等你向快取中新增好了資料,也許會想要在應用程式的其它地方去獲取到該資料,可以用 Get() 來做到。如下程式碼會告訴你如何來做這件事情。

public IActionResult Show()
{
  string timestamp = cache.Get<string>("timestamp");
  return View("Show",timestamp);
}

上述程式碼從 HomeController 的另外一個action(Show)那裡獲取到了一個快取的資料項。Get() 方法會指定資料項的型別以及它的鍵名。如果該資料項存在的話,就會被返回並且被賦值給 timestamp 這個字串變數。然後這個 timestamp 的值就會被傳遞給 Show 檢視。

Show 檢視只是簡單地輸出了 timestamp 的值,如下所示:

<h1>TimeStamp : @Model</h1>

<h2>@Html.ActionLink("Go back", "Index", "Home")</h2>

為了對目前為止你所寫的程式碼進行一下測試,請執行應用程式。首先將瀏覽器導航至 /Home/Index ,這樣 timestamp 鍵就會被賦值。然後導航至 /Home/Show 並檢視 timestamp 值是否會輸出。下圖所示是 Show() 這個 action 執行起來的一個例子。

5. 你可以使用 TryGet() 來檢查快取中是否存在特定的鍵值

如果你觀察前面的示例,會發現每次你導航至 /Home/Index 的時候, 都會有一個新的 timestamp 被賦值給了快取項。這是因為我們並沒有對此進行檢查,規定只有在資料項不存在的時候才賦值。許多時候你都會想要這樣做的。這裡有兩種辦法可以在 Index() 這個 action 裡面來做這樣的檢查。我們把兩種辦法都在下面列了出來。

//first way
if (string.IsNullOrEmpty
(cache.Get<string>("timestamp")))
{
  cache.Set<string>("timestamp", DateTime.Now.ToString());
}

//second way
if (!cache.TryGetValue<string>
("timestamp", out string timestamp))
{
    cache.Set<string>("timestamp", DateTime.Now.ToString());
}

第一種辦法使用了你早先用過的同一個 Get() 方法,這一次它被拿來跟 if 塊一起用。如果 Get() 不能在快取中找到指定的資料項,IsNullOrEmpty() 就會返回 true。而只有這時候 Set() 才會被呼叫,一次來新增資料項。

第二種辦法更加優雅一點。它使用 TryGet() 方法來獲取一個資料項。TryGet() 方法會返回一個布林值來指明資料項有沒有被找到。實際的資料項可以使用一個輸出引數拉取出來。如果 TryGet() 返回false,Set() 就會被用來新增資料。

6. 如果不存在的話,可以使用 GetOrCreate() 來新增一項

有時你需要從快取中檢索現有項。如果該專案不存在,則希望新增該項。這兩個任務 – 如果它存在獲取值,否則建立之 – 可以使用 GetOrCreate() 方法來實現。修改後的 Show() 方法展示瞭如何實現的。

public IActionResult Show()
{
  string timestamp = cache.GetOrCreate<string>
  ("timestamp", entry => { 
return DateTime.Now.ToString(); });
  return View("Show",timestamp);
}

Show() 動作現在使用 GetOrCreate() 方法。 GetOrCreate() 方法將檢查時間戳的鍵值是否存在。如果是,現有值將被賦值給區域性變數。否則,將根據第二個引數中指定的邏輯建立一個新條目並將其新增到快取中。

為了測試此程式碼,請直接執行 /Home/Show,不需要跳轉到 /Home/Index。你仍然會看到輸出的時間戳值,因為在該值不存在的情況下,GetOrCreate() 現在是新增了它。

7. 你可以在一個快取的資料項上面設定絕對和滾動的過期時間

在前述示例中,一個快取項只要被新增到快取就會一直儲存,除非它被明確地使用 Remove() 從快取中移除。你也可以在一個快取項上面設定一個絕對和滾動的過期時間。一個絕對的過期設定意味著該快取項會在嚴格指定的日期和時間點被移除,而滾動過期設定則意味著它在給定的一段時間量處於空閒狀態(也就是沒人去訪問)之後被移除。

為了能在一個快取項上面設定這兩種過期策略,你要用到 MemoryCacheEntryOptions 物件。如下程式碼向你展示瞭如何去使用。

MemoryCacheEntryOptions options = 
new MemoryCacheEntryOptions();
options.AbsoluteExpiration = 
DateTime.Now.AddMinutes(1);
options.SlidingExpiration = 
TimeSpan.FromMinutes(1);
cache.Set<string>("timestamp", 
DateTime.Now.ToString(), options);

上述程式碼來自於修改過的 Index() action,它建立了一個 MemoryCacheEntryOptions 的物件,然後將它的 AbsoluteExpiration 屬性設定為從此刻到一分鐘之後的一個 DateTime 值,它還將 SlidingExpiration 屬性設定為一分鐘。這些值都指定了該快取項會在一分鐘之後從快取移除,不管其是否會被訪問。此外,如果該快取項如初持續空閒了有一分鐘,它也會被從快取中移除。

等你將 AbsoluteExpiration 和 SlidingExpiration 的值設定後, Set() 方法就可以被用來將一個資料項新增到快取。這一次 MemoryCacheEntryOptions 物件會被作為第三個引數傳遞給 Set() 方法。

8. 當快取項會被移除時,你可以連線回撥

有時你會想要在快取項從快取中被移除時收到通知。可能會有多種原因需要從快取中移除資料項。例如,因為明確地執行了 Remove() 方法而移除了一個快取項, 也有可能是因為它的 AbsoluteExpiration 和 SlidingExpiration 值已經到期而被移除,諸如此類的原因。

為了能知道專案是何時從快取移除的,你需要編寫一個快取函式。如下程式碼向你展示瞭如何去做這件事情:

MemoryCacheEntryOptions options = 
new MemoryCacheEntryOptions();
options.AbsoluteExpiration = 
DateTime.Now.AddMinutes(1);
options.SlidingExpiration = 
TimeSpan.FromMinutes(1);
options.RegisterPostEvictionCallback
(MyCallback, this);
cache.Set<string>("timestamp", 
DateTime.Now.ToString(), options);

上述程式碼同之前使用 MemoryCacheEntryOptions 來配置 AbsoluteExpiration 和 SlidingExpiration 的程式碼相當類似。更加重要的是它也呼叫了 RegisterPostEvictionCallback() 方法來繫結剛剛討論過的回撥函式。在這裡回撥函式被命名為 MyCallback。第二個引數是一個你會想要傳遞給回撥函式的狀態物件。這裡我們傳入了 HomeController 的例項 (用 this 將當前的 HomeController 物件“點”出來) 作為狀態物件。

前面提到的MyCallback函式,其程式碼如下所示:

private static void MyCallback(object key, object value,
EvictionReason reason, object state)
{
    var message = $"Cache entry was removed : {reason}";
    ((HomeController)state).
cache.Set("callbackMessage", message);
}

請仔細觀察這段程式碼。 MyCallback() 是 HomeController 類裡面的一個私有靜態函式,它有四個引數。前面兩個參數列示剛剛刪除的快取項的鍵和值,第三個參數列示的是該資料項被刪除的原因。EvictionReason 是一個列舉型別,它維護者各種可能的刪除原因,如過期,刪除以及替換。

在回撥函式的內部,我們會基於刪除的原因構造一個字串訊息。我們想要將此訊息設定成另外一個快取項。這樣做的話就需要訪問 HomeController 的快取物件,此時狀態引數就可以排上用場了。使用狀態物件,你可以對 HomeController 的快取物件進行控制,並使用 Set() 增加一個 callbackMessage 快取項。

你可以通過 Show() 這個 action 來訪問到 callbackMessage,如下所示:

public IActionResult Show()
{
  string timestamp = cache.Get<string>("timestamp");
  ViewData["callbackMessage"] = 
    cache.Get<string>("callbackMessage");
  return View("Show",timestamp);
}

最後就可以在 Show 檢視中顯示出來了:

<h1>TimeStamp : @Model</h1>

<h3>@ViewData["callbackMessage"]</h3>

<h2>@Html.ActionLink("Go back", "Index", "Home")</h2>

為了測試回撥,我們需要執行應用程式並跳轉到 /Home/Index。然後跳轉到 /Home/Show,並不停地重新整理瀏覽器。在某些時間點,由於其 AbsoluteExpiration 設定之後,時間戳專案將會過期。你會看到這樣的 callbackMessage:

9. 你可以設定快取項的優先順序

正如你可以設定快取項的到期策略一樣,你還可以為快取項賦予優先順序。如果伺服器記憶體緊缺的話,就會基於此優先順序對快取項進行清理以回收記憶體。 想要設定優先順序的話,就要再一次用到 MemoryCacheEntryOptions。

MemoryCacheEntryOptions options = 
new MemoryCacheEntryOptions();
options.Priority = CacheItemPriority.Normal;
cache.Set<string>("timestamp", 
DateTime.Now.ToString(), options);

MemoryCacheEntryOptions 的 Priority 屬性讓你可以使用 CacheItemPriority 列舉來設定快取項的優先順序取值。可選的值有 Low,Normal,High 以及 NeverRemove。

10. 你可以設定多個快取項之間的依賴關係

你還可以對一組快取專案之間的依賴關係進行設定,例如在刪除一個快取項時,所有依賴的項也會被刪除。 要是你想要了解它是如何工作的,可以像下面這樣對 Index()這個 action 做一下修改:

public IActionResult Index()
{
    var cts = new CancellationTokenSource();
    cache.Set("cts", cts);

    MemoryCacheEntryOptions options = 
new MemoryCacheEntryOptions();
    options.AddExpirationToken(
new CancellationChangeToken(cts.Token));
    options.RegisterPostEvictionCallback
(MyCallback, this);
    cache.Set<string>("timestamp", 
DateTime.Now.ToString(), options);

    cache.Set<string>("key1", "Hello World!", 
new CancellationChangeToken(cts.Token));
    cache.Set<string>("key2", "Hello Universe!", 
new CancellationChangeToken(cts.Token));

    return View();
}

程式碼首先建立了一個 CancellationTokenSource 物件,該物件被儲存為一個獨立的快取項 cts。然後像之前那樣建立出 MemoryCacheEntryOptions 物件。這時候呼叫 MemoryCacheEntryOptions 的  AddExpirationToken() 方法來指定過期令牌。我們不會在這裡探討 CancellationChangeToken 的細節。可以這樣理解,過期令牌能讓你有權利讓一個快取項過期。如果令牌處於活動狀態的話,則快取項就會在快取中維持,而如果令牌被取消掉了,則該快取項就將從快取中刪除掉。一旦快取項從快取中刪除掉了,MyCallback 就像之前一樣被呼叫。之後程式碼又建立了兩個快取項—— key1 和 key2。在新增這兩個快取項時,Set() 的第三個引數將基於之前所建立的 cts 物件傳遞一個 CancellationChangeToken。

這樣做就意味著這裡我們有了三個鍵 – timestamp 是主鍵,而 key1 和 key2 則依賴於 timestamp。當 timestamp 被刪除時,key1 和 key2 也應該被刪除掉。要刪除 timestamp,你需要在程式碼中的某個地方取消其令牌。我們可以單獨的一個 action(Remove())中進行這樣的操作。

public IActionResult Remove()
{
    CancellationTokenSource cts = 
cache.Get<CancellationTokenSource>("cts");
    cts.Cancel();
    return RedirectToAction("Show");
}

這裡我們先獲取到之前儲存的 CancellationTokenSource 物件,並呼叫它的 Cancel() 方法。這樣做會把 timestamp,key1 以及 key2 都刪除掉。 你可以通過在 Show() 這個 action 中獲取一下所有這三個鍵來確認它們是否已經被刪除掉了。

為了測試這個例子,執行應用程式並導航至 /Home/Index。然後再導航至 /Home/Show,並檢查所有這三個鍵值是否按預期顯示了出來。然後導航至 /Home/ Remove,瀏覽器將被重定向回 /Home/Show。由於 Remove() 取消了令牌,所有的鍵都已經被刪除調了,而現在 Show 檢視會將刪除的原因(TokenExpired)顯示出來,如下所示:

到目前為止就是這些了!筆耕不輟!

相關文章