基于C#的计时管理器
問題
我們使用各種系統(tǒng)時候會遇到以下問題:
12306上購買火車票如果15分鐘內(nèi)未完成支付則訂單自動取消。
會議場館預定座位,如果10分鐘內(nèi)未完成支付則預定自動取消。
在指定時間之后,我需要執(zhí)行一項任務(wù)。
我之前做的很多系統(tǒng),往往都是定期執(zhí)行一個特定任務(wù)。而上訴問題都涉及到滑動窗口時間的定時任務(wù)。
比如:我早上10點20分預定了一張火車票,我需要在15分鐘內(nèi)支付完成,否則訂單會被取消。同一時間可能會有成百上千的人預定其他火車票,我需要在每個人的15分鐘期限達時候執(zhí)行檢查,如果還未支付則自動取消訂單。
方案
我們搞清楚了要解決的問題以后,我們來思考方案。有經(jīng)驗的程序員會立即思考出下面的方案:
使用消息隊列的延遲投送功能,每個訂單添加成功后發(fā)送一個延遲15分鐘的延遲消息。訂單狀態(tài)處理器15分鐘后收到消息,檢查支付狀態(tài),如果未支付則取消訂單。
Redis也有類似的功能,原理大致相同。
但我不想使用消息隊列的功能,因為延遲消息投送是一種技術(shù)實現(xiàn),我希望用代碼反應(yīng)這種業(yè)務(wù)實現(xiàn),所以用純代碼來處理他。(我并不是為了從新發(fā)明車輪,因為這是一種業(yè)務(wù)需求,會有變化擴展的需要,所以決定自己嘗試做一下增加經(jīng)驗)
算法思路:
我們的需求定時時間都在15分鐘以內(nèi),假定都沒有超過1個小時的或者幾天的。(如果超過1個小時的,可以擴展這個設(shè)計,這篇暫時不展開討論)
我們可考慮將一個小時分成3600秒,每秒代表一個位置來存儲所有到期的訂單,當下單的時候根據(jù)當前時間 加上 15分鐘時間間隔,我們就可以得到15分鐘以后的時間,將這個訂單添加到對應(yīng)的位置上。
數(shù)據(jù)結(jié)構(gòu)選擇:
我們選擇C#中提供的最新的并發(fā)字典作為基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),Key值是3600秒中的每一秒的數(shù)值,內(nèi)容是一個隊列用于存放該時間點的所有訂單。
public ConcurrentDictionary<int, ConcurrentQueue<IJob>> jobs = new ConcurrentDictionary<int, ConcurrentQueue<IJob>>();數(shù)據(jù)結(jié)構(gòu)我們思考好了,其實功能就完成了大半了,代碼的設(shè)計也就基本定下來了。
代碼實現(xiàn)(我喜歡使用控制臺應(yīng)用程序做實驗)
建立一個定時管理器
public class TimerManager{//并發(fā)字典存儲需要檢查的任務(wù)(這里可以是訂單檢查任務(wù),每個任務(wù)可以包含一個訂單Id)public ConcurrentDictionary<int, ConcurrentQueue<IJob>> jobs = new ConcurrentDictionary<int, ConcurrentQueue<IJob>>();private Timer timer;public TimerManager(){//每間隔1秒鐘執(zhí)行一次。和當前時間同步。timer = new Timer(ProcessJobs, null, 0, 1000);}}增加一個任務(wù)到字典
/// <summary>/// 增加一個任務(wù)到時間字典中/// </summary>/// <param name="timeKey">根據(jù)延遲時間計算出的key值</param>/// <param name="duetime">毫秒單位</param>/// <exception cref="NotImplementedException"></exception>public void AddJob(IJob job, TimeSpan duetime){var key = GetKey(duetime);ConcurrentQueue<IJob> queue = new ConcurrentQueue<IJob>();queue.Enqueue(job);jobs.AddOrUpdate(key, queue, (key, jobs) =>{jobs.Enqueue(job);return jobs;});}根據(jù)時間計算Key的方法
/// <summary>/// 根據(jù)延遲時間生成當前鍵值/// </summary>/// <param name="duetime"></param>/// <returns></returns>private int GetKey(TimeSpan duetime){var currentDateTime = DateTime.Now;//到期時間var targetDateTime = currentDateTime.Add(duetime);//不要忘了把分鐘換算成秒,然后在和延遲時間相加就得到Keyvar key = targetDateTime.Minute * 60 + targetDateTime.Second;return key;}將任務(wù)添加到字典
/// <summary>/// 增加一個任務(wù)到時間字典中/// </summary>/// <param name="job">需要執(zhí)行的任務(wù)</param>/// <param name="duetime">多少時間間隔后檢查</param>public void AddJob(IJob job, TimeSpan duetime){var key = GetKey(duetime);ConcurrentQueue<IJob> queue = new ConcurrentQueue<IJob>();queue.Enqueue(job);//這是并發(fā)字典的方法,這里就是當Key不存在就增加新的值進去,當Key存在就在Key的隊列中增加一個新任務(wù)jobs.AddOrUpdate(key, queue, (key, jobs) =>{jobs.Enqueue(job);return jobs;});}計時器每秒執(zhí)行時處理任務(wù)的方法,循環(huán)從隊列中取出任務(wù)直到所有任務(wù)處理完畢。
private async void ProcessJobs(object state){//根據(jù)當前時間計算Key值var key = DateTime.Now.Minute * 60 + DateTime.Now.Second;Console.WriteLine(key);//查找Key值對應(yīng)的任務(wù)隊列并處理。bool keyExists = jobs.TryGetValue(key, out var jobQueue);if (keyExists){IJob job;while(jobQueue.TryDequeue(out job)){await job.Run();}}}代碼中設(shè)計IJob 和Job的一個實現(xiàn),為了易于理解,這個job沒有做太多事情。如果需要擴展去檢查訂單,可以在這里記錄訂單Id,創(chuàng)建任務(wù)的時候?qū)⒂唵蜪D和任務(wù)關(guān)聯(lián),這樣定時器處理這個任務(wù)的時候能找到對應(yīng)訂單了。
public interface IJob{Task Run();}/// <summary>/// 代表一個工作/// </summary>public class Job : IJob{public Guid JobId { get; set; }public Job(){JobId = Guid.NewGuid();}public async Task Run(){Console.WriteLine(" Job Id: " + JobId.ToString() + " is running.");await Task.Delay(2000);Console.WriteLine(" Job Id:" + JobId.ToString() + " have completed.");}}主程序Programe中調(diào)用定時管理器
using TimerTest;Console.WriteLine("Hello, World!");TimerManager timerManager = new TimerManager();Job job1 = new Job();// 添加一個任務(wù)1分鐘后執(zhí)行 timerManager.AddJob( job1, TimeSpan.FromMinutes(1));// 在添加另一個任務(wù)2分鐘后執(zhí)行 Job job2 = new Job(); timerManager.AddJob(job2, TimeSpan.FromMinutes(2));Console.ReadLine();執(zhí)行結(jié)果
結(jié)果中可以看到, 任務(wù)1 在1014的鍵值上被處理,1014的鍵值對應(yīng)的時間是 16:54 秒,也就是在我運行這個程序1分鐘后。
| 第一次任務(wù)(計時1分鐘) | 15:54 | 16:54 |
| 第二次任務(wù)(計時2分鐘) | 15:54 | 17:54 |
任務(wù)2 在 1074的鍵值上被處理,1074對應(yīng)的時間是 17:54 秒 執(zhí)行。從上表可以看出程序正常運行得出結(jié)果。
總結(jié)
這是一個簡單的控制臺程序驗證了這個定時管理器的實現(xiàn)方法,我們將1個小時分成3600秒,每一秒對應(yīng)一個Key值,在這個值上我們存儲需要被處理的任務(wù)。在增加任務(wù)時候,我們也用同樣的算法確定這個Key值。處理的時候根據(jù)當前時間計算除Key值進行處理。
這樣的話,在真實場景中,我們有3600個Key值可以存儲每一秒鐘用戶提交的所有訂單,時間沒走過1秒我就處理對應(yīng)的任務(wù)。
后續(xù)可以完善的地方
我們可以將這個類添加到ASP.NET MVC中,使用依賴注入為單實例生命周期,并發(fā)字典和并發(fā)隊列是線程安全的,所以這里可以放心使用。
我們可以擴展Job方法,根據(jù)業(yè)務(wù)邏輯添加更多的信息以便于處理。例如處理訂單的ID,或其他什么業(yè)務(wù)ID。
處理任務(wù)的方法可以采用多個消費者并發(fā)執(zhí)行,增加處理速度。
可以將任務(wù)實體存儲到數(shù)據(jù)庫,以便于應(yīng)對突發(fā)宕機事故可以快速重建任務(wù)。
當然我們也可以用Hangfire來輕松實現(xiàn)這個業(yè)務(wù)。
var jobId = BackgroundJob.Schedule(() => Console.WriteLine("Delayed!"),TimeSpan.FromDays(7)); //這里改成分鐘就好了
最后祝.NET 20周年快樂。
總結(jié)
以上是生活随笔為你收集整理的基于C#的计时管理器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Elasticsearch数据库
- 下一篇: ABP vNext微服务架构详细教程——