ASP.NET 中的定時執行任務

溫謙發表於2013-05-12

FluentScheduler is a better solution for cronjob in asp.net . https://www.nuget.org/packages/FluentScheduler

在一個網站中,經常需要設定一些任務能夠定時執行。

比如一些統計資料,例如網站會員的積分,積分的計算可能涉及很多因素和變數,而且需要累計很多條記錄才能得到結果,如果每次顯示會員資訊的時候,都是實時計算,那麼可想而知計算量非常大。如果還需要對所有會員的積分排序後在顯示列表,那就更是不可能完成的任務了。因此,顯然需要把積分的計算和顯示分開,計算不是實時的,當然這樣就會導致一定的不一致情況發生。這就要看對一致性的要求了。對於不贏房子不贏地的會員積分,稍微更新晚一點倒是無所謂的。

再比如,現在很多網站會根據會員的行為,頒發一些榮譽勳章,頒發的條件都是根據會員在網站行為來判斷的。顯然不能當一名會員在網站做了任何一個行為,都立即計算一次是否符合頒發各種勳章的條件。因此,也只能定期地計算一次。

因此關鍵就在於,非實時的計算會員積分,必定要求定時地執行,比如每半小時或一分鐘計算一次,具體時間間隔就根據業務的實際需要了。

那麼對於 ASP.NET 開發的網站,應該如何實現在後臺定時執行某個任務呢? 如果你在Google上用 cron job ASP.NET 搜尋,會搜到很多帖子,很顯眼的是 Stack Overflow 上的問題,被大多數人頂的答案是這樣的:

ASP.NET 不是完成這個工作的工具,你應該單獨做一個獨立的程式,然後通過設定為任務計劃,使該程式定期執行,要麼乾脆做成Service。而 ASP.NET 應該只被用來完成 請求/響應 模型的工作。

這個答案很有道理,確實應該如此。但是對於一個規模並不大的網站,如果能夠在 ASP.NET 內部解決這個問題,還是很有吸引力的。

實際上解決方案還是有的,並且是經過實際應用的,StackOverflow 的 Jeff Atwood 發過一個帖子,講解了一個方案,並且提到,StackOverflow 早期就是用這個方案每60秒鐘計算會員的徽章等資料的。當然,他也提到後隨著規模增大,他們已經不使用這個方案了。有興趣的可以參見:http://blog.stackoverflow.com/2008/07/easy-background-tasks-in-aspnet/

我這裡簡單描述一下這個方案,我用lambda表示式的方式改寫了一下,程式碼看起來簡單一些。

public static void AddTask(int seconds, Action todo)
{
    HttpRuntime.Cache.Insert(
        Guid.NewGuid().ToString(),
        0,
        null,
        DateTime.Now.AddSeconds(seconds),
        Cache.NoSlidingExpiration,
        CacheItemPriority.NotRemovable,
        (key, value, reason) => 
        {
            todo.Invoke();
            AddTask(seconds, todo);
        });
}

就這麼一個非常簡單的函式,然後在 global.asax 裡面的呼叫這個函式即可。

void x()
{
    Debug.WriteLine(DateTime.Now);
}

void x2()
{
    Debug.WriteLine("---" + DateTime.Now);
}

protected void Application_Start()
{
    AddTask(40, x);
    AddTask(60, x2);
    //.......
}

上面的程式碼設定了兩個void型別的方法,然後在 Application_Start() 中設定二者的執行時間間隔,一個40秒執行一次,一個60秒執行一次。在IDE的output視窗中,可以看到執行的結果如下:

2013/5/12 18:09:00
---2013/5/12 18:09:20
2013/5/12 18:09:40
---2013/5/12 18:10:20
2013/5/12 18:10:20
2013/5/12 18:11:00
---2013/5/12 18:11:20
2013/5/12 18:11:40
2013/5/12 18:12:20
---2013/5/12 18:12:20

可以看到,一個40秒列印一行,另一個60秒列印一行。具體要做什麼,就看你的了!

需要說明的是,這個方案的原理是利用了ASP.NET的快取機制,向ASP.NET的快取集合中新增一個快取項,設定過期的時間,當到達這個時間的時候,會執行一個回撥函式,就把你要做的事情放到這個回撥函式裡,並且在執行要做的事兒之後,再次加入一個快取項,如此不斷地往復進行。

這裡存在一個問題就是,在預設情況下,ASP.NET 每20秒回收一次快取項,因此,這個方法不能把時間間隔設定的太小,我覺得至少分鐘級別比較合適。

最後需要再次提醒的是,這個方法可以算是一個 hack 方法,並不是一個常規的優雅的做法,在 Jeff Atwood 的帖子後面,很多人提出了反對意見,大家有興趣可以看一看。而且在這個帖子的半年多已後的2009年初,Stack Overflow 就不再使用這個方法,而改用更正常的方式來實現了。但是你要知道,Stack Overflow 的訪問量在初期發展異常迅猛,在2009年初的時候,Alexa 排名已經升到全世界的1000多名了。Stack Overflow 半年的路,很多網站可能要走很多年,甚至很多年也到不了,因為全世界也就1000個網站可以排到前1000名。

enter image description here

好了,希望這個小技巧對你有所幫助。說實話,這個技巧可以解決大問題滴。

相關文章