资讯动态

.NET 8 + HangFire 库存同步定时任务落地实战

发布时间:2026/9/20 15:06:41 来源:尧图企业网站定制
简介一套基于.NET 8与HangFire的电商多平台库存同步Demo面向需要处理京东、天猫、抖音等O2O/B2C场景库存实时同步的中高级后端开发者。整体采用HangFire调度后台任务、Redis缓存中间状态、SqlSugar简化数据访问项目结构清晰合理拆分SKU处理、库存服务、任务调度及单元测试等模块便于定位与扩展。压缩包共2000个文件以638个C#源码为核心辅以571个动态库、111个XML注释/配置以及前端样式、JS脚本与工程配置资源包139.71MB适合直接研究或二次开发。已有165人学习下载对于搭建库存同步任务队列、设计幂等更新与缓存降级策略、理解HangFire持久化存储与仪表盘接入都有实际借鉴价值。 我有一个现成的库存同步场景正好可以用它来带大家走一遍 .NET 8 HangFire 的完整落地流程。后台系统的库存同步算是最典型的定时任务场景了——电商平台、ERP、WMS 各跑各的库存数据总有对不齐的时候。传统做法要么写个 Windows 服务轮询要么塞个控制台程序挂计划任务麻烦且不好监控。HangFire 在 .NET 生态里几乎是这类需求的标准答案自带可视化面板、失败重试、任务持久化用起来非常顺手。这位朋友想用 .NET 8 搞一个库存同步 Demo我直接把这个 Demo 从零搭完项目结构、关键代码、遇到的问题、能直接抄的配置全都摊开讲适合正在做系统集成、或者刚接触 HangFire 想快速上手的开发者参考。1. 项目需求与方案选型1.1 库存同步的业务背景库存同步这件事做过电商或供应链系统的朋友都不陌生线上商城一个库存ERP 一个库存仓库 WMS 又是一个库存各系统之间数据不互通结果就是前台显示有货、下单后仓库说没货或者仓库明明堆满了、线上却显示缺货。这种问题一旦发生轻则用户投诉重则引发超卖、错发全是实打实的业务损失。常见的同步方案有几种接口实时同步、消息队列异步通知、定时任务批量拉取。前两种对系统改造要求高而且很多老系统根本不具备实时推送的能力。定时任务批量同步是最务实的选择——逻辑独立、侵入性小、出错可以重跑。但定时任务要做得可靠并不简单尤其是任务挂了要有日志、能重试、能可视化排查这时候 HangFire 这种后台任务框架就派上用场了。1.2 为什么选 HangFire 而不是其他方案老项目里最常见的定时任务方案有 BackgroundService、Quartz.NET、HangFire 三种我简单说下自己的使用感受BackgroundService 是微软官方的后台托管服务写法简单但任务调度、持久化、重试都要自己实现一台机器上跑多个实例还会出现重复执行适合很简单的场景。Quartz.NET 非常成熟功能也全但配置偏重CRON 表达式、JobDetail、Trigger 一堆概念而且没有开箱即用的监控面板。HangFire 把这些痛点基本都解决了基于存储SQL Server、Redis、SQLite 等持久化作业写作业就是一个方法调用自带 Web Dashboard可以实时看任务成功失败情况支持自动重试和延时执行多个实例部署时基于分布式锁避免任务重复执行。这是官方 Dashboard 的截图左侧能看到“处理中”“已成功”“已失败”几个板块。我在真实项目里排查问题时这个面板帮了大忙——任务什么时间跑的、跑了多久、报了什么错点进去一目了然不用再翻日志文件。![HangFire Dashboard概览](说明请在本地部署后访问 /hangfire 查看实际面板这里截图位置自行补充)选型的时候我还对比了下两者的 CRON 支持。Quartz 的 CRON 是 7 段格式秒分时日月周年HangFire 是 6 段格式分时日月周年虽然标准不同但写起来 HangFire 更接近常规习惯。例如下面这个定时器RecurringJob.AddOrUpdateInventorySyncJob( inventory-sync-job, job job.ExecuteAsync(), */5 * * * *); // 每5分钟执行一次对库存同步这种业务来说5 分钟一次的频率已经足够不用像支付回调那样做到秒级。如果是对账任务可以放到凌晨跑CRON 表达式改成“0 2 * * *”即可。1.3 Demo 的整体流程与数据模型这个 Demo 模拟的场景是这样的有一个“外部 ERP 库存接口”我直接在 Demo 里伪造的返回各商品的库存数本地数据库有一张商品库存表HangFire 定时任务周期性调用外部接口把获取到的库存数更新到本地库存表并记录每次同步的日志。先来看数据模型表名字段说明ProductsId, Name, StockQuantity本地商品及库存表SyncLogsId, SyncTime, SyncCount, Status, Message每次同步操作运行记录后面的实操部分我会把建表 SQL、模拟外部接口、同步作业代码都写出来。2. HangFire 核心细节解析与配置要点2.1 三种作业类型先搞懂再动手HangFire 提供三种作业类型库存同步主要用第三种但前面两种也得了解因为它们经常会一起出现Fire-and-forget 即发即忘作业调用后立即执行用BackgroundJob.Enqueue()触发。适合发邮件、发短信等异步通知场景。Delayed 延迟作业指定多长时间后执行用BackgroundJob.Schedule()触发。适合超时关单等场景。Recurring 定时作业按 CRON 表达式反复执行用RecurringJob.AddOrUpdate()触发。库存同步、对账、数据统计这类场景全都属于这一类。三种作业可以混用比如一个 Recurring 作业发现库存异常又 Enqueue 一个即时作业去发告警通知这在做库存预警时非常实用。2.2 持久化存储选型内存、SQL Server 还是 RedisHangFire 必须有一个持久化存储来记录作业状态。Demo 阶段最快的是用内存存储——不需要装任何额外组件跑起来就能调试。但我建议你直接体验一下 SQL Server或 SQLite持久化因为一旦换成内存存储重启进程后所有历史记录全部消失排查问题时没有痕迹可用跟生产环境差别太大。存储方式安装成本执行历史生产适用性内存MemoryStorage最低重启丢失仅调试SQLite / 本地文件低保留小规模可接受SQL Server中保留主流选择Redis中保留高并发场景推荐我在生产环境用过 SQL Server 和 Redis 两种。IT 团队如果已经有用得很熟的 SQL Server 实例直接用最省事因为 Dashboard 的历史记录、任务统计也会落到库里DBA 顺手就能查。Redis 的好处是不占用数据库资源但要注意 Redis 连接稳定性一旦 Redis 挂了HangFire 的调度也就跟着停了。2.3 关键配置项与代码结构在 Program.cs 里配置 HangFire 服务。这里我以 SQLite 为例因为零依赖最适合拿来跑 Demobuilder.Services.AddHangfire(config { config.UseSimpleAssemblyNameTypeSerializer(); config.UseRecommendedSerializerSettings(); // 使用 SQLite 存储数据库文件会自动创建 config.UseSqliteStorage(Data Sourcehangfire.db); }); builder.Services.AddHangfireServer();然后是启用 Dashboard 和注册定时任务app.UseHangfireDashboard(/hangfire, new DashboardOptions { // Demo 阶段先放开权限生产环境必须改 Authorization new[] { new AllowAnonymousFilter() } }); // 应用启动时注册定时作业 RecurringJob.AddOrUpdateInventorySyncJob( inventory-sync-job, job job.ExecuteAsync(), */5 * * * *);这几点要注意AddHangfireServer()必须调用否则作业不会真正执行。Dashboard 路由可以随便改生产环境建议藏在反代后面并做好身份认证。RecurringJob 的注册不要放在轮询或请求里反复执行否则会造成重复注册。官方推荐放在启动时注册它内部会做幂等处理。3. 实操过程从创建项目到完成库存同步3.1 创建项目并安装依赖我用的是 .NET 8 SDK命令行直接创建 Web API 项目dotnet new webapi -n HangFireInventoryDemo cd HangFireInventoryDemo然后安装 HangFire 及 SQLite 存储包dotnet add package HangFire.AspNetCore dotnet add package HangFire.SqlServer dotnet add package HangFire.Storage.SQLite如果你用的是 SQL Server那安装 HangFire.SqlServer 就够了我这里用 SQLite 方便大家直接跑。3.2 建表与模拟数据创建本地库存表。真实项目里表结构要复杂得多这里只保留最核心的商品与库存字段方便看效果CREATE TABLE IF NOT EXISTS Products ( Id INTEGER PRIMARY KEY AUTOINCREMENT, SkuCode TEXT NOT NULL UNIQUE, Name TEXT NOT NULL, StockQuantity INTEGER NOT NULL DEFAULT 0, UpdatedAt DATETIME NOT NULL ); CREATE TABLE IF NOT EXISTS SyncLogs ( Id INTEGER PRIMARY KEY AUTOINCREMENT, SyncTime DATETIME NOT NULL, SyncCount INTEGER NOT NULL DEFAULT 0, Status INTEGER NOT NULL, -- 0失败 1成功 Message TEXT NULL );模拟数据方面我直接写了一组商品记录并伪造一个“外部 ERP 库存接口”。真实项目中这里一般是一个 HTTP 调用比如 GET /api/erp/stock返回 JSON。Demo 里为方便把它做成了本地服务接口public interface IExternalErpStockService { TaskListExternalStockDto GetStockAsync(); } public class FakeErpStockService : IExternalErpStockService { // 模拟从 ERP 拉取库存 public TaskListExternalStockDto GetStockAsync() { var stockList new ListExternalStockDto { new() { SkuCode SKU001, StockQuantity 158 }, new() { SkuCode SKU002, StockQuantity 64 }, new() { SkuCode SKU003, StockQuantity 320 } }; return Task.FromResult(stockList); } }3.3 实现库存同步作业库存同步作业是整个 Demo 的核心。这里我做了几件事调用外部服务拿最新库存、遍历比对数据库里的旧值、有变化才更新、记录同步日志。把这几步拆成独立方法方便日志里定位卡在哪个环节。public class InventorySyncJob { private readonly IExternalErpStockService _erpService; private readonly ApplicationDbContext _dbContext; public InventorySyncJob(IExternalErpStockService erpService, ApplicationDbContext dbContext) { _erpService erpService; _dbContext dbContext; } public async Task ExecuteAsync() { try { var externalStockList await _erpService.GetStockAsync(); int updatedCount 0; foreach (var stock in externalStockList) { var product _dbContext.Products .FirstOrDefault(p p.SkuCode stock.SkuCode); if (product ! null product.StockQuantity ! stock.StockQuantity) { product.StockQuantity stock.StockQuantity; product.UpdatedAt DateTime.Now; updatedCount; } } await _dbContext.SaveChangesAsync(); _dbContext.SyncLogs.Add(new SyncLog { SyncTime DateTime.Now, SyncCount updatedCount, Status 1, Message $同步完成更新 {updatedCount} 条数据 }); await _dbContext.SaveChangesAsync(); Console.WriteLine($[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 库存同步成功); } catch (Exception ex) { _dbContext.SyncLogs.Add(new SyncLog { SyncTime DateTime.Now, SyncCount 0, Status 0, Message ex.Message }); await _dbContext.SaveChangesAsync(); // 抛给 HangFire触发自动重试机制 throw; } } }3.4 注册服务与定时任务Program.cs 里注册服务。需要注意HangFire 的 AddHangFire 配置和 AddHangFireServer 是两回事——前者是配存储和序列化方式后者是启动后台 Worker 进程去捞作业执行。少了后者作业注册了也不会跑。builder.Services.AddDbContextApplicationDbContext(options options.UseSqlite(Data Sourceinventory.db)); builder.Services.AddScopedIExternalErpStockService, FakeErpStockService(); builder.Services.AddScopedInventorySyncJob(); builder.Services.AddHangfire(config { config.UseSimpleAssemblyNameTypeSerializer(); config.UseRecommendedSerializerSettings(); config.UseSqliteStorage(Data Sourcehangfire.db); }); builder.Services.AddHangfireServer();启动时注册 Recurring JobRecurringJob.AddOrUpdateInventorySyncJob( inventory-sync-job, job job.ExecuteAsync(), */5 * * * *); // 每5分钟执行一次然后在中间件管道中启用 Dashboardapp.UseHangfireDashboard(/hangfire, new DashboardOptions { Authorization new[] { new AllowAnonymousFilter() } });到这里一个可运行的库存同步 Demo 就算搭完了。dotnet run启动应用后访问/hangfire等上 5 分钟就能看到任务被执行Products 表里数据发生变化SyncLogs 表也会留下同步记录。3.5 手动触发与调试技巧开发调试的时候等 5 分钟太难受了。HangFire 的 Dashboard 上有一个触发按钮可以直接手动执行一次 Recurring Job。另外两个调试技巧调试时可以让 Recurring Job 的 CRON 表达式改成* * * * *每分钟执行一次方便观察状态变化调试完改回真实频率。如果不想等 CRON也可以注册完成后直接调一次BackgroundJob.EnqueueInventorySyncJob(job job.ExecuteAsync())确保刚启动就执行一次验证代码有没有问题。4. 常见问题与排查技巧实录4.1 作业没有按时执行的排查思路新人最常问的问题就是“我明明注册了 Recurring Job为什么它不跑”按下面顺序排查基本都能找到原因现象可能原因解决办法Dashboard 能看到作业但不执行未调用AddHangfireServer()Program.cs 里补上这行执行时间总是不对CRON 表达式用的是服务器本地时间确认服务器时区或者用 UTC 时间设置调度作业执行一次后不再执行CRON 表达式写错比如把 5 段和 6 段格式搞混用 crontab.guru 这类工具校验表达式多实例部署时同一个任务重复跑使用了内存存储没有分布式锁切换到 SQL Server 或 Redis 存储日常排查时请把 Dashboard 的“Recurring Jobs”和“Scheduled Jobs”两个页面打开对照着看Recurring 页面能看到下次执行时间Scheduled 页面能看到排队中的任务两边一对比问题多半就暴露出来了。4.2 依赖注入导致的任务执行异常HangFire 在执行作业时会通过依赖注入容器去解析作业类的实例。如果忘了注册作业依赖的服务就会出现“Cannot resolve service”之类的错误但这类错误不会直接阻断调度只会在执行时把任务标记为失败。我在 Demo 中把InventorySyncJob注册为Scoped构造函数里的ApplicationDbContext也是 Scoped两者生命周期一致不会出问题。但如果你把作业类注册成 Singleton里面注入 Scoped 的 DbContext那就会抛“Cannot consume scoped service from singleton”的异常。正确的做法是作业类注册为 Scoped短生命周期依赖也注册为 Scoped实在要 Singleton 就用 IServiceScopeFactory 创建临时作用域。4.3 HangFire 自动重试机制与防止重复问题HangFire 默认会给失败的作业自动重试 10 次且重试间隔呈指数递增。对库存同步这种读多写少的任务重试机制很实用因为常见的失败比如 ERP 接口抖动、数据库连接超时往往重试一次就好了。但这也会造成另一个问题如果你的同步逻辑没有做幂等一次失败重试可能导致部分数据重复更新。因此在ExecuteAsync里建议先查出外部 SKU 集合再与本地数据做对比有变化才更新而不是无脑先删后插。上面 Demo 的写法就是按这个思路来的天然幂等。另外如果某个任务确实不适合重试比如参数错误的作业可以在方法上标记[AutomaticRetry(Attempts 0)]精确控制重试策略。4.4 Dashboard 页面的访问权限Demo 里为了省事直接把 Dashboard 的 Authorization 配成了匿名访问但这在生产环境是绝对不能接受的——因为 Dashboard 可以随时触发任务、看到任务内容相当于给入侵者开了一扇门。生产环境常见的做法有两种// 方案一通过 AuthorizeFilter 限制本地或内网访问 app.UseHangfireDashboard(/hangfire, new DashboardOptions { Authorization new[] { new LocalRequestsOnlyAuthorizationFilter() } }); // 方案二自己写一个 IAuthorizationFilter接入公司的统一登录 public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) { var httpContext context.GetHttpContext(); return httpContext.User.Identity?.IsAuthenticated true; } }在真实项目中我一般还会把UseHangfireDashboard放在反向代理之后并且只允许内网 IP 访问这样双保险。5. 生产环境落地时不能忽略的细节遇到比较紧急的情况时HangFire 的即发即忘作业也能派上用场。如果接口调用方刚好在更新某个商品的库存你可以让它在业务处理成功后调BackgroundJob.Enqueue把单条库存变更扔到后台去跑只处理 SKU001 这一条不会像 Recurring Job 那样全量扫描所有商品。这会成为一个非常有用的“增量同步”通道。不过真正上线之前还有两个地方要留意一是数据量大时全量同步会拉取大量数据建议加上“增量同步 断点续传”的逻辑比如记录上一次同步成功的时间点只拉这个时间点之后变更的数据二是如果同一时刻有多个任务同时跑SQLite 的并发写性能不够要考虑文件锁问题生产环境直接用 SQL Server 或 Redis 存储更稳妥。最后再说一句我自己的经验库存同步这个功能逻辑本身不难难的是“同步完如何确认两边一致、出问题如何快速定位”。HangFire 的价值不只是把任务跑起来而是给你一套可视化的追踪体系——任务什么时候执行的、执行了多久、成功还是失败、失败信息是什么全都在 Dashboard 里一目了然。这也是我这两年越来越喜欢它的原因。项目搭好之后别急着关掉 Dashboard 页面跑几天看看实际执行情况你会发现这些数据比任何日志都直观。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价