ASP.NET Cache
测试一阵子的专案小明接到回报,
系统明明有写设定快取机制, 并且快取内容保存10 秒,
按照道理说, 系统一分钟读取资料库最多应该是6 次,
但是资料库追蹤纪录显示系统一分钟内读取了400次以上.
小明所写的快取程式片段如下
public class FoodList{ public List<Data> GetFoodList(DateTime date) { var data = (List<Data>)cache["foodList"]; if( data == null ) { data = ReadFoodListFromDatabase(); var policy = new CacheItemPolicy(); policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10); cache.Set("foodList", data, policy); } return data; }}
观察到这段快取程式, 基本上是没有问题, 快取机制并不是无效的.
但是作为应用在生产正式环境当中, 没有什么是如此简单.
首先, 当在正式环境中通常会有很多个使用者同时在线上,
故有可能在同一个时间内, 系统同时得到多个请求(Request).
假设有10 个使用者同时要求读取GetFoodList 资料.
然后这10 个请求(Request)就会同时呼叫GetFoodList() 方法.
当10 个请求(Request)在GetFoodList() 取得过期快取资料的时候,
这时候10 个请求(Request)就会同时执行ReadFoodListFromDatabase().
这10 次取得资料动作只有一次是必要的, 其余9 次将取得相同结果覆写同一Cache,
平白消耗资源.
所以虽然专案程式有设定按照小明的期望系统一分钟读取资料库最多应该是6 次,
但实际上有可能却是6 次以上.
所以我们该怎么做, 才能让专案程式一分钟最多读取资料库6 次呢?
于是小明利用C# lock 陈述式, 修改程式码为
public List<Data> GetFoodList(DateTime date){ List<Data> data = null; lock(_sync) { data = (List<Data>)cache["foodList"]; if( data == null ) { data = ReadFoodListFromDatabase(); var policy = new CacheItemPolicy(); policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10); cache.Set("foodList", data, policy); } } return data;}
有了这个, 当你试图取得资料的时候,
如果同一个资料正在被另一个线程创建, 你将等待另一个完成.
然后你将获得由另一个线程创建的已快取的资料.
缺点
这个做法有缺点,
假设从数据库中获取资料需要10秒钟,
当你试图取得资料的时候,
如果同一个资料正在被另一个线程创建, 你将等待另一个完成.
也就是说你就要等待10 秒之后才能拿到由另一个线程创建的已快取的资料.
所以对高乘载压力的系统来说这效能并不好.
更好的解决方案
现在我们知道了缺点, 让我们继续寻找更好的解决方案.
首先我们可以另外建立读取资料库的任务(Task), 并利用AutoResetEvent 通知这个任务(Task) 去检查快取资料
var task = new Task(()=> { while( _cacheCheckSign.WaitOne() ) { if( 快取资料过期 ) { _data = ReadFoodListFromDatabase(); } }}).Start();
然后在GetFoodList() 方法内,
只发出一个讯号通知给任务(Task) 去检查快取工作,
不管快取资料有没有过期, 直接回传快取资料.
AutoResetEvent _cacheCheckSign = new AutoResetEvent(false);public List<Data> GetFoodList(DateTime date){ _cacheCheckSign.Set(); return _data;}
改用这方法, 即使同时三条Thread 呼叫GetFoodList() 只会触发一次读取资料库的动作,
可减少高承载系统产生重複读取资料的压力.