IIS 10.0上C# Web应用FluentScheduler Job遇ThreadAbortException后停止重复执行问题
解决方案:让FluentScheduler Job在系统终止线程后持续运行+大数据量优化
嘿,看了你的问题,我之前维护IIS上的定时Job时也碰到过类似的系统终止线程导致Job停摆的情况,还有大数据量处理的性能问题,咱们一步步来解决:
一、核心问题:Job被系统终止后不再调度
Windows终止线程通常会抛出ThreadAbortException,这种异常比较特殊——默认情况下它会在捕获后自动重新抛出,导致FluentScheduler认为Job执行失败并停止后续调度。你需要做的是全局捕获所有异常,包括ThreadAbortException,并强制让Job保持调度状态:
1. 在Job内部添加顶级异常捕获
修改你的GenerateFiles Job的Execute方法,包裹所有逻辑,重点处理ThreadAbortException:
public class GenerateFiles : IJob { public void Execute() { try { // 你的业务逻辑:查询数据库、生成文件等 ProcessData(); } catch (ThreadAbortException ex) { // 记录终止日志 Log.Error($"Job被系统终止: {ex.Message}", ex); // 重置线程终止请求,避免异常重新抛出导致Job停摆 Thread.ResetAbort(); } catch (Exception ex) { // 处理其他异常,记录日志 Log.Error($"Job执行异常: {ex.Message}", ex); } } private void ProcessData() { // 原来的业务逻辑放在这里 using (var context = new YourDbContext()) { // 后面会优化这里的查询逻辑 } } }
2. 配置FluentScheduler全局异常处理
额外在调度初始化时添加全局异常事件,确保任何Job的异常都被捕获,不会导致整个调度器停摆:
// 在应用启动时(比如Startup.cs/Global.asax) JobManager.JobException += (sender, args) => { Log.Error($"调度Job异常: {args.Exception.Message}", args.Exception); // 如果是ThreadAbortException,同样重置终止请求 if (args.Exception is ThreadAbortException) { Thread.ResetAbort(); } }; // 然后再注册你的Job JobManager.AddJob(() => new GenerateFiles(), s => s .NonReentrant() .ToRunOnceAt(DateTime.Now.AddMinutes(10)) .AndEvery(30).Seconds());
二、解决大数据量遍历导致的崩溃/长时间运行问题
你现在的foreach是一次性加载context.GenerateData.Include(f => f.Datum)所有数据,哪怕每次返回100条,DbContext依然会持有所有已加载实体的引用,内存占用会持续升高,最终导致系统终止线程。改成分页批量处理:
1. 分页查询替代全量遍历
每次只加载固定数量的条目,处理完后释放上下文,再加载下一页:
private void ProcessData() { int pageSize = 100; int pageIndex = 0; bool hasMoreData = true; while (hasMoreData) { using (var context = new YourDbContext()) { // 分页查询未生成的数据,Include关联表 var dataToProcess = context.GenerateData .Include(f => f.Datum) .Where(f => !f.Datum.Generated) .OrderBy(f => f.Id) // 必须排序,否则分页结果不稳定 .Skip(pageIndex * pageSize) .Take(pageSize) .ToList(); if (!dataToProcess.Any()) { hasMoreData = false; break; } // 处理当前页的数据 var output = new List<Datum>(); foreach (var datumToGenerate in dataToProcess) { var datum = datumToGenerate.Datum; if (!datum.Generated) { output.Add(datum); } } // 生成文件逻辑... // 处理完成后,批量标记为已生成或删除条目 MarkDataAsGeneratedOrDelete(output); pageIndex++; } // 上下文在这里自动Dispose,释放资源 } } private void MarkDataAsGeneratedOrDelete(List<Datum> processedData) { using (var context = new YourDbContext()) { var ids = processedData.Select(d => d.Id).ToList(); // 批量更新或删除,比逐条操作高效 var dataToUpdate = context.Datum.Where(d => ids.Contains(d.Id)); foreach (var d in dataToUpdate) { d.Generated = true; } // 或者删除:context.GenerateData.Where(f => ids.Contains(f.DatumId)).ToList().ForEach(f => context.GenerateData.Remove(f)); context.SaveChanges(); } }
2. 额外优化点
- 缩短单个Job执行时间:如果30秒一次的调度,每次执行时间不要接近30秒,避免NonReentrant导致积压。可以调整调度间隔,比如改成1分钟一次,或者优化处理逻辑。
- 监控内存和CPU:用Windows性能监视器跟踪应用的内存使用,确认是否有内存泄漏。
- 关闭EF的跟踪:如果只是查询数据,不需要修改,可以用
.AsNoTracking()减少内存占用:var dataToProcess = context.GenerateData .Include(f => f.Datum) .Where(f => !f.Datum.Generated) .OrderBy(f => f.Id) .AsNoTracking() // 关闭跟踪,减少内存消耗 .Skip(pageIndex * pageSize) .Take(pageSize) .ToList();
三、优化Job启动时机:替代固定10分钟延迟
固定延迟不可靠,最好等缓存初始化完成后再启动Job:
1. 缓存初始化完成后启动调度
假设你的缓存初始化是异步方法,在应用启动时等待缓存加载完成:
// 在Startup.cs的ConfigureServices或Configure方法中 public async void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 等待缓存初始化完成 await CacheHelper.InitializeDatabaseCacheAsync(); // 缓存加载完成后,再启动Job调度 JobManager.JobException += (sender, args) => { // 全局异常处理逻辑 }; JobManager.AddJob(() => new GenerateFiles(), s => s .NonReentrant() .ToRunNow() // 现在立即运行,不需要延迟 .AndEvery(30).Seconds()); }
如果缓存初始化是同步方法,直接在启动时调用后再注册Job即可。
总结
- 用
Thread.ResetAbort()处理系统终止线程的异常,确保Job不会停摆 - 分页批量处理数据,减少内存占用和执行时间
- 等缓存初始化完成后再启动Job,替代固定延迟
内容的提问来源于stack exchange,提问作者Kim Souza
相关产品推荐
相关产品推荐

