You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 6中HangFire每日2点定时任务未运行排查

ASP.NET Core 6 Hangfire 周期性任务未执行问题排查与修复

问题场景

  • 技术栈:ASP.NET Core 6 + Hangfire 实现每日凌晨2点同步第三方分支机构数据的周期性任务
  • 预期行为:每日2点调用第三方JSON API,拉取分支机构数据完成新增/更新写入数据库
  • 实际故障:部署后3天定时任务从未运行,数据库无任何同步记录

现有实现参考

第三方API与基础配置

第三方API地址:

https://api.thirdpartycompany.com:2233/api/BranchDetail

appsettings.json 节点配置:

"Endpoints": {
  "branchUrl": "https://api.thirdpartycompany.com:2233/api/BranchDetail"
}

API返回结构示例:

{
    "Branches": [
        {
            "BranchName": "Accra",
            "BranchNumber": 1,
            "Country": "Ghana"
        },
        {
            "BranchName": "Kumasi",
            "BranchNumber": 2,
            "Country": "Ghana"
        }
    ]
}

核心代码定义

实体与DTO

数据库实体类:

public class Branch
{
    public int Id { get; set; }
    public string BranchName { get; set; }
    public int BranchNumber { get; set; }
}

传输对象定义:

public class BranchCreateUpdateDto
{
    public string BranchName { get; set; }
    public int BranchNumber { get; set; }
}

public class BranchResponse
{
    public List<BranchCreateUpdateDto> Branches { get; set; }
}

统一响应类:

public class BaseResponse
{
    public bool Success { get; set; } = true;
    public string Message { get; set; }
}

AutoMapper配置

public class AdminMapperProfile: Profile
{
    public AdminMapperProfile()
    {
        CreateMap<BranchCreateUpdateDto, Branch>().ReverseMap();
    }
}

仓储层实现

泛型仓储基类:

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    private readonly ApplicationDbContext _dbContext;
    private readonly DbSet<T> _dbSet;

    public GenericRepository(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = _dbContext.Set<T>();
    }
    public async Task InsertAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
    }
    public T GetById(string id)
    {
        return _dbSet.Find(id);
    }
    public void Update(T entity)
    {
        _dbSet.Set<T>().Update(entity);
    }
}

分支机构仓储接口与实现:

public interface IAdminBranchRepository : IGenericRepository<Branch>
{

}

public class AdminBranchRepository : GenericRepository<Branch>, IAdminBranchRepository
{
    private readonly ApplicationDbContext _dbContext;
    private readonly DbSet<Branch> _branches;

    public AdminBranchRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
        _dbContext = dbContext;
        _branches = _dbContext.Set<Branch>();
    }
}

工作单元接口与实现:

public interface IUnitOfWork : IDisposable
{
    IAdminBranchRepository Branches { get; }
    Task Save();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _dbContext;
    private IAdminBranchRepository _branches;

    public UnitOfWork(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    public IAdminBranchRepository Branches => _branches ??= new AdminBranchRepository(_dbContext);
    public async Task Save()
    {
        await _dbContext.SaveChangesAsync();
    }
}

服务层实现

服务接口方法:

Task<BaseResponse> CreateBranchAsync();

服务实现类:

public class AdminBranchService : IAdminBranchService
{
    private readonly ApplicationDbContext _dbContext;
    private readonly IMapper _mapper;
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger _logger;
    private readonly IConfiguration _config;
    private readonly HttpClient _myClient;
    public AdminBranchService(
        ApplicationDbContext dbContext,
        IUnitOfWork unitOfWork,
        ILogger logger,
        IMapper mapper,
        IConfiguration config,
        HttpClient myClient
        )
    {
        _dbContext = dbContext;
        _mapper = mapper;
        _unitOfWork = unitOfWork;
        _logger = logger;
        _config = config;
        _myClient = myClient;
    }
    public async Task<BaseResponse> CreateBranchAsync()
    {
        var branchResponse = new BaseResponse();
        var branches = new List<Branch>();
        try
        {
            string branchUrl = _config.GetSection("Endpoints").GetValue<string>("branchUrl");
            _myClient.DefaultRequestHeaders.Accept.Clear();
            _myClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            HttpResponseMessage response = _myClient.GetAsync(branchUrl).Result;

            var stringResult = response.Content.ReadAsStringAsync().Result;
            BranchResponse list = JsonConvert.DeserializeObject<BranchResponse>(stringResult);
            foreach (var singleBranch in list.Branches)
            {
                Branch res = new Branch();
                if (_dbContext.Branches.Any(x => x.BranchName == singleBranch.BranchName))
                {
                    res.BranchNumber = singleBranch.BranchNumber;
                    _unitOfWork.Branches.Update(res);
                }
                else
                {
                    res.BranchName = singleBranch.BranchName;
                    res.BranchNumber = singleBranch.BranchNumber;
                    await _unitOfWork.Branches.InsertAsync(res);
                }
                await _unitOfWork.Save();
            }              
            _logger.Information("Branches Added Successfully");
        }
        catch (Exception ex)
        {
            _logger.Error("An Error occured " + ex.ToString());
        }
        return branchResponse;
    }
}

基础配置代码

数据库连接扩展:

public static class ConnectionConfiguration
{
    public static void AddDbContextAndConfigurations(this IServiceCollection services, IWebHostEnvironment env, IConfiguration config)
    {
        services.AddDbContextPool<ApplicationDbContext>(options =>
        {
            string connStr;
            connStr = config.GetConnectionString("DefaultConnection");
            options.UseSqlServer(connStr);
        });
    }
}

Hangfire配置扩展:

public static class HangFireServiceExtension
{
    public static void AddHangFireConfigurations(this IServiceCollection services, IConfiguration config)
    {
        string connStr;
        connStr = config.GetConnectionString("DefaultConnection");
        services.AddHangfire(configuration => configuration
                .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseRecommendedSerializerSettings()
                .UseSqlServerStorage(connStr, new SqlServerStorageOptions
                {
                    CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                    SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                    QueuePollInterval = TimeSpan.Zero,
                    UseRecommendedIsolationLevel = true,
                    DisableGlobalLocks = true
                }));
        services.AddHangfireServer();
    }
}

Program.cs 配置片段:

builder.Services.ConfigureAutoMappers();
// Db Injection
builder.Services.AddDbContextAndConfigurations(environment, configuration);
// HangFire
builder.Services.AddHangFireConfigurations(configuration);

app.UseHangfireDashboard();

RecurringJob.AddOrUpdate<IAdminBranchService>("Post_All_Branches", service => service.CreateBranchAsync(),
            Cron.Daily(2, 0), TimeZoneInfo.Local);

实现疏漏点

  • 依赖注入缺失:代码中没有将IAdminBranchService、AdminBranchService、IUnitOfWork、UnitOfWork注册到DI容器,服务中注入的ILogger未指定泛型类型,直接注入原生HttpClient也未做注册,Hangfire无法解析IAdminBranchService实例,任务注册后根本无法触发执行。
  • 异步代码同步阻塞:服务方法中使用.Result同步等待HttpClient异步请求结果,在ASP.NET Core同步上下文中极易造成死锁,即使任务触发也会卡死无法执行完成。
  • 更新逻辑完全错误:判断分支已存在时,直接new了一个空的Branch对象,没有查询出数据库中现有实体的主键ID,直接调用Update会要么抛主键为空异常,要么插入新数据而非更新存量数据。
  • 任务注册位置错误:RecurringJob.AddOrUpdate写在中间件配置段,此时DI容器还未完全构建完成,应用启动时可能因为依赖未就绪导致任务注册失败,异常被启动流程掩盖后不会有任何报错提示。
  • 时区配置风险:使用TimeZoneInfo.Local在部署到非本地时区的服务器(比如云服务器默认UTC时区)时,实际执行时间会和预期的凌晨2点出现偏差,甚至因为时区ID不存在抛异常。
  • 异常处理吞错:所有异常只打日志不向外抛出,Hangfire无法感知任务执行失败,不会触发重试,也不会在仪表盘标记失败状态,排查时看不到任何错误痕迹。
  • 循环提交数据库性能差:每一条分支数据就调用一次SaveChanges,数据量大时执行极慢,长事务容易导致锁表,任务超时被Hangfire判定为失败。

修复方案

  1. 补全依赖注入配置
    在Program.cs中添加服务注册,使用IHttpClientFactory注册HttpClient避免套接字耗尽,同时修正日志注入类型:

    // 注册工作单元
    builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
    // 注册分支服务
    builder.Services.AddScoped<IAdminBranchService, AdminBranchService>();
    // 注册HttpClient
    builder.Services.AddHttpClient();
    

    服务构造函数中把ILogger改为ILogger<AdminBranchService>,确保日志组件能正常解析。

  2. 修正异步代码写法,移除同步阻塞
    把服务中所有.Result替换为await异步等待:

    HttpResponseMessage response = await _myClient.GetAsync(branchUrl);
    var stringResult = await response.Content.ReadAsStringAsync();
    
  3. 重写分支新增/更新逻辑
    一次性查询所有现有分支,避免循环内每次查库,更新操作必须基于已查询到的存量实体:

    BranchResponse list = JsonConvert.DeserializeObject<BranchResponse>(stringResult);
    var existingBranches = _dbContext.Branches.ToList();
    foreach (var singleBranch in list.Branches)
    {
        var existingBranch = existingBranches.FirstOrDefault(x => x.BranchName == singleBranch.BranchName);
        if (existingBranch != null)
        {
            existingBranch.BranchNumber = singleBranch.BranchNumber;
            _unitOfWork.Branches.Update(existingBranch);
        }
        else
        {
            var newBranch = new Branch
            {
                BranchName = singleBranch.BranchName,
                BranchNumber = singleBranch.BranchNumber
            };
            await _unitOfWork.Branches.InsertAsync(newBranch);
        }
    }
    // 循环结束后一次性提交,减少数据库交互
    await _unitOfWork.Save();
    
  4. 修正任务注册位置与时区配置
    把RecurringJob注册放到DI容器构建完成、应用启动的阶段,使用明确的时区配置:

    var app = builder.Build();
    // 原有中间件配置(UseRouting、UseAuthorization、UseHangfireDashboard等)保留
    // 容器构建完成后注册定时任务
    using var scope = app.Services.CreateScope();
    RecurringJob.AddOrUpdate<IAdminBranchService>(
        "Post_All_Branches", 
        service => service.CreateBranchAsync(),
        Cron.Daily(2, 0), 
        TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")
    );
    
    app.Run();
    

    如果部署在Linux服务器,时区ID替换为Asia/Shanghai即可

  5. 修正异常处理逻辑
    记录日志后重新抛出异常,让Hangfire可以感知任务失败,在仪表盘展示错误详情:

    catch (Exception ex)
    {
        _logger.Error("分支同步任务执行失败:" + ex.ToString());
        throw;
    }
    

内容的提问来源于stack exchange,提问作者Ayobamilaye

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 23:40:37