採用簡易的環形延時隊列處理秒級定時任務的解決方案

来源:http://www.cnblogs.com/hohoa/archive/2017/10/29/7739271.html
-Advertisement-
Play Games

業務背景 在稍微複雜點業務系統中,不可避免會碰到做定時任務的需求,比如淘寶的交易超時自動關閉訂單、超時自動確認收貨等等。對於一些定時作業比較多的系統,通常都會搭建專門的調度平臺來管理,通過創建定時器來周期性執行任務。如剛纔所說的場景,我們可以給訂單創建一個專門的任務來處理交易狀態,每秒輪詢一次訂單表 ...


 業務背景

在稍微複雜點業務系統中,不可避免會碰到做定時任務的需求,比如淘寶的交易超時自動關閉訂單、超時自動確認收貨等等。對於一些定時作業比較多的系統,通常都會搭建專門的調度平臺來管理,通過創建定時器來周期性執行任務。如剛纔所說的場景,我們可以給訂單創建一個專門的任務來處理交易狀態,每秒輪詢一次訂單表,找出那些符合超時條件的訂單然後標記狀態。這是最簡單粗暴的做法,但明顯也很low,自己都下不去手寫這樣的代碼,所有必須要找個更好的方案。

回到真實項目中的場景,系統中某個活動上線後要給目標用戶發送簡訊通知,這些通知需要按時間點批量發送。雖然已經基於quartz.net給系統搭建了任務調度平臺,但著實不想用上述方案來實現。在網上各種搜索和思考,找到一篇文章讓我眼前一亮,稍加分析發現裡面的思路完全符合現在的場景,於是決定在自己項目中實現出來。

 

原理分析

 這種方案的核心就是構造一種數據結構,稱之為環形隊列,但實際上還是一個數組,加上對它的迴圈遍歷,達到一種環狀的假象。然後再配合定時器,就可以實現按需延時的效果。上面提到的文章中也介紹了實現思路,這裡我採用我的理解再更加詳細的解釋一下。

我們先為這個數組分配一個固定大小的空間,比如60,每個數組的元素用來存放任務的集合。然後開啟一個定時器每隔一秒來掃描這個數組,掃完一圈剛好是一分鐘。如果提前設置好任務被掃描的圈數(CycleNum)和在數組中的位置(Slot),在剛好掃到數組的Slot位置時,集合里那些CycleNum為0的任務就是達到觸發條件的任務,拉出來做業務操作然後移除掉,其他的把圈數減掉一次,然後留到下次繼續掃描,這樣就實現了延時的效果。原理如下圖所示:

可以看出中間的重點是計算出每個任務所在的位置以及需要迴圈的圈數。假設當前時間為15:20:08,當前掃描位置是2,我的任務要在15:22:35這個時刻觸發,也就是147秒後。那麼我需要迴圈的圈數就是147/60=2圈,需要被掃描的位置就是(147+2)%60=29的地方。計算好任務的坐標後塞到數組中屬於它的位置,然後靜靜等待被消費就好啦。

 

擼碼實現

光講原理不上代碼怎麼能行呢,根據上面的思路,下麵一步步在.net平臺下實現出來。

先做一些基礎封裝。

首先構造任務參數的基類,用來記錄任務的位置信息和定義業務回調方法:

    public class DelayQueueParam
    {
        internal int Slot { get; set; }

        internal int CycleNum { get; set; }

        public Action<object> Callback { get; set; }
    }

接下來是核心地方。再構造隊列的泛型類,真實類型必須派生自上面的基類,用來擴展一些業務欄位方便消費時使用。隊列的主要屬性有當前位置指針以及數組容器,主要的操作有插入、移除和消費。插入任務時需要傳入執行時間,用來計算這個任務的坐標。

    public class DelayQueue<T> where T : DelayQueueParam
    {
        private List<T>[] queue;

        private int currentIndex = 1;

        public DelayQueue(int length)
        {
            queue = new List<T>[length];
        }

        public void Insert(T item, DateTime time)
        {
            //根據消費時間計算消息應該放入的位置
            var second = (int)(time - DateTime.Now).TotalSeconds;
            item.CycleNum = second / queue.Length;
            item.Slot = (second + currentIndex) % queue.Length;
            //加入到延時隊列中
            if (queue[item.Slot] == null)
            {
                queue[item.Slot] = new List<T>();
            }
            queue[item.Slot].Add(item);
        }

        public void Remove(T item)
        {
            if (queue[item.Slot] != null)
            {
                queue[item.Slot].Remove(item);
            }
        }

        public void Read()
        {
            if (queue.Length >= currentIndex)
            {
                var list = queue[currentIndex - 1];
                if (list != null)
                {
                    List<T> target = new List<T>();
                    foreach (var item in list)
                    {
                        if (item.CycleNum == 0)
                        {
                            //在本輪命中,用單獨線程去執行業務操作
                            Task.Run(()=> { item.Callback(item); });
                            target.Add(item);
                        }
                        else
                        {
                            //等下一輪
                            item.CycleNum--;
                            System.Diagnostics.Debug.WriteLine($"@@@@@索引:{item.Slot},剩餘:{item.CycleNum}");
                        }
                    }
                    //把已過期的移除掉
                    foreach (var item in target)
                    {
                        list.Remove(item);
                    }
                }
                currentIndex++;
                //下一遍從頭開始
                if (currentIndex > queue.Length)
                {
                    currentIndex = 1;
                }
            }
        }
    }

接下來是使用方法。

創建一個管理隊列實例的靜態類,裡面封裝對隊列的操作:

    public static class NotifyPlanManager
    {
        private static DelayQueue<NotifyPlan> _queue = new DelayQueue<NotifyPlan>(60);

        public static void Insert(NotifyPlan plan, DateTime time)
        {
            _queue.Insert(plan, time);
        }

        public static void Read()
        {
            _queue.Read();
        }
    }

構建我們的實際業務參數類,派生自DelayQueueParam:

    public class NotifyPlan : DelayQueueParam
    {
        public Guid CamId { get; set; }

        public int PreviousTotal { get; set; }

        public int Amount { get; set; }
    }

生產端往隊列中插入數據:

    Action<object> callback = (result) =>
    {
        var np = result as NotifyPlan;
        //這裡做自己的業務操作
        //舉個例子:
        Debug.WriteLine($"活動ID:{np.CamId},已發送數量:{np.PreviousTotal},本次發送數量:{np.Amount}");
    };
    NotifyPlanManager.Insert(new NotifyPlan
    {
        Amount = set.MainAmount,
        CamId = camId,
        PreviousTotal = 0,
        Callback = callback
    }, smsTemplate.SendDate);

再創建一個每秒執行一次的定時器用做消費端,我這裡使用的是FluentScheduler,核心代碼:

    internal class NotifyPlanJob : IJob
    {
        /// <summary>
        /// 執行計劃
        /// </summary>
        public void Execute()
        {
            NotifyPlanManager.Read();
        }
    }

    internal class JobFactory : Registry
    {
        public JobFactory()
        {
            //每秒運行一次
            Schedule<NotifyPlanJob >().ToRunEvery(1).Seconds();
        }
    }

  JobManager.Initialize(new JobFactory());

然後開啟調試運行,打開本機的系統時間面板,對著時間看輸出結果。親測有效。

 

總結

 這種方案的好處是避免了頻繁地掃描資料庫和不必要的業務操作,另外也很方便控制時間精度。帶來的問題是如果web服務異常或重啟可能會發生任務丟失的情況,我目前的處理方法是在資料庫中標記任務狀態,服務啟動時把狀態為“排隊中”的任務重新載入到隊列中等待消費。

以上方案在單機環境測試沒問題,多節點情況下暫時沒有深究。若有設計實現上的缺陷,歡迎討論與指正,要是有更好的方案,那就當拋磚引玉,再好不過了~

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SQL語句: INSERT INTO test (cat, pid)( SELECT GROUP_CONCAT(id) cat, pid FROM manage_store_cat GROUP BY pid); 所有操作截圖以及運行結果: 下圖是最終的想要的結果圖 ...
  • 在我們生產環境中,熟悉伺服器配置是必不可少的,以下是本人整理的一些常用的伺服器配置查看命令: ################### cpu性能查看 ############################################################1、查看物理cpu個數:cat ...
  • 誤刪資料庫時,可以利用insert插入刪除的數據,但是有時表可能有自增欄位如id。這是插入數據如果包含自增欄位就會出現錯誤,提示"IDENTITY_INSERT設置為OFF,插入失敗"。 所以我們將其設置為on即可,sql語句:set IDENTITY_INSERT 表名 on。完美地解決了問題,當 ...
  • 1、把Oracle壓縮文件解壓出來後打開目錄,雙擊“setup.exe”開始安裝 2、彈出“Oracle Universal Installer”視窗 3、出現電子郵件提示,按照預設下一步操作 4、第二步直接按照系統預設即可,點擊‘下一步’ 5、預設安裝桌面類,點擊‘下一步 6、輸入Oracle a ...
  • Asp.net中Request.Url的各個屬性對應的意義介紹 本文轉載自 http://www.jb51.net/article/30254.htm Asp.net中Request.Url的各個屬性對應的意義介紹 本文轉載自 http://www.jb51.net/article/30254.ht ...
  • 什麼是單點登錄? 我想肯定有一部分人“望文生義”的認為單點登錄就是一個用戶只能在一處登錄,其實這是錯誤的理解(我記得我第一次也是這麼理解的)。 單點登錄指的是多個子系統只需要登錄一個,其他系統不需要登錄了(一個瀏覽器內)。一個子系統退出,其他子系統也全部是退出狀態。 如果你還是不明白,我們舉個實際的 ...
  • 用於進行遷移的 Entity Framework Core NuGet 包 註意:必須通過編輯 .csproj 文件來安裝此包;不能使用 install-package 命令或程式包管理器 GUI。 你可以編輯.csproj通過右鍵單擊中的項目名稱的文件解決方案資源管理器並選擇編輯 <ItemGro ...
  • 一、摘要 一說到ADO.NET大家可能立刻想到的就是增、刪、改、查(CRUD)操作,然後再接就想到項目中的SQLHelper。沒錯本課分享課阿笨給大家帶來的是來源於github上開源的DAO資料庫訪問組件DBHelpers。如果您對本次分享《.NET輕量級DBHelpers數據訪問組件》課程感興趣的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...