ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Quartz.NET 多调度器实战:使用 Microsoft DI 在同一应用中运行多个独立 Scheduler

Quartz.NET 多调度器实战:使用 Microsoft DI 在同一应用中运行多个独立 Scheduler 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载本文基于 Quartz 4.x 官方文档《Multiple Schedulers with Microsoft DI》整理而成。单个应用进程里往往同时存在临时任务与持久化任务、关键业务与后台维护等互不干扰的调度需求本文讲解如何通过AddQuartz(string name, ...)注册多个命名调度器Named Scheduler每个调度器拥有独立的配置、任务、触发器、监听器与日历并通过ISchedulerRepository/ISchedulerRegistry在运行时按名称查找与枚举它们。读完你将掌握命名调度器的注册方式与适用场景、基于 Keyed Service 的依赖注入、appsettings.json 的批量配置方法以及每个调度器独立的启停控制。在 Microsoft DI 容器中AddQuartz(string name, ...)注册的是一个命名调度器。每个命名调度器都有自己的一套配置、Job、Trigger、Listener 和 Calendar并且全部通过常规的 DI 流式 API 配置。ISchedulerRepository负责按名称跟踪已经构建出来的调度器。::: tip 不使用 Microsoft DI 时可以每个调度器各用一个独立的QuartzSchedulerBuilder通过Create(q q.ConfigureScheduler(options options.InstanceName ...))配置实例名再对每个 builder 调用BuildScheduler()完成构建。从源码看BuildScheduler定义在 src/Quartz/QuartzSchedulerBuilder.cs是一个返回ValueTaskIScheduler的异步构建入口默认调度器的完整示例见 src/Quartz/README.md。 :::什么时候应该使用命名调度器命名调度器解决的是一个进程内多种调度诉求并存的问题官方文档给出的典型场景包括不同的 Job Store临时任务用内存存储RAMJobStore需要持久化的任务用数据库存储如 AdoJobStore。工作负载隔离关键任务与后台维护任务分别运行在独立的线程池上互不抢占执行资源。不同的配置misfire 阈值、批量大小batch size或集群clustering设置各调度器之间可以不同。从源码层面看命名调度器的独立性是有保障的在 src/Quartz/Configuration/QuartzServiceCollectionExtensions.cs 中AddQuartz(string name, ...)最终进入AddQuartzScheduler(services, schedulerName: name, ...)调度器的名字同时充当其服务注册键service key、实例名InstanceName和选项名options name因此它的注册与它的配置永远指向同一个对象。基本配置两个调度器并存给每次AddQuartz(string name, ...)调用一个唯一的名字即可。下面示例注册了两个调度器FastScheduler使用内存 Job Store 跑即时通知任务DurableScheduler使用 SQL Server 持久化存储跑报表任务最后一次AddQuartzHostedService调用即可启动全部命名调度器var builder Host.CreateApplicationBuilder(args); // First scheduler: fast in-memory jobs builder.Services.AddQuartz(FastScheduler, q { q.UseInMemoryStore(); q.UseDefaultThreadPool(tp tp.MaxConcurrency 5); q.ScheduleJobNotificationJob(trigger trigger .WithIdentity(notify-trigger) .WithSimpleSchedule(TimeSpan.FromSeconds(30))); }); // Second scheduler: persistent database jobs builder.Services.AddQuartz(DurableScheduler, q { q.UsePersistentStore(s { s.UseSqlServer(sqlServer { sqlServer.ConnectionString your connection string; }); s.UseSystemTextJsonSerializer(); }); q.ScheduleJobReportJob(trigger trigger .WithIdentity(report-trigger) .WithCronSchedule(0 0 2 * * ?)); }); // Single call starts all named schedulers builder.Services.AddQuartzHostedService(options { options.WaitForJobsToComplete true; }); builder.Build().Run();这段代码里UseInMemoryStore与UsePersistentStore配合UseSqlServerUseSystemTextJsonSerializer分别演示了内存与持久化两种 Job Store 的配置路径MaxConcurrency 5限定FastScheduler线程池并发度UseDefaultThreadPool的完整参数说明可参考微软 DI 集成文档 microsoft-di-integration.md。每个调度器独立的监听器与日历在某个命名AddQuartz调用内部注册的 Listener 和 Calendar只作用于该调度器builder.Services.AddQuartz(Scheduler1, q { q.AddSchedulerListenerAuditSchedulerListener(); q.AddJobListenerLoggingJobListener(); q.AddTriggerListenerMetricsTriggerListener(); q.AddCalendarHolidayCalendar(holidays, new AddCalendarOptions { Replace true, UpdateTriggers true }, cal cal.AddExcludedDay(new DateOnly(2025, 12, 25))); // These listeners and calendars only apply to Scheduler1 }); builder.Services.AddQuartz(Scheduler2, q { // Scheduler2 has no listeners or calendars unless explicitly added here });这里AddCalendarHolidayCalendar的AddCalendarOptions中Replace true表示已存在同名日历时覆盖UpdateTriggers true表示让已引用该日历的触发器立即感知日历变更。Scheduler2没有显式注册任何 Listener/Calendar因此它是干净的——这正体现了命名调度器按需装配、互不污染的隔离能力。注入一个命名调度器调度器的名字就是它的服务键service key因此可以把它作为Keyed Service注入public class MyService { private readonly IScheduler scheduler; public MyService([FromKeyedServices(FastScheduler)] IScheduler scheduler) { this.scheduler scheduler; } public async Task DoWork() { await scheduler.TriggerJob(new JobKey(my-job)); } }也可以直接从容器解析var fast provider.GetRequiredKeyedServiceIScheduler(FastScheduler); var standard provider.GetRequiredServiceIScheduler(); // the default scheduler, if one is registered命名调度器的每一个部件都注册在它的键之下例如GetRequiredKeyedServiceISchedulerFactory(FastScheduler)。无键unkeyed注册则属于默认调度器。这一点在源码中非常明确AddQuartz(name, ...)的注册流程见 src/Quartz/Configuration/QuartzServiceCollectionExtensions.cs将所有部件都以调度器名为键注册而默认调度器schedulerName null则使用Options.DefaultName作为选项名组件直接落进无键槽位。关于注入句柄的注意事项被注入的IScheduler实际上是一个句柄handle由于调度器构建是异步的而容器构造是同步的所以该句柄会在第一次使用时才真正构建调度器。官方文档明确提醒异步成员会等待构建完成永远安全。Status、SchedulerInstanceId、Context和ListenerManager这些同步属性如果读取它们会触发调度器构建则抛出InvalidOperationException。SchedulerName从不触发任何构建。在AddQuartzHostedService()之下宿主启动完成后同步成员就是安全的hosted service 在宿主启动期间就构建了每一个调度器早于你的业务代码运行之后才在ApplicationStarted事件触发时启动它们——除非关闭了AwaitApplicationStarted。这一行为的源码依据在 src/Quartz/Hosting/QuartzHostedService.cs启动任务分两种路径StartSchedulers(waitForApplicationStarted: false, ...)在宿主启动阶段就完成构建StartSchedulers(waitForApplicationStarted: true, ...)则等到ApplicationStarted之后才真正启动。运行时按名称查找调度器当调度器名称只在运行时才知道例如来自仪表盘页面或某个请求参数时使用容器的ISchedulerRepository。它保存每一个已被构建的调度器public class MyService { private readonly ISchedulerRepository schedulerRepository; public MyService(ISchedulerRepository schedulerRepository) { this.schedulerRepository schedulerRepository; } public async Task DoWork() { var scheduler schedulerRepository.Lookup(FastScheduler); if (scheduler ! null) { await scheduler.TriggerJob(new JobKey(my-job)); } // Or every scheduler this container has built var all schedulerRepository.LookupAll(); } }ISchedulerRepository定义在 src/Quartz/Extensibility/ISchedulerRepository.cs接口提供了Bind、Remove、Lookup(name)、LookupByName和LookupAll。注意它还有一个按instance id消歧的维度同名但不同实例 ID 的调度器可以共存例如指向同一集群不同节点的远程代理此时可在Lookup时传入instanceId参数区分。使用ISchedulerRepository时有两点需要注意仓库中只有已构建的调度器因此启动过程中它可能并不完整。按键注入Keyed Injection没有这个问题——句柄会构建它指向的那个调度器。仓库是**按容器per container**的不是按进程的。由独立QuartzSchedulerBuilder构建的调度器不在其中4.0 起不存在进程级全局调度器详见 migration-guide.md。如果要知道容器注册了哪些调度器无论是否已构建则解析ISchedulerRegistry并调用QuerySchedulers()。它针对每条注册返回一个SchedulerRegistration外加每一个已绑定进仓库但没有任何注册对应的调度器。尚未创建的调度器其Status为null并且查询不会触发创建。简言之做库存清单用QuerySchedulers()要活跃实例用LookupAll()。从实现上看ISchedulerRegistrysrc/Quartz/ISchedulerRegistry.cs与ISchedulerRepository的分工是注册 vs 运行前者读注册QuerySchedulers不会构建任何东西未解析的注册以null的SchedulerRegistration.Status呈现后者持实例只有别人请求过的东西才在里面。SchedulerRegistration的记录结构名称、来源SchedulerOrigin、状态Status、实例 ID 等定义在 src/Quartz/SchedulerRegistration.cs。混合使用默认调度器与命名调度器无名的AddQuartz()与命名调度器可以共存于同一容器// Default scheduler (traditional single-scheduler usage) builder.Services.AddQuartz(q { q.ScheduleJobMainJob(trigger trigger .WithIdentity(main-trigger) .WithSimpleSchedule(TimeSpan.FromMinutes(1))); }); // Additional named scheduler builder.Services.AddQuartz(Auxiliary, q { q.ScheduleJobCleanupJob(trigger trigger .WithIdentity(cleanup-trigger) .WithCronSchedule(0 0 3 * * ?)); }); // Starts both the default and the named scheduler builder.Services.AddQuartzHostedService();::: tip调用顺序无关紧要。hosted service 在宿主启动时才解析调度器因此无论AddQuartz在它之前还是之后调用它都会启动每一个已注册的调度器。如果容器中根本没有调度器启动时会直接报告出来而不是静默无作为。这正是 src/Quartz/Hosting/QuartzServiceCollectionExtensions.cs 中AddQuartzHostedService不再必须在AddQuartz之后调用才生效的设计初衷。 :::通过 appsettings.json 配置传入 Quartz 配置的根 section命名调度器的设置从Schedulers:{name}读取builder.AddQuartz(DurableScheduler); // or, naming the section yourself: builder.Services.AddQuartz(DurableScheduler, builder.Configuration.GetSection(Quartz));AddQuartzSchedulers则为Schedulers的每个子节点注册一个命名调度器builder.AddQuartzSchedulers(); // or: builder.Services.AddQuartzSchedulers(builder.Configuration.GetSection(Quartz));对应的 JSON 配置{ Quartz: { Schedulers: { DurableScheduler: { Scheduler: { InstanceId: AUTO }, JobStore: { Type: Quartz.Impl.AdoJobStore.LocalTransactionJobStore, Quartz } } } } }源码实现位于 src/Quartz/Configuration/QuartzServiceCollectionExtensions.csAddQuartzSchedulers先校验 section 中存在Schedulers子节点再遍历其每个子节点把子节点 key 作为调度器名调用AddQuartz(services, scheduler.Key, scheduler, configure)。同时它做了三处防呆校验没有Schedulers子节点时报错提示改用AddQuartz(configuration)Schedulers与直接调度器配置并存时报错Schedulers与顶层Schedule/Scheduling并存时报错Job/Trigger 必须归属到具体调度器名下。对于没有对应类型化选项的扁平键例如某个插件的自有设置放进命名选项的Properties字典builder.Services.ConfigureQuartzOptions(DurableScheduler, options options.Properties[quartz.plugin.myPlugin.someSetting] value);注意这里ConfigureQuartzOptions(DurableScheduler, ...)的命名方式调度器名即选项名这条规则贯穿整个多调度器机制。每个调度器独立的启动与关闭AddQuartzHostedService(configure)配置的是每一个调度器带名字的重载为某一个调度器覆盖该配置两种调用顺序均无影响// shared by every scheduler builder.Services.AddQuartzHostedService(options options.WaitForJobsToComplete true); // ...except this one, which waits longer before its first fire builder.Services.AddQuartzHostedService(DurableScheduler, options { options.StartDelay TimeSpan.FromMinutes(2); });这条规则的实现见 src/Quartz/Hosting/QuartzServiceCollectionExtensions.cs无名的AddQuartzHostedService使用ConfigureAll让选项作用于所有调度器带名字的重载则使用PostConfigure(schedulerName, configure)保证该调度器自己的设置在共享设置之后应用因此无论两个调用谁先谁后命名的覆盖总是生效。两个重载最终都通过内部AddHostedService注册同一个QuartzHostedService实现确保容器中恰好只有一个 hosted service 实例src/Quartz/Hosting/QuartzServiceCollectionExtensions.cs。QuartzHostedServiceOptionssrc/Quartz/Hosting/QuartzHostedServiceOptions.cs中与本例相关的可配置项包括属性默认值说明WaitForJobsToCompletefalse为true时关闭流程会等待所有正在执行的 Job 完成后再返回StartDelaynull非空时调度器在指定延迟后启动若AwaitApplicationStarted为true延迟从应用启动完成时开始计时AwaitApplicationStartedtrue为true默认时 Job 直到应用启动完成后才开始执行避免应用启动期间就运行 JobAutoStarttrue为true默认时 hosted service 启动调度器设为false则只构建、初始化并绑定调度器让其停留在SchedulerStatus.Created由应用在合适时机自行Start()AutoStart的语义值得一提它优先于AwaitApplicationStarted与StartDelay后两者描述的是何时启动而AutoStartfalse是根本不由 hosted service 启动。但关闭不受影响——hosted service 仍会关闭它创建的每一个调度器无论是否启动过。一次性配置所有调度器ConfigureAllQuartzSchedulers(configure)把同一个 builder 回调应用到每一个通过AddQuartz、AddQuartz(name, …)或AddQuartzSchedulers注册的调度器无论该调用发生在它之前还是之后回调添加的每个组件每个调度器都得到自己的实例一个插件加到三个调度器上就是三个插件实例。通过AddQuartzHttpClient注册的远程调度器没有 builder会被跳过。实现见 src/Quartz/Configuration/QuartzServiceCollectionExtensions.cs它借助SchedulerNameRegistry先记录回调保证后续注册的调度器也被覆盖再对已注册的默认调度器和所有命名调度器逐个Apply。因为委托按调度器各得一个 builder其注册落在该调度器自己的键下等价于写在该调度器的AddQuartz(name, q …)回调内部——这正是每个调度器一份实例、而非共享一份的来源。更细的语义比如给每个调度器相同的东西在租户隔离场景下的用法参见 multi-tenancy.md。限制调度器名称必须唯一比较时忽略大小写。Job 类型不是限制。AddJobT以无键方式注册类型因此同一个 Job 类可以服务所有调度器而AddJobTypeTJob, TImplementation()、AddJobTypeTJob(lifetime)和AddJobTypeTJob(factory)注册在某一个调度器的键之下Job 工厂会先检查该键再回退到容器的无键注册。于是两个调度器可以以不同方式构建同一个 Job 类型。详见 multi-tenancy.md 与 microsoft-di-integration.md 中关于 Job 构造方式的章节作用域注册、TryAdd语义、每触发一次开一个 scope 等。延伸阅读单调度器的完整 DI 集成作业构造、持久化存储、日历、插件、超时与监听器的组装范例microsoft-di-integration.md多租户场景下每个调度器配同一套东西与 Job 类型注册的细节multi-tenancy.md4.0 移除进程级全局调度器与连接状态的迁移说明migration-guide.md赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐Quasar-Preview训练策略揭秘三阶段训练与去中心化蒸馏完整解析Quasar Preview训练策略揭秘三阶段训练与去中心化蒸馏完整解析 Quasar Preview是一个革命性的混合架构大语言模型采用创新的三阶段训练策MQTTnet多租户架构在同一Broker中服务多个独立应用MQTTnet多租户架构在同一Broker中服务多个独立应用 终极指南 如何在单个MQTT Broker中实现完美的多租户隔离让多个应用共享同一消息物联网消息队列Cling多解释器环境如何在同一进程中运行多个独立C解释器Cling多解释器环境如何在同一进程中运行多个独立C解释器 想要在同一进程中同时运行多个独立的C解释器实例吗Cling作为基于LLVM的C交互式上一篇CANN shmem 算子开发 Phase 0 启动门禁与 Intake 清单指南下一篇CANN HCCL 点对点通信实战基于 HcclSend/HcclRecv 实现单进程多卡数据收发创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表