Stories

Detail Return Return

C#.NET NCrontab 深入解析:輕量級 Cron 表達式解析器 - Stories Detail

簡介

NCrontab.NET 平台下功能完備的 Cron 表達式解析與調度計算庫,用於處理類似 Unix Cron 的時間調度邏輯。它不依賴外部系統服務,純託管實現,是構建定時任務系統的核心組件。

解決的關鍵問題

  • Cron 表達式解析:將字符串表達式轉換為可計算的時間模型
  • 時間序列生成:計算下次執行時間或生成時間序列
  • 跨平台支持:純 .NET 實現,無操作系統依賴
  • 輕量高效:無外部依賴,內存佔用低(<100KB)

相比於自己手寫解析器或引入重量級調度框架(如 Quartz.NET),NCrontab 專注於表達式分析和下一次運行時間計算,體積輕巧、依賴少、性能高。

Cron表達式格式詳解

  1. 標準格式(5段式)
*    *    *    *    *
┬    ┬    ┬    ┬    ┬
│    │    │    │    │
│    │    │    │    └── 星期幾 (0-6, 0=週日)
│    │    │    └─────── 月份 (1-12)
│    │    └──────────── 日 (1-31)
│    └───────────────── 小時 (0-23)
└────────────────────── 分鐘 (0-59)
  1. 擴展格式(6段式,支持秒級)
*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    │
│    │    │    │    │    └── 星期幾 (0-6)
│    │    │    │    └─────── 月份 (1-12)
│    │    │    └──────────── 日 (1-31)
│    │    └───────────────── 小時 (0-23)
│    └────────────────────── 分鐘 (0-59)
└─────────────────────────── 秒 (0-59)
  1. 特殊字符説明
字符 含義 示例 説明
* 任意值 * * * * * 每分鐘執行
, 值列表 0,15,30 * * * * 每小時的0,15,30分執行
- 範圍 9-17 * * * * 9點到17點每小時執行
/ 步長 */5 * * * * 每5分鐘執行
? 不指定(僅用於日和星期) 0 0 ? * 1 每週一午夜
L 最後 (Last) 0 0 L * * 每月最後一天午夜執行
W 最近工作日(Weekday) 0 0 15W * * 每月15日最近的工作日執行
# 第N個星期X 0 0 * * 1#2 每月第二個週一執行

安裝與配置

Install-Package NCrontab
NCrontab 兼容 .NET Framework 4.6.1+、.NET Standard 2.0+,以及所有 .NET Core/.NET 5+ 版本。

只需在代碼文件頂部添加引用:

using NCrontab;

核心功能

  • Cron 表達式解析

支持標準 5 段(分、時、日、月、周)格式,以及可選的第 6 段“年”字段擴展。

  • 下次執行時間計算

    • CrontabSchedule.GetNextOccurrence(DateTime baseTime):獲取從 baseTime 開始的下一條匹配時間。
    • CrontabSchedule.GetNextOccurrences(DateTime start, DateTime end):枚舉指定時間範圍內的所有匹配時間。
  • 可配置解析選項

    • CrontabSchedule.Parse(string expression, CrontabSchedule.ParseOptions options):控制是否支持年字段或秒級字段。
    • CrontabSchedule.ParseOptions.IncludeSeconds(僅在擴展包 NCrontab.Scheduler 中支持)。
  • 線程安全

    • CrontabSchedule 實例在多線程間可安全共享,建議對同一表達式只調用一次 Parse 並緩存結果。

API 用法

方法 / 屬性 説明
CrontabSchedule.Parse(string expression) 解析 5 段標準 Cron 表達式,返回調度對象
CrontabSchedule.Parse(string expression, ParseOptions opt) 按指定選項解析 Cron 表達式
DateTime GetNextOccurrence(DateTime baseTime) 獲取從 baseTime 之後的第一條匹配時間
IEnumerable<DateTime> GetNextOccurrences(DateTime start, DateTime end) 獲取指定時間區間內的所有匹配時間
string ToString() 返回原始表達式文本
ParseOptions.IncludeSeconds true 時支持解析第 0 段(秒)字段;默認只支持分級別。

使用示例

  1. 基本示例:每小時第 15 分鐘執行
// 解析表達式 "15 * * * *":每小時的第 15 分鐘
var schedule = CrontabSchedule.Parse("15 * * * *");

// 獲取下一次執行時間(相對於當前時間)
var next = schedule.GetNextOccurrence(DateTime.Now);
Console.WriteLine($"下一次執行時間:{next}");

// 枚舉未來 24 小時內的所有執行時間
var now = DateTime.Now;
var list = schedule.GetNextOccurrences(now, now.AddHours(24));
foreach (var dt in list)
{
    Console.WriteLine(dt);
}
  1. 支持年字段:每年 1 月 1 日凌晨 0 點
// 6 段表達式:"0 0 1 1 * *"(秒 分 時 日 月 周 年)
var opts = new CrontabSchedule.ParseOptions { IncludingSeconds = false, // NCrontab 默認不支持秒
                                                // NCrontab 默認不支持年字段,需要擴展包或自定義支持
};
var yearly = CrontabSchedule.Parse("0 0 1 1 *", new CrontabSchedule.ParseOptions());

// 獲取未來 5 次執行
var occs = yearly.GetNextOccurrences(DateTime.Now, DateTime.Now.AddYears(10)).Take(5);
foreach (var dt in occs) Console.WriteLine(dt);

高級功能詳解

時區處理

// 創建帶時區的調度器
var cron = CrontabSchedule.Parse("0 12 * * *", new CrontabSchedule.ParseOptions
{
    IncludingSeconds = false // 使用5段式
});

// 轉換到特定時區
var tz = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
DateTime utcNow = DateTime.UtcNow;

// 計算東京時區的下次中午12點
DateTime next = cron.GetNextOccurrence(utcNow);
DateTime nextInTokyo = TimeZoneInfo.ConvertTimeFromUtc(next, tz);

複雜表達式解析

// 每月最後一個工作日上午10:15
var cron = CrontabSchedule.Parse("15 10 LW * *");

// 每月第三個週五下午3點
var cron = CrontabSchedule.Parse("0 15 * * 5#3");

// 工作日上午9點到下午6點,每10分鐘
var cron = CrontabSchedule.Parse("*/10 9-18 * * Mon-Fri");

構建簡單調度器

public class CronScheduler
{
    private readonly CrontabSchedule _schedule;
    private DateTime _nextRun;
    
    public CronScheduler(string cronExpression)
    {
        _schedule = CrontabSchedule.Parse(cronExpression);
        _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
    }
    
    public async Task StartAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var now = DateTime.Now;
            if (now >= _nextRun)
            {
                await ExecuteJobAsync();
                _nextRun = _schedule.GetNextOccurrence(now);
            }
            await Task.Delay(TimeSpan.FromSeconds(30), ct); // 每30秒檢查
        }
    }
    
    private Task ExecuteJobAsync() 
    {
        // 任務執行邏輯
        Console.WriteLine($"任務於 {DateTime.Now} 執行");
        return Task.CompletedTask;
    }
}

在 ASP.NET Core 中使用

// Program.cs
builder.Services.AddHostedService<CronBackgroundService>();

// 後台服務實現
public class CronBackgroundService : BackgroundService
{
    private readonly CrontabSchedule _cron;
    private DateTime _nextRun;
    
    public CronBackgroundService()
    {
        _cron = CrontabSchedule.Parse("0 */2 * * *"); // 每2小時
        _nextRun = _cron.GetNextOccurrence(DateTime.Now);
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var now = DateTime.Now;
            if (now > _nextRun)
            {
                await DoHourlyTaskAsync();
                _nextRun = _cron.GetNextOccurrence(now);
            }
            await Task.Delay(5000, stoppingToken); // 每5秒檢查
        }
    }
}

錯誤處理策略

try
{
    var schedule = CrontabSchedule.Parse(userInput);
}
catch (CrontabException ex)
{
    // 捕獲特定解析錯誤
    logger.LogError($"無效的cron表達式: {userInput}, 錯誤: {ex.Message}");
    // 提供默認表達式
    schedule = CrontabSchedule.Parse("0 0 * * *");
}

性能優化技巧

// 緩存高頻使用的調度器
private static readonly ConcurrentDictionary<string, CrontabSchedule> _scheduleCache = new();

public CrontabSchedule GetCachedSchedule(string cron)
{
    return _scheduleCache.GetOrAdd(cron, CrontabSchedule.Parse);
}

// 批量計算優化
DateTime[] GetNextOccurrencesBatch(CrontabSchedule schedule, int count)
{
    var results = new DateTime[count];
    DateTime current = DateTime.Now;
    
    for (int i = 0; i < count; i++)
    {
        current = schedule.GetNextOccurrence(current);
        results[i] = current;
    }
    
    return results;
}

結合 Quartz.NET

NCrontab 可與 Quartz.NET 集成,用於更復雜的調度:

using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;

public class MyJob : IJob
{
    public Task Execute(IJobExecutionContext context)
    {
        Console.WriteLine($"Job executed at: {DateTime.Now}");
        return Task.CompletedTask;
    }
}

class Program
{
    static async Task Main()
    {
        var factory = new StdSchedulerFactory();
        var scheduler = await factory.GetScheduler();
        await scheduler.Start();

        var job = JobBuilder.Create<MyJob>()
            .WithIdentity("myJob", "group1")
            .Build();

        var trigger = TriggerBuilder.Create()
            .WithIdentity("myTrigger", "group1")
            .WithCronSchedule("0 0 8 * * ?") // 每天 8:00
            .Build();

        await scheduler.ScheduleJob(job, trigger);
    }
}

使用 NCrontab.Scheduler

NCrontab.Scheduler 是基於 NCrontab 的輕量級調度器,支持動態添加任務:

using NCrontab.Scheduler;

class Program
{
    static void Main()
    {
        var scheduler = new Scheduler();
        scheduler.AddTask(CrontabSchedule.Parse("*/1 * * * *"), ct =>
        {
            Console.WriteLine($"Task runs every minute: {DateTime.Now:O}");
        });
        scheduler.Start();
        Console.ReadLine(); // 保持運行
    }
}

簡單定時任務示例

public class CronJob
{
    private readonly CrontabSchedule _schedule;
    private DateTime _nextRun;

    public CronJob(string cronExpression)
    {
        _schedule = CrontabSchedule.Parse(cronExpression);
        _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
    }

    public void CheckAndRun(Action action)
    {
        DateTime now = DateTime.Now;
        
        if (now >= _nextRun)
        {
            action.Invoke();
            _nextRun = _schedule.GetNextOccurrence(now);
        }
    }
}

// 使用示例:每小時執行一次
var hourlyJob = new CronJob("0 * * * *");
while (true)
{
    hourlyJob.CheckAndRun(() => {
        Console.WriteLine($"執行於: {DateTime.Now}");
    });
    Thread.Sleep(60_000); // 每分鐘檢查一次
}

封裝為可配置服務

public class CronService : BackgroundService
{
    private readonly List<CronJob> _jobs = new();
    
    public void AddJob(string cron, Action action)
    {
        _jobs.Add(new CronJob(cron, action));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            foreach (var job in _jobs)
            {
                job.CheckAndRun();
            }
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

// 註冊服務
services.AddHostedService<CronService>();

常見使用場景

適用場景

  • 後台服務定時任務

ASP.NET Core、Windows ServiceWorker Service 中,用來調度郵件發送、報表生成、緩存清理等週期性任務。

  • 動態配置調度

從數據庫或配置中心讀取 Cron 表達式,並動態生成 CrontabSchedule 實例,允許業務人員無需重啓即可調整調度策略。

  • 微服務消息投遞

結合消息隊列(RabbitMQ、Kafka)實現延遲隊列或定時重試功能。

不適用場景

  • 高精度定時(<1秒級精度)
  • 分佈式協調任務(需用分佈式調度器)
  • 動態實時調整(表達式變更需重啓)
  • 長週期任務(超過5年的調度計算)

何時選擇其他方案:

  • 需要分佈式任務調度 → Quartz.NET
  • 需要任務持久化和重試 → Hangfire
  • 需要複雜工作流管理 → Elsa Workflows

性能與注意事項

  • 性能

    • 解析開銷:Parse 方法對錶達式做詞法和語法分析,建議對同一表達式只執行一次,並緩存 CrontabSchedule 實例。
    • 計算開銷:GetNextOccurrence 算法為線性掃描,遇到複雜範圍(如“每月的最後一個工作日”)時性能略有下降,但對常見表達式足夠快速。
  • 線程安全

    • CrontabScheduleGetNext* 方法可在多線程併發調用,無需額外同步。
  • 時區問題

    • 輸入的 DateTimeNCrontab 不涉及時區轉換,所有計算均在 DateTime 自身的 Kind 上執行。
    • UTC vs Local:如果系統跨時區或夏令時環境,建議統一使用 DateTime.UtcNow 並將調度時間也轉換為 UTC
  • 表達式合法性

    • 對於不合法的表達式,Parse 會拋出 CrontabException
  • 擴展限制

    • 正式包不支持秒級(第 0 段)或年級(第 6 段)字段;社區擴展或自定義修改後可按需添加。

資源和文檔

  • NuGet 包:https://www.nuget.org/packages/NCrontab
  • GitHub 倉庫:https://github.com/atifaziz/NCrontab
  • NCrontab 表達式測試工具:https://ncrontab.swimburger.net
user avatar chen_christins Avatar zhanwang Avatar tangzhangming Avatar xiao_dingo Avatar nao_67b4a1f8dcf9e Avatar akziyuanzhan Avatar
Favorites 6 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.