.NET 6站点启动后执行数据库操作的最佳方式(解决503错误)
.NET 6 启动时稳定执行数据库清理操作避免503错误的解决方案
问题描述
我在Program.cs中编写了中间件,期望站点启动时删除数据库中的指定信息。该逻辑在本地运行正常,可成功执行查询并删除数据,但发布至服务器后数分钟会出现503错误;移除该中间件后,503错误不再出现,站点恢复正常运行。更换服务器后问题仍存在。当前实现代码如下:
IHostApplicationLifetime lifetime = app.Lifetime; static async void AddLogs(IApplicationBuilder app){ using (var serviceScope = app.ApplicationServices.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<DBContext>(); DateTime dateNow = DateTime.ParseExact(DateConvertor.ToShamsi(), "yyyy/MM/dd", null); bool ExistRememberNow = context.HistoryReserves.AsEnumerable().Any(h => DateTime.ParseExact(h.RemmberDate.ToString(), "yyyy/MM/dd", null) == dateNow); if (ExistRememberNow) { var ExistHistoryDateNow = context.HistoryReserves.Include(u => u.User).ThenInclude(u => u.UserDetail).AsEnumerable().Where(h => DateTime.ParseExact(h.RemmberDate, "yyyy/MM/dd", null) == dateNow).ToList(); foreach (var user in ExistHistoryDateNow) { string message = "Sen Text Message"; SmsSender.VerifySend(user.User.Mobile, message); } context.HistoryReserves.RemoveRange(ExistHistoryDateNow); context.SaveChanges(); } string timeNow = DateConvertor.GetShamsiTime(); var reserveUsers = context.ReserveTimes .Include(r => r.ReserveDate) .AsEnumerable() .Where(r => DateTime.ParseExact(r.ReserveDate.DateReserve, "yyyy/MM/dd", null) == dateNow && TimeSpan.Parse(r.EndTimeReserve, CultureInfo.InvariantCulture) < TimeSpan.Parse(timeNow, CultureInfo.InvariantCulture)) .FirstOrDefault(); if (reserveUsers != null) { var deleteReserveTime = context.ReserveTimes.Find(reserveUsers.Id); context.ReserveTimes.Remove(deleteReserveTime); context.SaveChanges(); } var pastDate = context.ReserveDates.AsEnumerable().Where(r => DateTime.ParseExact(r.DateReserve, "yyyy/MM/dd", null) < DateTime.Now).ToList(); if (pastDate.Count() != 0) { context.RemoveRange(pastDate); context.SaveChanges(); } lifetime.ApplicationStarted.Register(() => AddLogs(app)); app.UseHttpsRedirection(); app.UseStaticFiles(); }
问题根源分析
- async void 异步风险:
async void在非UI场景下无法捕获未处理异常,一旦出现异常可能直接导致进程崩溃,触发服务器503错误。 - 递归注册启动事件:在
AddLogs内部重复注册ApplicationStarted事件,导致清理逻辑被多次执行,消耗大量服务器资源。 - 中间件管道混乱:在启动任务中调用
UseHttpsRedirection、UseStaticFiles等中间件配置,破坏了中间件管道的正常执行顺序。 - 低效EF查询:
AsEnumerable()将全表数据拉取到内存后过滤,数据量大时会占用大量内存和CPU,导致服务器资源耗尽。 - 多次数据库提交:多次调用
SaveChanges()增加数据库连接开销,加重数据库负载。
解决方案步骤
1. 替换async void为async Task
异步方法返回Task而非void,确保异常可以被捕获和处理,避免进程崩溃。
2. 分离启动清理任务与中间件
将数据库清理逻辑从中间件中剥离,作为独立异步方法,通过IHostApplicationLifetime的ApplicationStarted事件注册,仅执行一次。
3. 优化EF Core查询性能
- 使用EF Core异步扩展方法(如
ToListAsync、FirstOrDefaultAsync)避免阻塞线程。 - 移除不必要的
AsEnumerable(),让过滤逻辑在数据库层面执行,减少内存占用。
4. 合并数据库提交
将所有修改操作合并后一次性调用SaveChangesAsync(),减少数据库交互次数,降低负载。
5. 恢复中间件管道正常配置
中间件配置应放在Program.cs的主管道中,启动任务不应该修改管道结构。
修改后的代码示例
独立的数据库清理方法
using System.Globalization; using Microsoft.EntityFrameworkCore; static async Task CleanupDatabaseAsync(IServiceProvider services) { using var scope = services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<DBContext>(); var dateNow = DateTime.ParseExact(DateConvertor.ToShamsi(), "yyyy/MM/dd", null); bool hasChanges = false; // 处理HistoryReserves:发送短信并删除记录 var historyRecords = await context.HistoryReserves .Include(u => u.User) .ThenInclude(u => u.UserDetail) .Where(h => DateTime.ParseExact(h.RemmberDate.ToString(), "yyyy/MM/dd", null) == dateNow) .ToListAsync(); if (historyRecords.Any()) { foreach (var record in historyRecords) { string message = "发送文本消息"; SmsSender.VerifySend(record.User.Mobile, message); } context.HistoryReserves.RemoveRange(historyRecords); hasChanges = true; } // 处理ReserveTimes:删除过期预约时段 string timeNow = DateConvertor.GetShamsiTime(); var expiredReserve = await context.ReserveTimes .Include(r => r.ReserveDate) .Where(r => DateTime.ParseExact(r.ReserveDate.DateReserve, "yyyy/MM/dd", null) == dateNow && TimeSpan.Parse(r.EndTimeReserve, CultureInfo.InvariantCulture) < TimeSpan.Parse(timeNow, CultureInfo.InvariantCulture)) .FirstOrDefaultAsync(); if (expiredReserve != null) { context.ReserveTimes.Remove(expiredReserve); hasChanges = true; } // 处理ReserveDates:删除过期预约日期 var pastDates = await context.ReserveDates .Where(r => DateTime.ParseExact(r.DateReserve, "yyyy/MM/dd", null) < DateTime.Now) .ToListAsync(); if (pastDates.Any()) { context.RemoveRange(pastDates); hasChanges = true; } // 一次性提交所有更改 if (hasChanges) { await context.SaveChangesAsync(); } }
Program.cs中注册启动任务
var builder = WebApplication.CreateBuilder(args); // 注册数据库上下文等服务 builder.Services.AddDbContext<DBContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); var app = builder.Build(); // 正常配置中间件管道 app.UseHttpsRedirection(); app.UseStaticFiles(); // 其他中间件(如路由、授权等)... // 注册启动时的清理任务 var lifetime = app.Lifetime; lifetime.ApplicationStarted.Register(async () => { try { await CleanupDatabaseAsync(app.Services); } catch (Exception ex) { // 记录异常日志,避免未处理异常导致进程崩溃 Console.WriteLine($"启动清理任务失败: {ex.Message}"); // 可根据需要添加日志框架记录 } }); app.Run();
额外优化建议
- 日期存储优化:如果数据库中日期以字符串存储,建议改为DateTime类型,避免频繁字符串解析开销;若无法修改,可使用EF Core的
EF.Functions或数据库特定函数在数据库层面完成日期比较。 - 短信发送异步化:若
SmsSender.VerifySend是同步方法,建议改为异步版本,避免阻塞清理任务线程。 - 日志记录:在清理任务中添加详细日志,便于排查执行过程中的问题。
内容的提问来源于stack exchange,提问作者RoyalDesigner
相关产品推荐
相关产品推荐

