如何實現定時推送?

dotNet源計劃發表於2021-04-16

一、概要

在工作當中遇到了一個需要定時向客戶端推送新聞、文章等內容。這個時候在網上搜了很久沒有找到合適的解決方案,其實能解決這個問題的方案有很多比如說用到一些大廠貢獻的xxMQ中介軟體之類的,確實能解決問題。但是目前專案比較小根本用不上這麼重的框架,在偶然的看到了一位大佬寫的文章提供了一個非常不錯的思路本篇文章也是受到他的啟發實現了之後這裡分享給大家。這個大佬的是58的沈劍文章名稱是“1分鐘實現延遲訊息功能”。

關注本公眾號回覆“定時推送”即可獲得原始碼地址

原文地址:

二、詳細內容

詳細內容大概分為4個部分,1.應用場景 2.遇到問題 3.設計 4.實現 5.執行效果

1.應用場景

需要定時推送資料,且輕量化的實現。

2.遇到問題

  • 如果啟動一個定時器去定時輪詢
  • (1)輪詢效率比較低
  • (2)每次掃庫,已經被執行過記錄,仍然會被掃描(只是不會出現在結果集中),會做重複工作
  • (3)時效性不夠好,如果每小時輪詢一次,最差的情況下會有時間誤差
  • 如何利用“延時訊息”,對於每個任務只觸發一次,保證效率的同時保證實時性,是今天要討論的問題。

3.設計

高效延時訊息,包含兩個重要的資料結構:

(1)環形佇列,例如可以建立一個包含3600個slot的環形佇列(本質是個陣列)

(2)任務集合,環上每一個slot是一個Set

同時,啟動一個timer,這個timer每隔1s,在上述環形佇列中移動一格,有一個Current Index指標來標識正在檢測的slot。

Task結構中有兩個很重要的屬性:

(1)Cycle-Num:當Current Index第幾圈掃描到這個Slot時,執行任務

(2)Task-Function:需要執行的任務指標

假設當前Current Index指向第一格,當有延時訊息到達之後,例如希望3610秒之後,觸發一個延時訊息任務,只需:

(1)計算這個Task應該放在哪一個slot,現在指向1,3610秒之後,應該是第11格,所以這個Task應該放在第11個slot的Set中

(2)計算這個Task的Cycle-Num,由於環形佇列是3600格(每秒移動一格,正好1小時),這個任務是3610秒後執行,所以應該繞3610/3600=1圈之後再執行,於是Cycle-Num=1

Current Index不停的移動,每秒移動到一個新slot,這個slot中對應的Set,每個Task看Cycle-Num是不是0:

(1)如果不是0,說明還需要多移動幾圈,將Cycle-Num減1

(2)如果是0,說明馬上要執行這個Task了,取出Task-Funciton執行(可以用單獨的執行緒來執行Task),並把這個Task從Set中刪除

使用了“延時訊息”方案之後,“訂單48小時後關閉評價”的需求,只需將在訂單關閉時,觸發一個48小時之後的延時訊息即可:

(1)無需再輪詢全部訂單,效率高

(2)一個訂單,任務只執行一次

(3)時效性好,精確到秒(控制timer移動頻率可以控制精度)

4.實現

首先寫一個方案要理清楚自己的專案結構,我做了如下分層。

 

如何實現定時推送?

 

Interfaces , 這層裡主要約束延遲訊息佇列的佇列和訊息任務行。

public interface IRingQueue<T>
{
    /// <summary>
    /// Add tasks [add tasks will automatically generate: task Id, task slot location, number of task cycles]
    /// </summary>
    /// <param name="delayTime">The specified task is executed after N seconds.</param>
    /// <param name="action">Definitions of callback</param>
    void Add(long delayTime,Action<T> action);

    /// <summary>
    /// Add tasks [add tasks will automatically generate: task Id, task slot location, number of task cycles]
    /// </summary>
    /// <param name="delayTime">The specified task is executed after N seconds.</param>
    /// <param name="action">Definitions of callback.</param>
    /// <param name="data">Parameters used in the callback function.</param>
    void Add(long delayTime, Action<T> action, T data);

    /// <summary>
    /// Add tasks [add tasks will automatically generate: task Id, task slot location, number of task cycles]
    /// </summary>
    /// <param name="delayTime"></param>
    /// <param name="action">Definitions of callback</param>
    /// <param name="data">Parameters used in the callback function.</param>
    /// <param name="id">Task ID, used when deleting tasks.</param>
    void Add(long delayTime, Action<T> action, T data, long id);

    /// <summary>
    /// Remove tasks [need to know: where the task is, which specific task].
    /// </summary>
    /// <param name="index">Task slot location</param>
    /// <param name="id">Task ID, used when deleting tasks.</param>
    void Remove(long id);

    /// <summary>
    /// Launch queue.
    /// </summary>
    void Start();
}

 public interface ITask
 {
 }

Achieves,這層裡實現之前定義的介面,這裡寫成抽象類是為了後面方便擴充套件。

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DelayMessageApp.Interfaces;

namespace DelayMessageApp.Achieves.Base
{
public abstract class BaseQueue<T> : IRingQueue<T>
{
    private long _pointer = 0L;
    private ConcurrentBag<BaseTask<T>>[] _arraySlot;
    private int ArrayMax;

    /// <summary>
    /// Ring queue.
    /// </summary>
    public ConcurrentBag<BaseTask<T>>[] ArraySlot
    {
        get { return _arraySlot ?? (_arraySlot = new ConcurrentBag<BaseTask<T>>[ArrayMax]); }
    }
    
    public BaseQueue(int arrayMax)
    {
        if (arrayMax < 60 && arrayMax % 60 == 0)
            throw new Exception("Ring queue length cannot be less than 60 and is a multiple of 60 .");

        ArrayMax = arrayMax;
    }

    public void Add(long delayTime, Action<T> action)
    {
        Add(delayTime, action, default(T));
    }

    public void Add(long delayTime,Action<T> action,T data)
    {
        Add(delayTime, action, data,0);
    }

    public void Add(long delayTime, Action<T> action, T data,long id)
    {
        NextSlot(delayTime, out long cycle, out long pointer);
        ArraySlot[pointer] =  ArraySlot[pointer] ?? (ArraySlot[pointer] = new ConcurrentBag<BaseTask<T>>());
        var baseTask = new BaseTask<T>(cycle, action, data,id);
        ArraySlot[pointer].Add(baseTask);
    }

    /// <summary>
    /// Remove tasks based on ID.
    /// </summary>
    /// <param name="id"></param>
    public void Remove(long id)
    {
        try
        {
            Parallel.ForEach(ArraySlot, (ConcurrentBag<BaseTask<T>> collection, ParallelLoopState state) =>
            {
                var resulTask = collection.FirstOrDefault(p => p.Id == id);
                if (resulTask != null)
                {
                    collection.TryTake(out resulTask);
                    state.Break();
                }
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
    
    public void Start()
    {
        while (true)
        {
            RightMovePointer();
            Thread.Sleep(1000);
            Console.WriteLine(DateTime.Now.ToString());
        }
    }

    /// <summary>
    /// Calculate the information of the next slot.
    /// </summary>
    /// <param name="delayTime">Delayed execution time.</param>
    /// <param name="cycle">Number of turns.</param>
    /// <param name="index">Task location.</param>
    private void NextSlot(long delayTime, out long cycle,out long index)
    {
        try
        {
            var circle = delayTime / ArrayMax;
            var second = delayTime % ArrayMax;
            var current_pointer = GetPointer();
            var queue_index = 0L;

            if (delayTime - ArrayMax > ArrayMax)
            {
                circle = 1;
            }
            else if (second > ArrayMax)
            {
                circle += 1;
            }

            if (delayTime - circle * ArrayMax < ArrayMax)
            {
                second = delayTime - circle * ArrayMax;
            }

            if (current_pointer + delayTime >= ArrayMax)
            {
                cycle = (int)((current_pointer + delayTime) / ArrayMax);
                if (current_pointer + second - ArrayMax < 0)
                {
                    queue_index = current_pointer + second;
                }
                else if (current_pointer + second - ArrayMax > 0)
                {
                    queue_index = current_pointer + second - ArrayMax;
                }
            }
            else
            {
                cycle = 0;
                queue_index = current_pointer + second;
            }
            index = queue_index;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

    /// <summary>
    /// Get the current location of the pointer.
    /// </summary>
    /// <returns></returns>
    private long GetPointer()
    {
        return Interlocked.Read(ref _pointer);
    }

    /// <summary>
    /// Reset pointer position.
    /// </summary>
    private void ReSetPointer()
    {
        Interlocked.Exchange(ref _pointer, 0);
    }

    /// <summary>
    /// Pointer moves clockwise.
    /// </summary>
    private void RightMovePointer()
    {
        try
        {
            if (GetPointer() >= ArrayMax - 1)
            {
                ReSetPointer();
            }
            else
            {
                Interlocked.Increment(ref _pointer);
            }

            var pointer = GetPointer();
            var taskCollection = ArraySlot[pointer];
            if (taskCollection == null || taskCollection.Count == 0) return;

            Parallel.ForEach(taskCollection, (BaseTask<T> task) =>
            {
                if (task.Cycle > 0)
                {
                    task.SubCycleNumber();
                }

                if (task.Cycle <= 0)
                {
                    taskCollection.TryTake(out task);
                    task.TaskAction(task.Data);
                }
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }
}
}


using System;
using System.Threading;
using DelayMessageApp.Interfaces;

namespace DelayMessageApp.Achieves.Base
{
public class BaseTask<T> : ITask
{
    private long _cycle;
    private long _id;
    private T _data;

    public Action<T> TaskAction { get; set; }

    public long Cycle
    {
        get { return Interlocked.Read(ref _cycle); }
        set { Interlocked.Exchange(ref _cycle, value); }
    }

    public long Id
    {
        get { return _id; }
        set { _id = value; }
    }

    public T Data
    {
        get { return _data; }
        set { _data = value; }
    }

    public BaseTask(long cycle, Action<T> action, T data,long id)
    {
        Cycle = cycle;
        TaskAction = action;
        Data = data;
        Id = id;
    }

    public BaseTask(long cycle, Action<T> action,T data)
    {
        Cycle = cycle;
        TaskAction = action;
        Data = data;
    }

    public BaseTask(long cycle, Action<T> action)
    {
        Cycle = cycle;
        TaskAction = action;
    }
    
    public void SubCycleNumber()
    {
        Interlocked.Decrement(ref _cycle);
    }
}
}

Logic,這層主要實現呼叫邏輯,呼叫者最終只需要關心把任務放進佇列並指定什麼時候執行就行了,根本不需要關心其它的任何資訊。

public static void Start()
{
    //1.Initialize queues of different granularity.
    IRingQueue<NewsModel> minuteRingQueue = new MinuteQueue<NewsModel>();

    //2.Open thread.
    var lstTasks = new List<Task>
    {
        Task.Factory.StartNew(minuteRingQueue.Start)
    };

    //3.Add tasks performed in different periods.
    minuteRingQueue.Add(5, new Action<NewsModel>((NewsModel newsObj) =>
    {
        Console.WriteLine(newsObj.News);
    }), new NewsModel() { News = "Trump's visit to China!" });

    minuteRingQueue.Add(10, new Action<NewsModel>((NewsModel newsObj) =>
    {
        Console.WriteLine(newsObj.News);
    }), new NewsModel() { News = "Putin Pu's visit to China!" });

    minuteRingQueue.Add(60, new Action<NewsModel>((NewsModel newsObj) =>
    {
        Console.WriteLine(newsObj.News);
    }), new NewsModel() { News = "Eisenhower's visit to China!" });

    minuteRingQueue.Add(120, new Action<NewsModel>((NewsModel newsObj) =>
    {
        Console.WriteLine(newsObj.News);
    }), new NewsModel() { News = "Xi Jinping's visit to the US!" });

    //3.Waiting for all tasks to complete is usually not completed. Because there is an infinite loop.
    //F5 Run the program and see the effect.
    Task.WaitAll(lstTasks.ToArray());
    Console.Read();
}

Models,這層就是用來在延遲任務中帶入的資料模型類而已了。自己用的時候換成任意自定義型別都可以。

5.執行效果

如何實現定時推送?

相關文章