# 任务调度 (11_Quartz)

最后更新: 2024-09-20


# 📚 概述

DarkM 任务调度模块集成了 Quartz.NET 任务调度框架,提供了统一的定时任务管理功能,支持 Cron 表达式、任务持久化、任务日志等功能。

源代码位置: DarkM/src/Framework/Quartz


# 🏗️ 模块架构

# 完整目录结构

Quartz/
├── Quartz.Abstractions/
│   ├── IQuartzServer.cs                   # Quartz 服务器接口
│   ├── ITask.cs                           # 任务接口
│   ├── TaskAbstract.cs                    # 任务抽象基类
│   ├── ITaskExecutionContext.cs           # 任务执行上下文接口
│   ├── ITaskLogger.cs                     # 任务日志记录器接口
│   ├── QuartzTaskDescriptor.cs            # Quartz 任务描述符
│   ├── QuartzConfig.cs                    # Quartz 配置
│   ├── QuartzProvider.cs                  # Quartz 提供器枚举
│   └── QuartzModuleDescriptor.cs          # Quartz 模块描述符
│
└── Quartz.Core/
    ├── QuartzServer.cs                    # Quartz 服务器实现
    ├── JobFactory.cs                      # 任务工厂
    ├── TaskLogger.cs                      # 任务日志记录器
    ├── QuartzModuleCollection.cs          # Quartz 模块集合
    ├── QuartzAppShutdownHandler.cs        # Quartz 应用关闭处理器
    └── ServiceCollectionExtensions.cs     # DI 扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 💡 配置说明

# appsettings.json 配置

{
  "Quartz": {
    // 提供器:0=RAMJobStore, 1=ADO.NET
    "Provider": 1,
    // 数据库连接字符串(ADO.NET 模式)
    "ConnectionString": "",
    // 表前缀
    "TablePrefix": "QRTZ_",
    // 实例名称
    "InstanceName": "DarkM_Quartz",
    // 是否开启 Web 控制台
    "Dashboard": true,
    // 控制台端口
    "DashboardPort": 5000
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 配置项详解

配置项 类型 默认值 说明
Provider QuartzProvider 1 任务存储提供器
ConnectionString string - 数据库连接字符串
TablePrefix string QRTZ_ Quartz 表前缀
InstanceName string DarkM_Quartz Quartz 实例名称
Dashboard bool true 是否启用 Web 控制台
DashboardPort int 5000 Web 控制台端口

# 🔧 核心接口

# 1. IQuartzServer(Quartz 服务器)

文件位置: Quartz.Abstractions/IQuartzServer.cs

public interface IQuartzServer
{
    /// <summary>
    /// 启动服务器
    /// </summary>
    Task Start();
    
    /// <summary>
    /// 停止服务器
    /// </summary>
    Task Stop();
    
    /// <summary>
    /// 暂停所有任务
    /// </summary>
    Task PauseAll();
    
    /// <summary>
    /// 恢复所有任务
    /// </summary>
    Task ResumeAll();
    
    /// <summary>
    /// 获取所有任务
    /// </summary>
    Task<IList<QuartzTaskDescriptor>> GetAllTasks();
    
    /// <summary>
    /// 添加任务
    /// </summary>
    Task AddJob(QuartzTaskDescriptor task);
    
    /// <summary>
    /// 删除任务
    /// </summary>
    Task DeleteJob(string jobName, string jobGroup);
    
    /// <summary>
    /// 暂停任务
    /// </summary>
    Task PauseJob(string jobName, string jobGroup);
    
    /// <summary>
    /// 恢复任务
    /// </summary>
    Task ResumeJob(string jobName, string jobGroup);
    
    /// <summary>
    /// 立即执行任务
    /// </summary>
    Task TriggerJob(string jobName, string jobGroup);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 2. ITask(任务接口)

文件位置: Quartz.Abstractions/ITask.cs

public interface ITask
{
    /// <summary>
    /// 任务名称
    /// </summary>
    string Name { get; }
    
    /// <summary>
    /// 任务分组
    /// </summary>
    string Group { get; }
    
    /// <summary>
    /// Cron 表达式
    /// </summary>
    string Cron { get; }
    
    /// <summary>
    /// 任务描述
    /// </summary>
    string Description { get; }
    
    /// <summary>
    /// 是否启用
    /// </summary>
    bool Enabled { get; }
    
    /// <summary>
    /// 执行任务
    /// </summary>
    Task Execute(ITaskExecutionContext context);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 3. TaskAbstract(任务抽象基类)

文件位置: Quartz.Abstractions/TaskAbstract.cs

public abstract class TaskAbstract : ITask
{
    /// <summary>
    /// 任务名称
    /// </summary>
    public abstract string Name { get; }
    
    /// <summary>
    /// 任务分组
    /// </summary>
    public abstract string Group { get; }
    
    /// <summary>
    /// Cron 表达式
    /// </summary>
    public abstract string Cron { get; }
    
    /// <summary>
    /// 任务描述
    /// </summary>
    public virtual string Description => "";
    
    /// <summary>
    /// 是否启用
    /// </summary>
    public virtual bool Enabled => true;
    
    /// <summary>
    /// 执行任务
    /// </summary>
    public abstract Task Execute(ITaskExecutionContext context);
    
    /// <summary>
    /// 任务开始前调用
    /// </summary>
    protected virtual Task OnExecuting() => Task.CompletedTask;
    
    /// <summary>
    /// 任务执行完成后调用
    /// </summary>
    protected virtual Task OnExecuted() => Task.CompletedTask;
    
    /// <summary>
    /// 任务执行失败时调用
    /// </summary>
    protected virtual Task OnError(Exception ex) => Task.CompletedTask;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 4. ITaskExecutionContext(任务执行上下文)

文件位置: Quartz.Abstractions/ITaskExecutionContext.cs

public interface ITaskExecutionContext
{
    /// <summary>
    /// 任务名称
    /// </summary>
    string JobName { get; }
    
    /// <summary>
    /// 任务分组
    /// </summary>
    string JobGroup { get; }
    
    /// <summary>
    /// 触发器名称
    /// </summary>
    string TriggerName { get; }
    
    /// <summary>
    /// 触发器分组
    /// </summary>
    string TriggerGroup { get; }
    
    /// <summary>
    /// 任务数据
    /// </summary>
    JobDataMap JobDataMap { get; }
    
    /// <summary>
    /// 获取日志记录器
    /// </summary>
    ITaskLogger Logger { get; }
    
    /// <summary>
    /// 取消令牌
    /// </summary>
    CancellationToken CancellationToken { get; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 5. ITaskLogger(任务日志记录器)

文件位置: Quartz.Abstractions/ITaskLogger.cs

public interface ITaskLogger
{
    /// <summary>
    /// 记录调试日志
    /// </summary>
    void Debug(string message);
    
    /// <summary>
    /// 记录信息日志
    /// </summary>
    void Info(string message);
    
    /// <summary>
    /// 记录警告日志
    /// </summary>
    void Warn(string message);
    
    /// <summary>
    /// 记录错误日志
    /// </summary>
    void Error(string message, Exception ex = null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 💡 使用示例

# 1. 简单任务

文件位置: Module.Quartz/Jobs/HelloTask.cs

using DarkM.Lib.Quartz.Abstractions;

namespace DarkM.Module.Quartz.Jobs
{
    /// <summary>
    /// Hello World 示例任务
    /// </summary>
    public class HelloTask : TaskAbstract
    {
        public override string Name => "HelloTask";
        
        public override string Group => "Demo";
        
        public override string Cron => "0/10 * * * * ?"; // 每 10 秒执行一次
        
        public override string Description => "Hello World 示例任务";
        
        public override async Task Execute(ITaskExecutionContext context)
        {
            context.Logger.Info("Hello World!");
            
            await Task.Delay(1000);
            
            context.Logger.Info("任务执行完成");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 2. 带参数的任务

using DarkM.Lib.Quartz.Abstractions;

namespace DarkM.Module.Quartz.Jobs
{
    /// <summary>
    /// 数据同步任务
    /// </summary>
    public class DataSyncTask : TaskAbstract
    {
        private readonly IDataService _dataService;
        
        public DataSyncTask(IDataService dataService)
        {
            _dataService = dataService;
        }
        
        public override string Name => "DataSyncTask";
        
        public override string Group => "System";
        
        public override string Cron => "0 0 2 * * ?"; // 每天凌晨 2 点执行
        
        public override string Description => "数据同步任务";
        
        protected override async Task OnExecuting()
        {
            Logger.Info("开始数据同步...");
        }
        
        public override async Task Execute(ITaskExecutionContext context)
        {
            try
            {
                // 从 JobDataMap 获取参数
                var sourceDb = context.JobDataMap.GetString("SourceDb");
                var targetDb = context.JobDataMap.GetString("TargetDb");
                
                Logger.Info($"从 {sourceDb} 同步到 {targetDb}");
                
                await _dataService.SyncAsync(sourceDb, targetDb);
                
                Logger.Info("数据同步完成");
            }
            catch (Exception ex)
            {
                Logger.Error("数据同步失败", ex);
                throw;
            }
        }
        
        protected override async Task OnError(Exception ex)
        {
            Logger.Error("任务执行失败", ex);
            // 发送告警通知
            await SendAlertAsync(ex);
        }
        
        private Task SendAlertAsync(Exception ex)
        {
            // 发送告警逻辑
            return Task.CompletedTask;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# 3. 数据库清理任务

using DarkM.Lib.Quartz.Abstractions;
using DarkM.Lib.Data.Abstractions;

namespace DarkM.Module.Quartz.Jobs
{
    /// <summary>
    /// 清理日志任务
    /// </summary>
    public class CleanLogTask : TaskAbstract
    {
        private readonly IDbSet<LogEntity> _logDbSet;
        
        public CleanLogTask(IDbSet<LogEntity> logDbSet)
        {
            _logDbSet = logDbSet;
        }
        
        public override string Name => "CleanLogTask";
        
        public override string Group => "System";
        
        public override string Cron => "0 0 3 * * ?"; // 每天凌晨 3 点执行
        
        public override string Description => "清理日志数据(保留 30 天)";
        
        public override async Task Execute(ITaskExecutionContext context)
        {
            var keepDays = 30;
            var cutoffDate = DateTime.Now.AddDays(-keepDays);
            
            Logger.Info($"开始清理{keepDays}天前的日志数据...");
            
            var count = await _logDbSet.Find(e => e.CreateTime < cutoffDate)
                .ExecuteDeleteAsync();
            
            Logger.Info($"清理完成,共删除 {count} 条记录");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 🔧 任务配置

# 方式 1:代码配置

在模块的 ModuleDescriptor 中注册任务:

using DarkM.Lib.Quartz.Abstractions;

public class QuartzModuleDescriptor : QuartzModuleDescriptorAbstract
{
    public override void ConfigureServices(IServiceCollection services, IModuleCollection modules)
    {
        // 注册任务
        services.AddSingleton<ITask, HelloTask>();
        services.AddSingleton<ITask, DataSyncTask>();
        services.AddSingleton<ITask, CleanLogTask>();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 方式 2:数据库配置

任务信息保存在数据库的 Quartz_Job 表中:

INSERT INTO Quartz_Job (Name, [Group], Cron, Description, Enabled, Assembly, Class)
VALUES 
('HelloTask', 'Demo', '0/10 * * * * ?', 'Hello World 示例任务', 1, 'DarkM.Module.Quartz', 'DarkM.Module.Quartz.Jobs.HelloTask'),
('DataSyncTask', 'System', '0 0 2 * * ?', '数据同步任务', 1, 'DarkM.Module.Quartz', 'DarkM.Module.Quartz.Jobs.DataSyncTask');
1
2
3
4

# 🔧 Cron 表达式

# 格式

秒 分 时 日 月 周 年(可选)
1

# 特殊字符

字符 说明 示例
* 任意值 * * * * * ? 每秒执行
? 不指定值 0 0 12 ? * MON 每周一中午 12 点
- 范围 0 0 12-18 * * ? 每天 12-18 点每小时执行
, 枚举值 0 0 12,18 * * ? 每天 12 点和 18 点执行
/ 增量 0 0/10 * * * ? 每 10 分钟执行
L 最后 0 0 12 L * ? 每月最后一天中午 12 点
W 最近工作日 0 0 12 15W * ? 每月 15 日最近的工作日
# 第几个周 X 0 0 12 ? * 6#3 每月第 3 个周五

# 常用示例

表达式 说明
0/10 * * * * ? 每 10 秒执行
0 0/5 * * * ? 每 5 分钟执行
0 0 * * * ? 每小时执行
0 0 8-18 * * ? 每天 8-18 点每小时执行
0 0 2 * * ? 每天凌晨 2 点执行
0 0 2 ? * MON 每周一凌晨 2 点执行
0 0 2 1 * ? 每月 1 日凌晨 2 点执行
0 0 2 * * MON-FRI 每周一至周五凌晨 2 点执行
0 0 12 L * ? 每月最后一天中午 12 点执行
0 0 0 1 1 ? 每年 1 月 1 日零点执行

# 🔧 任务管理

# 1. 启动/停止任务

// 注入 IQuartzServer
private readonly IQuartzServer _quartzServer;

// 启动服务器
await _quartzServer.Start();

// 停止服务器
await _quartzServer.Stop();
1
2
3
4
5
6
7
8

# 2. 添加任务

await _quartzServer.AddJob(new QuartzTaskDescriptor
{
    Name = "HelloTask",
    Group = "Demo",
    Cron = "0/10 * * * * ?",
    Description = "Hello World 示例任务",
    Enabled = true,
    Assembly = "DarkM.Module.Quartz",
    Class = "DarkM.Module.Quartz.Jobs.HelloTask"
});
1
2
3
4
5
6
7
8
9
10

# 3. 删除任务

await _quartzServer.DeleteJob("HelloTask", "Demo");
1

# 4. 暂停/恢复任务

// 暂停任务
await _quartzServer.PauseJob("HelloTask", "Demo");

// 恢复任务
await _quartzServer.ResumeJob("HelloTask", "Demo");
1
2
3
4
5

# 5. 立即执行任务

await _quartzServer.TriggerJob("HelloTask", "Demo");
1

# 6. 获取所有任务

var tasks = await _quartzServer.GetAllTasks();

foreach (var task in tasks)
{
    Console.WriteLine($"{task.Name} ({task.Group}): {task.Cron}");
}
1
2
3
4
5
6

# 🖥️ Web 控制台

Quartz 提供了 Web 控制台,可以查看和管理任务。

# 启用控制台

appsettings.json:

{
  "Quartz": {
    "Dashboard": true,
    "DashboardPort": 5000
  }
}
1
2
3
4
5
6

# 访问控制台

启动后访问:http://localhost:5000

# 控制台功能

  • ✅ 查看所有任务列表
  • ✅ 查看任务执行历史
  • ✅ 手动触发任务
  • ✅ 暂停/恢复任务
  • ✅ 添加/删除任务
  • ✅ 查看任务日志

# 📚 最佳实践

# 1. 使用依赖注入

任务类通过构造函数注入所需服务:

public class DataSyncTask : TaskAbstract
{
    private readonly IDataService _dataService;
    private readonly ILogger _logger;
    
    public DataSyncTask(IDataService dataService, ILogger logger)
    {
        _dataService = dataService;
        _logger = logger;
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 2. 处理异常

Execute 方法中捕获并记录异常:

public override async Task Execute(ITaskExecutionContext context)
{
    try
    {
        // 任务逻辑
        await DoWorkAsync();
    }
    catch (Exception ex)
    {
        context.Logger.Error("任务执行失败", ex);
        throw; // 重新抛出,让 Quartz 记录失败
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3. 使用生命周期方法

利用 OnExecuting/OnExecuted/OnError

protected override async Task OnExecuting()
{
    Logger.Info("任务开始执行");
}

protected override async Task OnExecuted()
{
    Logger.Info("任务执行完成");
}

protected override async Task OnError(Exception ex)
{
    Logger.Error("任务执行失败", ex);
    await SendAlertAsync(ex);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4. 避免长时间运行的任务

如果任务执行时间较长,考虑:

  • 使用异步方法
  • 设置合理的超时时间
  • 考虑拆分为多个小任务

# 5. 任务幂等性

确保任务可以重复执行:

public override async Task Execute(ITaskExecutionContext context)
{
    // 检查是否已经在执行
    if (await IsRunningAsync())
    {
        Logger.Warn("任务已在执行,跳过");
        return;
    }
    
    try
    {
        await DoWorkAsync();
    }
    finally
    {
        // 清理状态
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 🔍 常见问题

# Q1: 任务不执行?

检查清单:

  1. ✅ Cron 表达式是否正确
  2. ✅ 任务是否启用(Enabled = true)
  3. ✅ Quartz 服务器是否启动
  4. ✅ 任务类是否正确注册

解决方案:

// 检查任务状态
var tasks = await _quartzServer.GetAllTasks();
var task = tasks.FirstOrDefault(t => t.Name == "HelloTask");
if (task == null)
{
    Logger.Error("任务未注册");
}
1
2
3
4
5
6
7

# Q2: 任务重复执行?

检查:

  1. ✅ 是否启动了多个 Quartz 实例
  2. ✅ 是否使用了集群模式
  3. ✅ 检查 InstanceId 配置

解决方案:

// 使用集群模式
{
  "Quartz": {
    "InstanceName": "DarkM_Quartz",
    "InstanceId": "AUTO",
    "Clustered": true
  }
}
1
2
3
4
5
6
7
8

# Q3: 任务执行时间不准确?

解决方案:

Quartz 默认使用简单触发器,如果需要精确时间,使用 Cron 触发器:

// ✅ 推荐:Cron 触发器
public override string Cron => "0 0 2 * * ?";

// ❌ 不推荐:简单触发器
public override int Interval => 3600;
1
2
3
4
5

# Q4: 任务执行失败?

查看:

  1. ✅ 任务日志
  2. ✅ Quartz 执行历史
  3. ✅ 数据库中的错误信息

解决方案:

// 启用详细日志
{
  "Quartz": {
    "Logging": true,
    "LogLevel": "Debug"
  }
}
1
2
3
4
5
6
7

# Q5: 如何停止正在执行的任务?

解决方案:

  1. 使用取消令牌
public override async Task Execute(ITaskExecutionContext context)
{
    try
    {
        await DoWorkAsync(context.CancellationToken);
    }
    catch (OperationCanceledException)
    {
        Logger.Warn("任务被取消");
    }
}
1
2
3
4
5
6
7
8
9
10
11
  1. 暂停任务
await _quartzServer.PauseJob("HelloTask", "Demo");
1

# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20