在B/S结构中要实现定时器(或者说是一个事务)实在不是一件好办的事。可当你在网上搜索“ASP.NET定时器”的时候,你会发现搜索结果是如此的多,可这大多数结果中的代码健壮性都是那样的脆弱——没有考虑诸如IIS进程的自然消亡、IIS进程的故障崩溃、重启服务器等等因素。这些不可捕捉的错误会让你的定时器失去定时的功能。可能会有人问,为什么要用Web程序做计时器呢?自己添加一个Windows服务或者在数据库中添加一个作业不就解决了么?可事实上,又有多少人对自己购买的虚拟主机具有如此权限呢?如果你用的是自己的服务器或者更高权限的服务器,那此文对你的确是多余了。
我曾经为ASP.NET中的Application_End感到窃喜,以为有了它,以上提及的自然消亡、故障崩溃都可迎刃而解了,可经过自己的实践发现那个Application_End真的是很无助——你在该代码段真的是nothing to do。国内搜索到的所谓ASP.NET定时器几乎都是采用System.Thread进程监控实现的,要让Thread在IIS进程中保持长期(比如1年365天时时刻刻)的稳定性实在是不敢打包票。
最后我在CodeProject上找到一篇文章《Simulate a Windows Service using ASP.NET to run scheduled jobs》(在ASP.NET中模拟Widnows服务执行计划任务)。在这篇文章中,他利用缓存的过期来实现定时器功能。在ASP.NET中,当你添加缓存的时候,会有一个缓存回调函数让你同时加入,表示当缓存过期被系统清除的时候,你可以在这个回调函数中做一些事情。缓存回调函数正是实现ASP.NET定时器的重要基石。假设我们在00:00:00时刻加入一个缓存对象,缓存的过期时间我们设置为00:02:00,当到达00:02:00或者更提前的时候,缓存将被系统回收,当系统通知缓存被回收的时候我们再次添加一个缓存,如此循环往复(缓存被添加->过期->再次添加->过期……)从而模拟出了钟表秒针。当每一次缓存过期的时候我们都检查是否有需要执行的事务,有则执行,然后再次添加一个缓存,从而模拟出了定时器。
但是要想添加缓存,必须有用户请求Web服务器才能使用Cache。所以,在缓存过期的时候,除了检查是否有事务需要执行外,还要用WebClient(或者其他方法)请求我们的Web页面,在页面被请求的时候添加缓存。
上面的思路已经基本模拟出了定时器的功能,但对于服务器重启、IIS故障仍然没有解决。《Simulate a Windows Service using ASP.NET to run scheduled jobs》中提到,可以把Google Robots抓取你网页的频率设置大一点,当出现上述情况时,以尽快的速度产生Web请求。只要有人(或者机器人)在访问你的Web程序,缓存定时器(我们把它称为“缓存时钟”吧)就可以正常工作,而他的工作是由系统自行维护的,而不是Thread进程维护,所以稳定性会有很大的提高。经过我的实践,只要服务器没有重启、IIS进程没有崩溃,上述中采用在缓冲过期时请求Web页面(WebClient)的行为是不会让IIS进程自然消亡的(缓存过期时间长短、服务器配置也会影响)。(下划线文字的描述与事实不符,观测的结果可能受到网络蜘蛛的影响)具体如何实现,请看下文。
复制代码 using System; using System.Data; using System.Net; using System.Configuration; using System.Web; using System.Text; using System.Web.Caching; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public classCacheTimer { const string logPath = @"d:\cacheLogs\cacheLog.txt"; const string cacheKeyName = "myCacheTimer";//缓存的名字 const string rawUrl = "http://localhost/CacheTimer/CacheTimmer.aspx";//缓存过期时请求的页面地址 public void RegistCacheTimmer() { if (HttpContext.Current.Cache[cacheKeyName] != null) return; try { HttpContext.Current.Cache.Add(cacheKeyName, '1', null, DateTime.Now.AddSeconds(12), Cache.NoSlidingExpiration, CacheItemPriority.High, new CacheItemRemovedCallback(CacheItemOnRemoved)); WriteCacheLog("缓存时钟注册成功"); } catch (Exception e) { WriteCacheLog("缓存时钟注册失败:" + e.Message); } } protected void CacheItemOnRemoved(string key, object value, CacheItemRemovedReason reason) { if (key == cacheKeyName) { ExeJob(); RequestWebPage(); } } protected void ExeJob() { WriteCacheLog("被执行了"); //如果日期的天数能被7整除、下午13点到13点15分的时候,则执行 if (DateTime.Now.Day % 7 == 0 && DateTime.Now.Hour == 13 && DateTime.Now.Minute <= 15) { //执行数据操作或者其他任务 } } protected void RequestWebPage() { WebClient wc = new WebClient(); wc.DownloadData(rawUrl); } public static void WriteCacheLog(string logInfor) { try { System.IO.StreamWriter sw = new System.IO.StreamWriter(logPath, true, Encoding.GetEncoding("GB2312")); sw.WriteLine(DateTime.Now.ToString() + " " + logInfor); sw.Close(); sw.Dispose(); } catch { //调试的时候注意是否对该文件有写权限 } } }
- 添加缓存:RegistCacheTimmer
- 请求页面:RequestWebPage
- 执行任务:ExeJob
- 缓存过期时候执行请求页、执行任务:CacheItemOnRemoved
- 写日志的方法:WriteCacheLog
然后在Global.asax文件中添加下列代码段:
复制代码 void Application_BeginRequest(Object sender, EventArgs e) { CacheTimer registTimer = new CacheTimer(); registTimer.RegistCacheTimmer(); }
但是如果我们的任务在没有执行完毕,IIS进程崩溃或者服务器重启了,怎么办?我的建议是把任务写人文件(包括数据库),检查的时候直接检查任务文件就可以保证重启IIS进程,任务不丢失了。
感谢《Simulate a Windows Service using ASP.NET to run scheduled jobs》的作者提供此定时器的解决方法。
Goberl缓存时钟源码下载