這種做法適用於對數據操作實時性要求不高的場景,在實際場景中還有一種比較常用的場景就是我們需要在某一個時間點立即執行某個操作,比如商城做搶購活動,同時開啟多個活動在不同的時間點開始促銷。如果我們采用輪訓數據庫的方式來實現的話會出現處理數據不及時的情況,因為每次都需要從數據庫撈取一批次的數據,根據狀態或者設定的活動開啟時間循環比對,如果達到時間點就更新數據狀態,開啟活動,每一批次處理的數據都需要時間,很容易就會在某一個活動已經到達開啟的時間點,但是job執行不及時導致活動的開啟時間晚於設定的時間點,誤差根據數據量以及內部邏輯的復雜度會遞增。這樣就會導致某一個活動在設定的開啟時間點沒有准時開啟,如果是商城做搶購倒計時活動的話,這中延遲對客戶來說是不被接受的。下面是我最近做的H5 商城的實例,這是一個搶購活動的列表頁,多個活動在不同時間點開啟或結束。
這是進行中的活動:
這是就緒狀態,等待開啟的活動:
我們想要在活動設定的某一個時間點准時開啟,就需要使用Quartz 中的另外一種方式來配置Job 在固定時間點執行。
在次之前我們還要考慮的一個問題就是搶購的活動是通過后台添加的,隨時都有可能增加,所以我們不僅僅是只從數據庫撈一次活動的數據,而是需要定時輪訓數據庫找出需要執行的活動,根據后台設定的開啟或者結束時間,添加到Quartz的調度隊列,讓它在固定時間點自己執行。
看到這里大家可能就要問開頭我們就說到不采用輪訓的方式來做,為什么這里又要說輪訓。注意了,我開始提到的是不輪詢每一個活動,在滿足開啟條件(狀態,開啟/結束時間)的情況下再開啟。而這里說到的輪詢指的是輪詢有沒有新添加進來的活動,這是完全不一樣的概念。
閑話不多說,上代碼。先按照前一篇中講到的輪詢方式新建一個MonitorJob:
namespace JobSchedule.JobMonitorSchedule { public class JobMonitorJob : IJob { NLog.Logger log = NLog.LogManager.GetCurrentClassLogger(); public void Execute(IJobExecutionContext context) { log.Info("監控Job開啟執行------------"); var processDataList = FlashItemOfflineDBHelper.GetOfflineFlashPromotion(); if (processDataList != null && processDataList.Count > 0) { processDataList.ForEach(data => { if (data.Status == 2) { if (!ScheduleBase.Scheduler.CheckExists(JobKey.Create("上線商品作業:" + data.SysNo, "定時觸發作業組" + +data.SysNo))) { var job = JobBuilder.Create(typeof(ItemOnlineJob)) .WithIdentity("上線商品作業:" + data.SysNo, "定時觸發作業組" + +data.SysNo) .UsingJobData("ItemSysNo", data.SysNo) .Build(); var trigger = TriggerBuilder.Create() .WithIdentity("上線商品作業Trigger" + data.SysNo, "作業觸發器" + data.SysNo) .StartAt(data.PromotionStartTime.AddSeconds(ConstValue.ItemOnlineStartOffset)) .Build(); ScheduleBase.Scheduler.ScheduleJob(job, trigger); log.Info(string.Format("監控Job開啟執行,商品上線作業已加入調度池, 活動編號:{0},活動名稱:{1}, 活動開始時間:{2}", data.SysNo, data.PromotionName, data.PromotionStartTime)); } } if (data.Status == 3) { if (!ScheduleBase.Scheduler.CheckExists(JobKey.Create("下線商品作業:" + data.SysNo, "定時觸發作業組" + +data.SysNo))) { var job = JobBuilder.Create(typeof(ItemOfflineJob)) .WithIdentity("下線商品作業:" + data.SysNo, "定時觸發作業組" + +data.SysNo) .UsingJobData("ItemSysNo", data.SysNo) .Build(); var trigger = TriggerBuilder.Create() .WithIdentity("下線商品作業Trigger:" + data.SysNo, "作業觸發器" + data.SysNo) .StartAt(data.PromotionEndTime.AddSeconds(ConstValue.ItemOfflineStartOffset)) .Build(); ScheduleBase.Scheduler.ScheduleJob(job, trigger); log.Info(string.Format("監控Job開啟執行,商品下線作業已加入調度池, 活動編號:{0},活動名稱:{1}, 活動結束時間:{2}", data.SysNo, data.PromotionName, data.PromotionEndTime)); } } }); } } } }
根據每一個活動的狀態來判斷是需要加入到開啟隊列的,還是加入到結束隊列的(2:就緒狀態的活動,即將要開啟;3:已經開啟的活動,即將要結束) 我們可以看到創建一個作業需要兩個條件,第一創建你要執行的實例,第二告訴Quartz你想要在什么時候執行。可以看到我們用到了UsingJobData的方法,這是Quartz中提供的內部方法,用於給加入到執行隊列中的作業傳遞數據用的,有6次重載,可以傳遞下面幾種類型的數據:
public JobBuilder UsingJobData(string key, string value); public JobBuilder UsingJobData(string key, int value); public JobBuilder UsingJobData(string key, long value); public JobBuilder UsingJobData(string key, float value); public JobBuilder UsingJobData(string key, double value); public JobBuilder UsingJobData(string key, bool value);
在這里我傳遞的是活動編號。
創建完MonitorJob之后還是按照上一篇文章講的方式加入到調度器:
public partial class JobManager : ServiceBase { public JobManager() { InitializeComponent(); } protected override void OnStart(string[] args) { //開啟調度器 ScheduleBase.Scheduler.Start(); //把作業,觸發器加入調度器 ScheduleBase.AddSchedule(new AutoVoidUnPaidFlashOrderService()); ScheduleBase.AddSchedule(new AutoVoidUnPaidNormalOrderService()); <span style="background-color: #ffff00;"> ScheduleBase.AddSchedule(new JobMonitorService());</span> } protected override void OnStop() { ScheduleBase.Scheduler.Shutdown(true); } }
這樣基本算是完成了,接下來就是具體的實現類了,需要注意的是我們在使用 ScheduleBase.Scheduler.ScheduleJob(job, trigger) 創建作業的時候Job名稱不能重復,所以在上面我們是根據活動Id來創建的。
接下來看實現類 ItemOnlineJob(活動上線job):
public class ItemOnlineJob : IJob { NLog.Logger log = NLog.LogManager.GetCurrentClassLogger(); public void Execute(IJobExecutionContext context) { log.Info("促銷活動上線Job開啟執行------------"); try { var sysno = context.JobDetail.JobDataMap.GetIntValue("ItemSysNo"); log.Info(string.Format("促銷活動上線Job:上線處理開始,促銷活動編號:{0}", sysno)); if (sysno > 0) { var promotion = FlashItemOfflineDBHelper.GetOfflineFlashPromotionBySysNo(sysno); //就緒的活動並且已經到達開啟時間自動開啟 if (promotion != null && promotion.Status == (int)FlashSaleStatusType.BeReady) { log.Info("促銷活動上線Job:上線處理請求開始,促銷活動編號:" + sysno); FlashItemOfflineDBHelper.UpdatePromotionStatus(sysno, (int)FlashSaleStatusType.Processing); log.Info("搶購商品到期上線Job:活動已開啟,活動編號:" + promotion.SysNo); } } } catch (Exception ex) { log.Error("促銷活動上線Job執行異常:" + ex.Message); } } }
可以看 context.JobDetail.JobDataMap 中存儲的就是我們在創建作業的時候傳的數據,在Job實時執行的時候可以取出來。
context.JobDetail.JobDataMap中提供了對應的幾個方法:
public virtual double GetDoubleValue(string key); public virtual double GetDoubleValueFromString(string key); public virtual float GetFloatValue(string key); public virtual float GetFloatValueFromString(string key); public virtual int GetIntValue(string key); public virtual int GetIntValueFromString(string key); public virtual long GetLongValue(string key); public virtual long GetLongValueFromString(string key);
ItemOfflineJob用於控制活動結束下架,實現和上線一樣。
namespace JobSchedule.JobMonitorSchedule { public class ItemOfflineJob : IJob { NLog.Logger log = NLog.LogManager.GetCurrentClassLogger(); public void Execute(IJobExecutionContext context) { log.Info("促銷活動下線Job開啟執行------------"); try { var sysno = context.JobDetail.JobDataMap.GetIntValue("ItemSysNo"); log.Info(string.Format("促銷活動下線Job:下線處理開始,促銷活動編號:{0}", sysno)); if (sysno > 0) { var promotion = FlashItemOfflineDBHelper.GetOfflineFlashPromotionBySysNo(sysno); if (promotion != null && promotion.Status == (int)FlashSaleStatusType.Processing) { log.Info("促銷活動下線Job:下線處理請求開始,促銷活動編號:" + sysno); FlashItemOfflineDBHelper.UpdatePromotionOffline(sysno, (int)FlashSaleStatusType.Finished); log.Info("搶購商品到期下線Job:活動已開啟,活動編號:" + promotion.SysNo); } } } catch (Exception ex) { log.Error("促銷活動下線Job執行異常:" + ex.Message); } } } }
代碼實現完了,我們來看看Web界面上的呈現如下:
順便再總結一下本次項目中遇到的幾個坑:
1.活動界面倒計時
最開始的時候計算倒計時的時候偷懶了,從客戶端取了時間來做倒計時,導致界面上顯示的倒計時不准確,這個只能取服務端的時間。實在是不應該犯的低級錯誤。
2.倒計時時間亂跳的問題,場景是我有兩個倒計時的活動,從活動列表頁面先后進入到詳情頁面的時候兩個計時器都在跑,導致倒計時的時間一直在閃動
最后分析原因是我的倒計時是在每一次進入到詳情頁面的時候開啟的,先后有兩個活動的時候就會觸發兩個定時器,這時界面上的顯示就是兩個倒計時同時切換,導致時間閃動
試想想如果有3個或者更多個,界面時間直接就看不清了。最后的做法是在每一次進入到詳情界面的時候把界面上所有的定時器清空,然后重新生成,這樣就解決了。
以前沒有做過移動端的開發,本次算是踩着坑過來了,也學習了不少,總結一下,繼續前行。