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