上次咱們實現了一個簡單的基於 Timer 的定時任務,詳細信息能夠看這篇文章。html
可是使用過程當中慢慢發現這種方式可能並不太合適,有些任務可能只但願在某個時間段內執行,只使用 timer 就顯得不是那麼靈活了,但願能夠像 quartz 那樣指定一個 cron 表達式來指定任務的執行時間。git
cron 常見於Unix和類Unix的操做系統之中,用於設置週期性被執行的指令。該命令從標準輸入設備讀取指令,並將其存放於「crontab」文件中,以供以後讀取和執行。該詞來源於希臘語 chronos(χρόνος),原意是時間。github
一般,
crontab
儲存的指令被守護進程激活,crond
經常在後臺運行,每一分鐘檢查是否有預約的做業須要執行。這類做業通常稱爲cron jobs。redis
cron 能夠比較準確的描述週期性執行任務的執行時間,標準的 cron 表達式是五位:express
30 4 * * ?
五個位置上的值分別對應 分鐘/小時/日期/月份/周(day of week)併發
如今有一些擴展,有6位的,也有7位的,6位的表達式第一個對應的是秒,7個的第一個對應是秒,最後一個對應的是年份框架
0 0 12 * * ?
天天中午12點
0 15 10 ? * *
天天 10:15
0 15 10 * * ?
天天 10:15
30 15 10 * * ? *
天天 10:15:30
0 15 10 * * ? 2005
2005年天天 10:15async
詳細信息能夠參考:http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.htmlide
CRON 解析庫 使用的是 https://github.com/HangfireIO/Cronos
,支持五位/六位,暫不支持年份的解析(7位)網站
基於 BackgroundService
的 CRON 定時服務,實現以下:
public abstract class CronScheduleServiceBase : BackgroundService { /// <summary> /// job cron trigger expression /// refer to: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html /// </summary> public abstract string CronExpression { get; } protected abstract bool ConcurrentAllowed { get; } protected readonly ILogger Logger; private readonly string JobClientsCache = "JobClientsHash"; protected CronScheduleServiceBase(ILogger logger) { Logger = logger; } protected abstract Task ProcessAsync(CancellationToken cancellationToken); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { { var next = CronHelper.GetNextOccurrence(CronExpression); while (!stoppingToken.IsCancellationRequested && next.HasValue) { var now = DateTimeOffset.UtcNow; if (now >= next) { if (ConcurrentAllowed) { _ = ProcessAsync(stoppingToken); next = CronHelper.GetNextOccurrence(CronExpression); if (next.HasValue) { Logger.LogInformation("Next at {next}", next); } } else { var machineName = RedisManager.HashClient.GetOrSet(JobClientsCache, GetType().FullName, () => Environment.MachineName); // try get job master if (machineName == Environment.MachineName) // IsMaster { using (var locker = RedisManager.GetRedLockClient($"{GetType().FullName}_cronService")) { // redis 互斥鎖 if (await locker.TryLockAsync()) { // 執行 job await ProcessAsync(stoppingToken); next = CronHelper.GetNextOccurrence(CronExpression); if (next.HasValue) { Logger.LogInformation("Next at {next}", next); await Task.Delay(next.Value - DateTimeOffset.UtcNow, stoppingToken); } } else { Logger.LogInformation($"failed to acquire lock"); } } } } } else { // needed for graceful shutdown for some reason. // 1000ms so it doesn't affect calculating the next // cron occurence (lowest possible: every second) await Task.Delay(1000, stoppingToken); } } } } public override Task StopAsync(CancellationToken cancellationToken) { RedisManager.HashClient.Remove(JobClientsCache, GetType().FullName); // unregister from jobClients return base.StopAsync(cancellationToken); } }
由於網站部署在多臺機器上,因此爲了防止併發執行,使用 redis 作了一些事情,Job執行的時候嘗試獲取 redis 中 job 對應的 master 的 hostname,沒有的話就設置爲當前機器的 hostname,在 job 中止的時候也就是應用中止的時候,刪除 redis 中當前 job 對應的 master,job執行的時候判斷是不是 master 節點,是 master 才執行job,不是 master 則不執行。完整實現代碼:https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Services/CronScheduleServiceBase.cs#L11
定時 Job 示例:
public class RemoveOverdueReservationService : CronScheduleServiceBase { private readonly IServiceProvider _serviceProvider; private readonly IConfiguration _configuration; public RemoveOverdueReservationService(ILogger<RemoveOverdueReservationService> logger, IServiceProvider serviceProvider, IConfiguration configuration) : base(logger) { _serviceProvider = serviceProvider; _configuration = configuration; } public override string CronExpression => _configuration.GetAppSetting("RemoveOverdueReservationCron") ?? "0 0 18 * * ?"; protected override bool ConcurrentAllowed => false; protected override async Task ProcessAsync(CancellationToken cancellationToken) { using (var scope = _serviceProvider.CreateScope()) { var reservationRepo = scope.ServiceProvider.GetRequiredService<IEFRepository<ReservationDbContext, Reservation>>(); await reservationRepo.DeleteAsync(reservation => reservation.ReservationStatus == 0 && (reservation.ReservationForDate < DateTime.Today.AddDays(-3))); } } }
使用 redis 這種方式來決定 master 並非特別可靠,正常結束的沒有什麼問題,最好仍是用比較成熟的服務註冊發現框架比較好