znlgis 博客

GIS开发与技术分享

第十二章:定时任务

一、定时任务概述

定时任务是后端应用中非常常见的需求,如定时数据清理、报表生成、状态检查、消息推送等。Furion 框架提供了多种定时任务实现方式,包括 SpareTime 静态类、IJob 接口、以及新版 Schedule 模块,满足从简单到复杂的各种调度需求。

1.1 定时任务应用场景

场景 说明 频率
数据库备份 定期备份数据库 每天凌晨
缓存刷新 定期更新缓存数据 每隔几分钟
订单超时处理 取消超时未支付的订单 每分钟检查
报表生成 生成日/周/月报表 按时间周期
日志清理 清理过期日志文件 每周/每月
健康检查 检测外部服务可用性 每隔几秒
消息推送 定时推送消息通知 定时触发
数据同步 与第三方系统同步数据 每隔一段时间

1.2 Furion 定时任务方案对比

方案 适用场景 复杂度 特点
SpareTime 简单定时任务 静态方法调用,快速上手
IJob 接口 中等复杂度任务 面向接口,便于管理
Schedule 模块 复杂调度需求 中高 功能完善,支持持久化
BackgroundService .NET 原生方案 适合长期运行的后台服务

二、SpareTime 静态类

2.1 基本用法

SpareTime 是 Furion 提供的轻量级定时任务静态类,适合快速创建简单任务:

// 间隔执行(每5秒执行一次)
SpareTime.Do(5000, (timer, count) =>
{
    Console.WriteLine($"第 {count} 次执行,时间:{DateTime.Now}");
}, "MyTimer");

// 只执行一次
SpareTime.DoOnce(3000, (timer, count) =>
{
    Console.WriteLine("延迟3秒后执行一次");
}, "OnceTimer");

// Cron 表达式
SpareTime.Do("*/5 * * * *", (timer, count) =>
{
    Console.WriteLine("每5分钟执行一次");
}, "CronTimer");

2.2 任务暂停与恢复

// 创建任务
SpareTime.Do(5000, (timer, count) =>
{
    Console.WriteLine($"正在执行任务...");
}, "task1");

// 暂停任务
SpareTime.Stop("task1");

// 恢复任务
SpareTime.Start("task1");

// 销毁任务
SpareTime.Cancel("task1");

// 获取所有任务
var allTimers = SpareTime.GetTimers();
foreach (var timer in allTimers)
{
    Console.WriteLine($"任务:{timer.WorkerName},状态:{timer.Status}");
}

2.3 在控制器中使用

[ApiDescriptionSettings("定时任务管理")]
public class TimerAppService : IDynamicApiController
{
    /// <summary>
    /// 启动数据同步任务
    /// </summary>
    public void StartSyncTask()
    {
        SpareTime.Do(60000, async (timer, count) =>
        {
            await SyncDataFromExternalApi();
        }, "DataSyncTimer", description: "数据同步定时任务");
    }

    /// <summary>
    /// 停止数据同步任务
    /// </summary>
    public void StopSyncTask()
    {
        SpareTime.Stop("DataSyncTimer");
    }

    /// <summary>
    /// 获取所有任务状态
    /// </summary>
    public object GetAllTimerStatus()
    {
        return SpareTime.GetTimers().Select(t => new
        {
            t.WorkerName,
            t.Description,
            Status = t.Status.ToString(),
            t.Tally
        });
    }

    private Task SyncDataFromExternalApi() => Task.CompletedTask;
}

三、IJob 接口

3.1 定义 Job

通过实现 IJob 接口定义定时任务:

/// <summary>
/// 订单超时检查任务
/// </summary>
[JobDetail("OrderTimeoutJob", Description = "检查并取消超时未支付订单")]
[Cron("0 */1 * * * ?", TriggerId = "OrderTimeoutTrigger", Description = "每分钟执行")]
public class OrderTimeoutJob : IJob
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<OrderTimeoutJob> _logger;

    public OrderTimeoutJob(
        IServiceScopeFactory scopeFactory,
        ILogger<OrderTimeoutJob> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
    {
        _logger.LogInformation("开始检查超时订单...");

        using var scope = _scopeFactory.CreateScope();
        var orderRepository = scope.ServiceProvider.GetRequiredService<IRepository<Order>>();

        var timeoutThreshold = DateTime.Now.AddMinutes(-30);
        var timeoutOrders = await orderRepository
            .Where(o => o.Status == OrderStatus.Pending && o.CreatedTime < timeoutThreshold)
            .ToListAsync(stoppingToken);

        foreach (var order in timeoutOrders)
        {
            order.Status = OrderStatus.Cancelled;
            order.CancelReason = "超时未支付,系统自动取消";
            _logger.LogInformation("订单 {OrderId} 已自动取消", order.Id);
        }

        await orderRepository.SaveNowAsync(stoppingToken);
        _logger.LogInformation("超时订单检查完成,共取消 {Count} 个订单", timeoutOrders.Count);
    }
}

3.2 多触发器任务

一个 Job 可以配置多个触发器:

/// <summary>
/// 报表生成任务
/// </summary>
[JobDetail("ReportJob", Description = "报表生成")]
[Cron("0 0 2 * * ?", TriggerId = "DailyReportTrigger", Description = "每天凌晨2点生成日报")]
[Cron("0 0 3 ? * MON", TriggerId = "WeeklyReportTrigger", Description = "每周一凌晨3点生成周报")]
[Cron("0 0 4 1 * ?", TriggerId = "MonthlyReportTrigger", Description = "每月1日凌晨4点生成月报")]
public class ReportJob : IJob
{
    private readonly ILogger<ReportJob> _logger;

    public ReportJob(ILogger<ReportJob> logger) => _logger = logger;

    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
    {
        var triggerId = context.TriggerId;

        switch (triggerId)
        {
            case "DailyReportTrigger":
                await GenerateDailyReport();
                break;
            case "WeeklyReportTrigger":
                await GenerateWeeklyReport();
                break;
            case "MonthlyReportTrigger":
                await GenerateMonthlyReport();
                break;
        }
    }

    private async Task GenerateDailyReport()
    {
        _logger.LogInformation("生成日报...");
        await Task.CompletedTask;
    }

    private async Task GenerateWeeklyReport()
    {
        _logger.LogInformation("生成周报...");
        await Task.CompletedTask;
    }

    private async Task GenerateMonthlyReport()
    {
        _logger.LogInformation("生成月报...");
        await Task.CompletedTask;
    }
}

四、Cron 表达式详解

4.1 Cron 表达式格式

Furion 支持标准的 6 位 Cron 表达式(含秒)和 7 位表达式(含年):

秒 分 时 日 月 周 [年]
*  *  *  *  *  *  [*]

4.2 字段说明

字段 允许值 允许特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * / ? L W
1-12 或 JAN-DEC , - * /
0-6 或 SUN-SAT , - * / ? L #
年(可选) 1970-2099 , - * /

4.3 常用 Cron 表达式

表达式 含义
* * * * * ? 每秒执行
0 * * * * ? 每分钟执行
0 */5 * * * ? 每5分钟执行
0 0 * * * ? 每小时执行
0 0 2 * * ? 每天凌晨2点执行
0 0 2 1 * ? 每月1日凌晨2点执行
0 0 10,14,16 * * ? 每天10点、14点、16点执行
0 0/30 9-17 * * ? 每天9-17点每半小时执行
0 0 2 ? * MON-FRI 工作日凌晨2点执行
0 0 2 L * ? 每月最后一天凌晨2点执行
0 0 2 ? * 6#3 每月第三个周五凌晨2点执行

4.4 特殊字符说明

字符 含义 示例
* 所有值 每秒/每分/每时
? 不指定值 用于日和周字段
- 范围 10-12 表示10到12
, 列表 MON,WED,FRI
/ 步长 0/15 从0开始每15
L 最后 月最后一天或周最后一天
W 工作日 15W 最近15号的工作日
# 第几个 6#3 第三个周五

五、任务注册与管理

5.1 在 Program.cs 中注册

var builder = WebApplication.CreateBuilder(args);

// 注册 Schedule 模块
builder.Services.AddSchedule(options =>
{
    // 自动扫描并注册所有 IJob 实现
    options.AddJob<OrderTimeoutJob>();
    options.AddJob<ReportJob>();
    options.AddJob<DataCleanupJob>();
});

var app = builder.Build();
app.Run();

5.2 动态管理任务

[ApiDescriptionSettings("任务管理")]
public class ScheduleAppService : IDynamicApiController
{
    private readonly ISchedulerFactory _schedulerFactory;

    public ScheduleAppService(ISchedulerFactory schedulerFactory)
    {
        _schedulerFactory = schedulerFactory;
    }

    /// <summary>
    /// 添加新任务
    /// </summary>
    public void AddJob(AddJobInput input)
    {
        _schedulerFactory.AddJob(
            JobBuilder.Create<DynamicJob>()
                .SetJobId(input.JobId)
                .SetDescription(input.Description),
            TriggerBuilder.Create(input.CronExpression)
                .SetTriggerId(input.TriggerId)
                .SetDescription(input.TriggerDescription)
        );
    }

    /// <summary>
    /// 暂停任务
    /// </summary>
    public void PauseJob(string jobId)
    {
        var scheduler = _schedulerFactory.GetJob(jobId);
        scheduler?.Pause();
    }

    /// <summary>
    /// 恢复任务
    /// </summary>
    public void ResumeJob(string jobId)
    {
        var scheduler = _schedulerFactory.GetJob(jobId);
        scheduler?.Start();
    }

    /// <summary>
    /// 移除任务
    /// </summary>
    public void RemoveJob(string jobId)
    {
        _schedulerFactory.RemoveJob(jobId);
    }

    /// <summary>
    /// 获取所有任务信息
    /// </summary>
    public object GetAllJobs()
    {
        return _schedulerFactory.GetJobs().Select(j => new
        {
            j.JobId,
            j.Description,
            Status = j.GetStatus().ToString(),
            Triggers = j.GetTriggers().Select(t => new
            {
                t.TriggerId,
                t.Description,
                t.NextRunTime,
                t.LastRunTime,
                Status = t.Status.ToString()
            })
        });
    }

    /// <summary>
    /// 立即触发任务
    /// </summary>
    public void TriggerJob(string jobId)
    {
        var scheduler = _schedulerFactory.GetJob(jobId);
        scheduler?.Run();
    }
}

六、任务监控

6.1 任务执行日志

/// <summary>
/// 任务监控器
/// </summary>
public class JobMonitor : IJobMonitor
{
    private readonly ILogger<JobMonitor> _logger;

    public JobMonitor(ILogger<JobMonitor> logger) => _logger = logger;

    /// <summary>
    /// 任务执行前
    /// </summary>
    public Task OnExecutingAsync(JobExecutingContext context, CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "任务 [{JobId}] 开始执行,触发器:{TriggerId},执行时间:{Time}",
            context.JobId, context.TriggerId, DateTime.Now);
        return Task.CompletedTask;
    }

    /// <summary>
    /// 任务执行后
    /// </summary>
    public Task OnExecutedAsync(JobExecutedContext context, CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "任务 [{JobId}] 执行完成,耗时:{Elapsed}ms,结果:{Result}",
            context.JobId, context.Elapsed.TotalMilliseconds, context.Result);
        return Task.CompletedTask;
    }
}

6.2 注册监控器

builder.Services.AddSchedule(options =>
{
    options.AddJob<OrderTimeoutJob>();

    // 注册任务监控器
    options.AddMonitor<JobMonitor>();
});

6.3 任务执行器(自定义执行逻辑)

/// <summary>
/// 自定义任务执行器
/// </summary>
public class CustomJobExecutor : IJobExecutor
{
    private readonly ILogger<CustomJobExecutor> _logger;

    public CustomJobExecutor(ILogger<CustomJobExecutor> logger) => _logger = logger;

    public async Task ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken)
    {
        try
        {
            // 执行前记录
            var stopwatch = System.Diagnostics.Stopwatch.StartNew();

            // 执行任务
            await jobHandler.ExecuteAsync(context, stoppingToken);

            stopwatch.Stop();
            _logger.LogInformation("任务 {JobId} 执行成功,耗时 {Elapsed}ms",
                context.JobId, stopwatch.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "任务 {JobId} 执行失败:{Message}", context.JobId, ex.Message);

            // 可以在此处发送告警通知
            await SendAlertNotification(context.JobId, ex.Message);
        }
    }

    private Task SendAlertNotification(string jobId, string error) => Task.CompletedTask;
}

七、任务异常处理

7.1 异常捕获与处理

[JobDetail("ResilientJob", Description = "带异常处理的任务")]
[Cron("0 */5 * * * ?", TriggerId = "ResilientTrigger")]
public class ResilientJob : IJob
{
    private readonly ILogger<ResilientJob> _logger;

    public ResilientJob(ILogger<ResilientJob> logger) => _logger = logger;

    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
    {
        try
        {
            await DoWork(stoppingToken);
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("任务被取消");
        }
        catch (TimeoutException ex)
        {
            _logger.LogError(ex, "任务执行超时");
            // 记录失败并等待下次重试
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "任务执行异常");

            // 根据异常类型决定是否需要人工介入
            if (ex is CriticalException)
            {
                await NotifyAdministrator(ex);
            }
        }
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        // 实际业务逻辑
        await Task.Delay(1000, stoppingToken);
    }

    private Task NotifyAdministrator(Exception ex) => Task.CompletedTask;
}

7.2 重试策略

[JobDetail("RetryJob", Description = "带重试的任务")]
[Cron("0 */10 * * * ?", TriggerId = "RetryTrigger")]
public class RetryJob : IJob
{
    private readonly ILogger<RetryJob> _logger;
    private const int MaxRetries = 3;

    public RetryJob(ILogger<RetryJob> logger) => _logger = logger;

    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
    {
        var retryCount = 0;

        while (retryCount < MaxRetries)
        {
            try
            {
                await ExecuteCore(stoppingToken);
                _logger.LogInformation("任务执行成功");
                return;
            }
            catch (Exception ex) when (retryCount < MaxRetries - 1)
            {
                retryCount++;
                var delay = (int)Math.Pow(2, retryCount) * 1000; // 指数退避
                _logger.LogWarning(ex, "任务执行失败,{RetryCount}/{MaxRetries} 次重试,等待 {Delay}ms",
                    retryCount, MaxRetries, delay);
                await Task.Delay(delay, stoppingToken);
            }
        }
    }

    private Task ExecuteCore(CancellationToken stoppingToken) => Task.CompletedTask;
}

八、Schedule 模块(新版)

8.1 Schedule 模块概述

Furion 新版 Schedule 模块提供了更强大、更灵活的任务调度能力:

builder.Services.AddSchedule(options =>
{
    // 构建器方式创建任务
    options.AddJob(JobBuilder.Create<DataSyncJob>()
        .SetJobId("data-sync")
        .SetDescription("数据同步任务")
        .SetConcurrent(false), // 不允许并发执行
        TriggerBuilder.Create("*/30 * * * * ?")
            .SetTriggerId("data-sync-trigger")
            .SetDescription("每30秒执行")
            .SetMaxNumberOfRuns(100) // 最多执行100次
            .SetStartNow(true) // 立即开始
    );
});

8.2 任务构建器

// 使用 JobBuilder 灵活配置任务
var jobBuilder = JobBuilder.Create<CleanupJob>()
    .SetJobId("cleanup-job")
    .SetDescription("清理临时数据")
    .SetGroupName("maintenance")
    .SetConcurrent(false);

// 使用 TriggerBuilder 配置触发器
var triggerBuilder = TriggerBuilder.Create("0 0 3 * * ?")
    .SetTriggerId("cleanup-trigger")
    .SetDescription("每天凌晨3点执行")
    .SetRunOnStart(false)
    .SetStartTime(DateTime.Now.AddHours(1)) // 1小时后开始
    .SetEndTime(DateTime.Now.AddMonths(6)); // 6个月后结束

8.3 动态创建任务

/// <summary>
/// 动态任务服务
/// </summary>
public class DynamicJobService
{
    private readonly ISchedulerFactory _factory;

    public DynamicJobService(ISchedulerFactory factory) => _factory = factory;

    /// <summary>
    /// 创建一次性任务
    /// </summary>
    public void CreateOneTimeJob(string jobId, DateTime executeTime)
    {
        _factory.AddJob(
            JobBuilder.Create<DynamicJob>()
                .SetJobId(jobId)
                .SetDescription("一次性任务"),
            TriggerBuilder.Create()
                .SetTriggerId($"{jobId}-trigger")
                .SetStartTime(executeTime)
                .SetMaxNumberOfRuns(1)
        );
    }

    /// <summary>
    /// 更新任务触发器
    /// </summary>
    public void UpdateJobTrigger(string jobId, string triggerId, string newCron)
    {
        var scheduler = _factory.GetJob(jobId);
        scheduler?.UpdateTrigger(triggerId, trigger =>
        {
            trigger.SetCron(newCron);
        });
    }
}

九、后台任务(BackgroundService)

9.1 使用 .NET 原生 BackgroundService

/// <summary>
/// 消息队列消费后台服务
/// </summary>
public class MessageConsumerService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<MessageConsumerService> _logger;

    public MessageConsumerService(
        IServiceScopeFactory scopeFactory,
        ILogger<MessageConsumerService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("消息消费服务已启动");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _scopeFactory.CreateScope();
                var messageService = scope.ServiceProvider.GetRequiredService<IMessageService>();

                var messages = await messageService.GetPendingMessagesAsync(stoppingToken);
                foreach (var message in messages)
                {
                    await messageService.ProcessMessageAsync(message, stoppingToken);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "消息处理异常");
            }

            await Task.Delay(5000, stoppingToken);
        }

        _logger.LogInformation("消息消费服务已停止");
    }
}

9.2 注册后台服务

builder.Services.AddHostedService<MessageConsumerService>();

9.3 带健康检查的后台服务

/// <summary>
/// 带健康检查的后台任务
/// </summary>
public class HealthCheckBackgroundService : BackgroundService
{
    private readonly ILogger<HealthCheckBackgroundService> _logger;
    private readonly IHttpClientFactory _httpClientFactory;
    private DateTime _lastHealthyTime = DateTime.Now;

    public HealthCheckBackgroundService(
        ILogger<HealthCheckBackgroundService> logger,
        IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var endpoints = new[]
            {
                "https://api.service1.com/health",
                "https://api.service2.com/health",
                "https://api.service3.com/health"
            };

            foreach (var endpoint in endpoints)
            {
                try
                {
                    var client = _httpClientFactory.CreateClient();
                    client.Timeout = TimeSpan.FromSeconds(10);
                    var response = await client.GetAsync(endpoint, stoppingToken);

                    if (response.IsSuccessStatusCode)
                    {
                        _logger.LogDebug("{Endpoint} 健康检查通过", endpoint);
                        _lastHealthyTime = DateTime.Now;
                    }
                    else
                    {
                        _logger.LogWarning("{Endpoint} 健康检查失败,状态码:{StatusCode}",
                            endpoint, response.StatusCode);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "{Endpoint} 健康检查异常", endpoint);
                }
            }

            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

十、分布式任务调度

10.1 分布式锁防止重复执行

在多实例部署时,需要使用分布式锁防止任务重复执行:

[JobDetail("DistributedJob", Description = "分布式安全任务")]
[Cron("0 */5 * * * ?", TriggerId = "DistributedTrigger")]
public class DistributedJob : IJob
{
    private readonly IDistributedLock _distributedLock;
    private readonly ILogger<DistributedJob> _logger;

    public DistributedJob(
        IDistributedLock distributedLock,
        ILogger<DistributedJob> logger)
    {
        _distributedLock = distributedLock;
        _logger = logger;
    }

    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
    {
        var lockKey = $"job:lock:{context.JobId}";

        // 尝试获取分布式锁
        await using var lockHandle = await _distributedLock.TryAcquireAsync(lockKey, TimeSpan.FromMinutes(5));
        if (lockHandle == null)
        {
            _logger.LogInformation("任务 {JobId} 已在其他实例执行中,跳过", context.JobId);
            return;
        }

        _logger.LogInformation("获取锁成功,开始执行任务 {JobId}", context.JobId);
        await DoDistributedWork(stoppingToken);
    }

    private Task DoDistributedWork(CancellationToken stoppingToken) => Task.CompletedTask;
}

10.2 任务持久化

/// <summary>
/// 基于数据库的任务持久化器
/// </summary>
public class DbJobPersistence : IJobPersistence
{
    private readonly IServiceScopeFactory _scopeFactory;

    public DbJobPersistence(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;

    /// <summary>
    /// 加载持久化的任务
    /// </summary>
    public IEnumerable<SchedulerBuilder> Preload()
    {
        using var scope = _scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var jobs = dbContext.ScheduledJobs
            .Where(j => j.IsEnabled)
            .ToList();

        return jobs.Select(j => SchedulerBuilder.Create(
            JobBuilder.Create(j.JobType)
                .SetJobId(j.JobId)
                .SetDescription(j.Description),
            TriggerBuilder.Create(j.CronExpression)
                .SetTriggerId(j.TriggerId)
        ));
    }

    /// <summary>
    /// 持久化任务变更
    /// </summary>
    public void OnChanged(PersistenceContext context)
    {
        using var scope = _scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        switch (context.Behavior)
        {
            case PersistenceBehavior.Appended:
                dbContext.ScheduledJobs.Add(new ScheduledJobEntity
                {
                    JobId = context.JobId,
                    CronExpression = context.Trigger?.Cron,
                    IsEnabled = true
                });
                break;

            case PersistenceBehavior.Removed:
                var job = dbContext.ScheduledJobs.Find(context.JobId);
                if (job != null)
                    dbContext.ScheduledJobs.Remove(job);
                break;

            case PersistenceBehavior.Updated:
                var existing = dbContext.ScheduledJobs.Find(context.JobId);
                if (existing != null)
                    existing.CronExpression = context.Trigger?.Cron;
                break;
        }

        dbContext.SaveChanges();
    }
}

十一、定时任务最佳实践

11.1 任务设计原则

/// <summary>
/// 最佳实践示例:幂等且可恢复的任务
/// </summary>
[JobDetail("BestPracticeJob", Description = "最佳实践任务示例")]
[Cron("0 */10 * * * ?", TriggerId = "BestPracticeTrigger")]
public class BestPracticeJob : IJob
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<BestPracticeJob> _logger;

    public BestPracticeJob(IServiceScopeFactory scopeFactory, ILogger<BestPracticeJob> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
    {
        // 1. 使用独立作用域
        using var scope = _scopeFactory.CreateScope();

        // 2. 幂等处理 - 检查是否已处理
        var processedIds = new HashSet<long>();

        // 3. 批量处理,控制单次处理量
        const int batchSize = 100;
        var items = await GetPendingItems(scope, batchSize, stoppingToken);

        // 4. 逐条处理,记录进度
        foreach (var item in items)
        {
            if (stoppingToken.IsCancellationRequested) break;

            try
            {
                await ProcessItem(scope, item);
                processedIds.Add(item.Id);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "处理项 {ItemId} 失败", item.Id);
                // 单条失败不影响其他项
            }
        }

        _logger.LogInformation("任务完成,成功处理 {Count}/{Total} 项",
            processedIds.Count, items.Count);
    }

    private Task<List<TaskItem>> GetPendingItems(IServiceScope scope, int batchSize, CancellationToken ct)
        => Task.FromResult(new List<TaskItem>());

    private Task ProcessItem(IServiceScope scope, TaskItem item) => Task.CompletedTask;
}

public class TaskItem
{
    public long Id { get; set; }
    public string Data { get; set; }
}

11.2 注意事项

要点 说明
幂等性 任务应设计为可重复执行而不产生副作用
超时控制 设置合理的超时时间,避免任务阻塞
异常处理 捕获异常并记录日志,避免任务静默失败
批量处理 控制单次处理数据量,避免内存溢出
作用域管理 使用 IServiceScopeFactory 创建独立作用域
并发控制 多实例部署时使用分布式锁
监控告警 配置任务监控和异常告警
日志记录 记录任务开始、结束、耗时和结果

通过合理选择和使用 Furion 提供的定时任务方案,可以轻松应对从简单的周期性任务到复杂的分布式调度场景。关键在于根据实际需求选择合适的方案,并遵循最佳实践来确保任务的可靠性和可维护性。