文章目錄
使用 .NET Framework 內建的 MemoryCache 來 Cache 常用資料 - Part 3 隱藏的效能瓶頸
之前筆記 使用 .NET Framework 內建的 MemoryCache 來 Cache 常用資料 - Part 2 使用 lock 避免 ddos db 解決程式可能 ddos db 的重大缺失,最近重新 review code 時發現一個效能瓶頸:取資料時會 lock 所有 MemoryCache 物件而造成所有 access cache 都被 blocking。
問題發生原因
測試方法有缺陷
測試方法
void Main() { MemoryCache _cache = MemoryCache.Default; _cache.Remove("TableData"); _cache.Remove("Table2Data"); // sleep 時間較久先跑 Task.Run(() => CacheHelper.RefreshTable2Data()); Task.Run( () => CacheHelper.RefreshTableData()); Console.WriteLine("Done"); }
並非直接測試取資料屬性,而是測試更新資料的方法
lock 根物件
取資料方法
/// <summary> /// TableData /// </summary> public static List<Table> TableData { get { MemoryCache _cache = MemoryCache.Default; //加上 lock lock (_cache) { if (!_cache.Contains("TableData")) { RefreshTableData(); } } return _cache.Get("TableData") as List<Table>; } }
lock 了整個 MemoryCache 物件
如何改善
重現問題
修改測試方法
Task.Run(() => CacheHelper.Table2Data.Dump()); Task.Run(() => CacheHelper.TableData.Dump());
多執行緒仍出現依序執行的現象
僅 lock 需要的物件
依 key 來產生唯一個的 lock 物件
// lock 以 key 產生的專屬 lock object,如果 object 過期或不存在時自動 new 出新的 // 如果直接 lock cache[key] 會造成無法寫入 cache 資料 lock (GetCacheObject(key))
如果 MemoryCache[key] 尚未被建立時需先建立
//設定一個 key 用來識別唯一的 lock const string LockKey = "@#$%"; static Object GetCacheObject(string key) { // 取得每個 Key 的鎖定 object string _lockKey =$"{LockKey}{key}"; //仍然會 lock 整個 memorycahce object 但少了取資料過程 lock 時間會縮短 lock (_cache) { // lock object 不存在時直接建立 if (_cache[_lockKey] == null) _cache.Add(_lockKey,new object(),new CacheItemPolicy(){SlidingExpiration = new TimeSpan(0, 10, 0)}); } return _cache[_lockKey]; }
因為有先建立暫存物件的關係,更新判斷流程也需調整
if ((_cache.Get("TableData") as List<Table>) == null ) { RefreshTableData(); }
調整後完整程式碼
public class CacheHelper { private static MemoryCache _cache = MemoryCache.Default; //設定一個 key 用來識別唯一的 lock const string LockKey = "@#$%"; /// <summary> /// 更新 TableData /// </summary> public static void RefreshTableData() { Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Start Job,Now:{DateTime.Now}"); Thread.Sleep(3000); //移除 cache 中資料 _cache.Remove("TableData"); //存取資料 List<Table> listAgency= new List<UserQuery.Table>(); for (int i = 0; i < 3; i++) { listAgency.Add(new Table { Id = Guid.NewGuid(), Name = $"{Guid.NewGuid()}"}); } //設定 cache 過期時間 CacheItemPolicy cacheItemPolicy = new CacheItemPolicy() { AbsoluteExpiration = DateTime.Now.AddDays(1) }; //加入 cache _cache.Add("TableData", listAgency, cacheItemPolicy); Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Stop Job,Now:{DateTime.Now}"); Console.WriteLine("OK"); } /// <summary> /// 依 key 取得 cache object /// </summary> static Object GetCacheObject(string key) { // 取得每個 Key 的鎖定 object string _lockKey =$"{LockKey}{key}"; //仍然會 lock 整個 memorycahce object 但少了取資料過程 lock 時間會縮短 lock (_cache) { // lock object 不存在時直接建立 if (_cache[_lockKey] == null) _cache.Add(_lockKey,new object(),new CacheItemPolicy(){SlidingExpiration = new TimeSpan(0, 10, 0)}); } return _cache[_lockKey]; } /// <summary> /// TableData /// </summary> public static List<Table> TableData { get { //加上 lock lock (GetCacheObject("TableData")) { if ((_cache.Get("TableData") as List<Table>) == null ) { RefreshTableData(); } } return _cache.Get("TableData") as List<Table>; } } }
測試結果
兩個 thread 同時啟動 而不是像之前的測試出現等待前一個 thread 執行結束才啟動的現象
測試及使用的完整程式碼(可以使用 linqpad 直接執行)
void Main() { // //先清除 cache 中資料 MemoryCache _cache = MemoryCache.Default; _cache.Remove("TableData"); _cache.Remove("Table2Data"); Task.Run(() => CacheHelper.Table2Data.Dump()); Task.Run(() => CacheHelper.TableData.Dump()); Console.WriteLine("Done"); } public class CacheHelper { private static MemoryCache _cache = MemoryCache.Default; //設定一個 key 用來識別唯一的 lock const string LockKey = "@#$%"; /// <summary> /// 更新 TableData /// </summary> public static void RefreshTableData() { Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Start Job,Now:{DateTime.Now}"); Thread.Sleep(3000); //移除 cache 中資料 _cache.Remove("TableData"); //存取資料 List<Table> listAgency= new List<UserQuery.Table>(); for (int i = 0; i < 3; i++) { listAgency.Add(new Table { Id = Guid.NewGuid(), Name = $"{Guid.NewGuid()}"}); } //設定 cache 過期時間 CacheItemPolicy cacheItemPolicy = new CacheItemPolicy() { AbsoluteExpiration = DateTime.Now.AddDays(1) }; //加入 cache _cache.Add("TableData", listAgency, cacheItemPolicy); Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Stop Job,Now:{DateTime.Now}"); Console.WriteLine("OK"); } /// <summary> /// 依 key 取得 cache object /// </summary> static Object GetCacheObject(string key) { // 取得每個 Key 的鎖定 object string _lockKey =$"{LockKey}{key}"; //仍然會 lock 整個 memorycahce object 但少了取資料過程 lock 時間會縮短 lock (_cache) { // lock object 不存在時直接建立 if (_cache[_lockKey] == null) _cache.Add(_lockKey,new object(),new CacheItemPolicy(){SlidingExpiration = new TimeSpan(0, 10, 0)}); } return _cache[_lockKey]; } /// <summary> /// TableData /// </summary> public static List<Table> TableData { get { //加上 lock lock (GetCacheObject("TableData")) { if ((_cache.Get("TableData") as List<Table>) == null ) { RefreshTableData(); } } return _cache.Get("TableData") as List<Table>; } } public static List<Table> Table2Data { get { lock (GetCacheObject("Table2Data")) { if ((_cache.Get("Table2Data") as List<Table>) == null ) { RefreshTable2Data(); } } return _cache.Get("Table2Data") as List<Table>; } } /// <summary> /// 更新 TableData /// </summary> public static void RefreshTable2Data() { Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Start Job,Now:{DateTime.Now}"); Thread.Sleep(10000); //移除 cache 中資料 _cache.Remove("Table2Data"); //存取資料 List<Table> listAgency = new List<UserQuery.Table>(); for (int i = 0; i < 3; i++) { listAgency.Add(new Table { Id = Guid.NewGuid(), Name = $"{Guid.NewGuid()}" }); } //設定 cache 過期時間 CacheItemPolicy cacheItemPolicy = new CacheItemPolicy() { AbsoluteExpiration = DateTime.Now.AddDays(1) }; //加入 cache _cache.Add("Table2Data", listAgency, cacheItemPolicy); Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Stop Job,Now:{DateTime.Now}"); Console.WriteLine("OK"); } } public class Table { public Guid Id{ get; set; } public string Name { get; set; } }
心得
又再次體會到寫筆記的好處,如果有個想法立馬紀錄一下,過程中可能還可以順道解決以前自己埋下的地雷XD
參考資料
文章作者 Yowko Tsai
上次更新 2021-08-18
授權合約
本部落格 (Yowko's Notes) 所有的文章內容(包含圖片),任何轉載行為,必須通知並獲本部落格作者 (Yowko Tsai) 的同意始得轉載,且轉載皆須註明出處與作者。
Yowko's Notes 由 Yowko Tsai 製作,以創用CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款 釋出。