Cache 回收更新機制不外乎幾種做法 - 絕對逾時期限、閒置固定時間後失效、鎖定檔案或資料庫異動後失效。以 .NET MemoryCache 為例,分別對映到 CachePolicy 的 AbsoluteExpirationSlidingExpiration 以及 ChangeMonitor。(延伸閱讀:使用 FileChangeMonitor 實現檔案資料快取自動更新)

針對會被不斷存取的項目,設定 SlidingExpiration 註定永遠不會逾期自動更新;ChangeMonitor 時效能最好,機制最複雜,實作起來頗為費事;相較之下, AbsoluteExipiration 是我最常用寫做法,每次讀取存入快取後,設定五分、20 分鐘到數小時不等的有效時間(依查詢成本及資料更新頻率而定),簡單好寫,資料異動後即使不清除快取,等上幾分鐘或半小時一小時再試,便能讀到新資料,「等一下再試看看」的做法日常生活中比比皆是,使用者多能接受。對於非關鍵性或即時性不高的資料應用,AbsoluteExipiration 算是很容易實作的解決方案。(延伸閱讀:改良式 GetCachableData 可快取查詢函式)

但這個做法有個小缺點,「該等幾分鐘再試?」要憑運氣,Cache 20 分鐘的資料,你可能需要等 19 分鐘 59 秒 (如果資料更新前一秒才剛存入快取),也可能只需要等一秒,性急的使用者只能多按幾次 F5 試看看,運氣不好失敗次數多了,心情難免煩躁。

最近,我想到一個好點子:若 Cache 定期更新時機具有可預測性,比照固定班次準點發車的公車,使用者能精準掌握等待及重試時間,便能提升系統友善度。原理很簡單,保留十分鐘的快取,就固定在每個小時的 00、10、20、30、40、50 分逾時並更新、若資料更新但在 10:08 仍讀到舊資料,等 10:11 再試一定是新資料。若設 20 分鐘快取,就等 21、41、01 分再試。當更新時機可被預期,不用反覆嘗試,使用者的體驗便能提升。當然,每次資料更新後立即清除 Cache 還是最完美的解決方案,當無法實現時,準點發車會又比等多久憑運氣的公車讓人滿意。

要實現這個想法很簡單,我寫了一個擴充函式 NextSlotStartTime(間隔分鐘數) 用來決定下次更新時間。DateTime.Now.NextSlotStartTime(20),表示若 Cache 每 20 分鐘到期,下次更新的時間。另外,為避免所有 Cache 都在準點到期,萬箭齊發同時查資料庫形成尖峰,對系統造成壓力,我預設要再加上 0-29 秒(長度可調)的隨機延遲,用以錯開讀取資料更新快取時機,使用者只需養成準點後再多等幾秒的習慣,便能換取較平穩的系統負載,

函式及測試程式如下:

using System;

namespace FixedSlotDurationCache
{
    class Program
    {
        static void Main(string[] args)
        {
            var rnd = new Random();
            var durations = new int[] { 5, 10, 20, 60, 120 };
            for (int i = 0; i < 10; i++)
            {
                var t = DateTime.Today.AddSeconds(rnd.Next(86400));
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine($"*** Time: {t:HH:mm:ss} ***");
                Console.ForegroundColor = ConsoleColor.White;
                foreach (var d in durations)
                {
                    var timeout = t.NextSlotStartTime(d);
                    Console.Write($"{d:00}m -> {timeout:HH:mm:ss} ");
                }
                Console.WriteLine();
                Console.WriteLine();
            }
            Console.ReadLine();
        }
    }

    public static class FixedSlotCacheUtil
    {
        static Random rnd = new Random();
        /// <summary>
        /// 下個時間格隔起點
        /// </summary>
        /// <param name="time">現在時間或推算基準</param>
        /// <param name="slotMins">時間格大小(以分鐘表示)</param>
        /// <param name="randomDelaySecs">隨機延遲</param>
        /// <returns>下個時間格的起算時間</returns>
        public static DateTime NextSlotStartTime(this DateTime time, int slotMins, int randomDelaySecs = 30)
        {
            var slotSecs = slotMins * 60;
            var remainingSecs = slotSecs - ((time - time.Date).TotalSeconds % slotSecs);
            //加上 Delay
            if (randomDelaySecs > 0) 
                remainingSecs += rnd.Next(randomDelaySecs);
            return time.AddSeconds(remainingSecs);
        }
    }
}

在專案試行一段時間,感覺還不錯,分享給大家。

Tips of setting fixed time slot for absolute expiration time cache to provider better user experience.


Comments

# by 阿光

黑暗大哥,我目前有這個需求,就是我的環境是.NET Framework 4.7.2,我的WEB API要對某些Object做MemoryCache,但是就像你講的 "所有 Cache 都在準點到期,萬箭齊發同時查資料庫形成尖峰"。 所以我有看到你最後的 FixedSlotDurationCache 這個功能。請問我如何在這個Concole Mode裡去更新我Web API的Cache呢?

# by Jeffrey

to 阿光,這個小工具函數是用來計算 Cache 到期時間,以實現固定在每個小時的 00、10、20、30、40、50 分更新邏輯(但加上隨機延遲防止尖峰) ,要實際應用可參考 https://blog.darkthread.net/blog/improved-getcachabledata/ ,用它計算 T GetCachableData<T>(string key, Func<T> callback, DateTimeOffset absExpire, bool forceRefresh = false) 中的 absExpire 參數。

# by 阿光

黑暗大哥,感謝提供參考讓我試試。

Post a comment