Quartz.Net触发器重复触发问题排查求助
Hey there, let's break down why your Quartz triggers are firing multiple times and how to fix it. Based on your code and the behavior you described, here are the most likely culprits and actionable fixes:
Key Issues Identified
Multiple Scheduler Instances Running
Your current setup creates a newJobSchedulerevery time the app starts (viaApplication_Start). If IIS recycles the app pool (especially with overlapping recycling enabled), old scheduler processes might not shut down properly, leaving multiple instances running—each firing the same triggers. That's why you saw the count jump from 1 to 2 and eventually stabilize at 4.Potential Trigger/Job Identity Collisions
If your 8 additional triggers/jobs use the sameWithIdentity("job", "Information")andWithIdentity("trigger", "Information")as the first one, Quartz might be registering duplicates or overwriting entries in unexpected ways.Static JobDataMap Values
You're settingtoDateandfromDateto values calculated when the scheduler starts, not when the trigger fires. This means every email will use the app start time's window, not the actual trigger time's window—this isn't causing the duplicate fires, but it's a critical bug to fix.
Step-by-Step Fixes
1. Make the Scheduler a Singleton and Handle Shutdown Properly
Quartz works best with a single scheduler instance. Modify your JobScheduler to be a singleton, and ensure it shuts down cleanly when the app stops:
JobScheduler.cs (Singleton Update)
public class JobScheduler { private static readonly JobScheduler _instance = new JobScheduler(); private readonly IScheduler _scheduler; private readonly LoggingRepository logging = new LoggingRepository(); // Private constructor to enforce singleton private JobScheduler() { // Initialize scheduler once _scheduler = StdSchedulerFactory.GetDefaultScheduler().Result; } public static JobScheduler Instance => _instance; public void Start(string serverPath, string httpPath) { // Don't restart if already running if (_scheduler.IsStarted) { logging.WriteLog(1, "JobScheduler", "Start", "Scheduler already running", "Skipping start", LogType.Info, ""); return; } try { var to = "clm@thansen.dk"; var serverMapPath = serverPath; var httpRunPath = httpPath; var workingDays = new DayOfWeek[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday }; _scheduler.Start(); logging.WriteLog(1, "JobScheduler", "Start", "Scheduler started", $"InstanceId: {_scheduler.SchedulerInstanceId}", LogType.Info, ""); // Create triggers for each hour (8-16) with unique identities for (int hour = 8; hour <= 16; hour++) { var jobIdentity = new JobKey($"brokenImgJob_{hour}", "Information"); var triggerIdentity = new TriggerKey($"brokenImgTrigger_{hour}", "Information"); // Skip if job/trigger already exists (prevents duplicates on app restart) if (_scheduler.CheckExists(jobIdentity).Result) continue; IJobDetail emailerJob = JobBuilder.Create<BrokenIMGMailJob>() .WithIdentity(jobIdentity) .WithDescription($"Broken image email job for {hour}:00") .Build(); ITrigger emailerTrigger = TriggerBuilder.Create() .WithIdentity(triggerIdentity) .UsingJobData("to", to) .UsingJobData("serverMapPath", serverMapPath) .UsingJobData("httpRunPath", httpRunPath) .WithSchedule(CronScheduleBuilder .AtHourAndMinuteOnGivenDaysOfWeek(hour, 0, workingDays)) .Build(); _scheduler.ScheduleJob(emailerJob, emailerTrigger).Wait(); } } catch (Exception ex) { logging.WriteLog(1, "JobScheduler", "Start", "Start trigger failed", $"EXCEPTION MESSAGE: {ex.Message} | INNER: {ex.InnerException}", LogType.Exception, ""); } } public void Shutdown() { if (_scheduler.IsStarted) { // Wait for running jobs to finish before shutting down _scheduler.Shutdown(true).Wait(); logging.WriteLog(1, "JobScheduler", "Shutdown", "Scheduler stopped", "", LogType.Info, ""); } } }
Update Global.asax to Use Singleton and Handle Shutdown
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); var httpRunPath = HttpRuntime.AppDomainAppPath; var serverMapPath = Server.MapPath("."); JobScheduler.Instance.Start(serverMapPath, httpRunPath); } protected void Application_End() { JobScheduler.Instance.Shutdown(); }
2. Disable IIS App Pool Overlapping Recycling
IIS's default overlapping recycling keeps old app processes running while new ones start—this can lead to multiple scheduler instances. Turn it off:
- Open IIS Manager → Your App Pool → Advanced Settings
- Set Overlapped Recycling to
False - Restart the app pool
3. Fix JobDataMap Time Values
Calculate the time window when the job runs, not when the scheduler starts. Update your job's Execute method:
[DisallowConcurrentExecution] public class BrokenIMGMailJob : IJob { LoggingRepository logging = new LoggingRepository(); Email _email = new Email(); public async Task Execute(IJobExecutionContext context) { try { JobDataMap dataMap = context.MergedJobDataMap; var tmpTo = dataMap.GetString("to"); // Calculate time window at trigger time, not app start var toDate = DateTime.Now; var fromDate = DateTime.Now.AddHours(-7); // Rest of your email logic... } catch (Exception ex) { logging.WriteLog(1, "JobClass", "BrokenIMGMailJob","Send broken images mail", $"EXCEPTION MESSAGE: {ex.Message} | INNER: {ex.InnerException}", LogType.Exception, ""); } } }
4. Verify Trigger Uniqueness
The loop in the singleton JobScheduler creates unique identities for each job/trigger (brokenImgJob_8, brokenImgTrigger_8, etc.), which prevents Quartz from registering duplicate triggers.
Why [DisallowConcurrentExecution] Didn't Fix It
This attribute only prevents the same job from running concurrently—it doesn't stop multiple triggers from firing the job multiple times. It's useful for ensuring your email logic doesn't run in parallel, but it won't solve the duplicate trigger issue.
After implementing these fixes, your triggers should fire exactly once per scheduled hour.
内容的提问来源于stack exchange,提问作者Carsten

