Hangfire从仪表盘手动触发作业时为何出现PK_HangFire_Hash主键约束冲突?
Hangfire手动触发周期性作业时的唯一约束冲突问题
环境信息
- 使用的Hangfire包版本:
Hangfire.AspNetCore: 1.8.0、Hangfire.Core: 1.8.0、Hangfire.SqlServer: 1.8.0
问题现象
从仪表盘手动触发周期性作业IWorkOrderBackgroundService.WatchInProgressWorkOrders时,偶尔会抛出主键约束冲突异常,提示无法向HangFire.Hash和HangFire.Set表插入重复键。
具体错误信息
Microsoft.Data.SqlClient.SqlException (0x80131904): 违反主键约束'PK_HangFire_Hash'。无法在对象'HangFire.Hash'中插入重复键,重复键值为(recurring-job:IWorkOrderBackgroundService.WatchInProgressWorkOrders, LastExecution)。 违反主键约束'PK_HangFire_Hash'。无法在对象'HangFire.Hash'中插入重复键,重复键值为(recurring-job:IWorkOrderBackgroundService.WatchInProgressWorkOrders, NextExecution)。 违反主键约束'PK_HangFire_Hash'。无法在对象'HangFire.Hash'中插入重复键,重复键值为(recurring-job:IWorkOrderBackgroundService.WatchInProgressWorkOrders, LastJobId)。 违反主键约束'PK_HangFire_Set'。无法在对象'HangFire.Set'中插入重复键,重复键值为(recurring-jobs, IWorkOrderBackgroundService.WatchInProgressWorkOrders)。 语句已终止。语句已终止。语句已终止。语句已终止。 在Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction) 在Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction) 在Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose) 在Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady) 在Microsoft.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted) 在Microsoft.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean isAsync, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest) 在Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String method) 在Microsoft.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String methodName) 在Microsoft.Data.SqlClient.SqlCommand.ExecuteNonQuery() 在Microsoft.Data.SqlClient.SqlCommandSet.ExecuteNonQuery() 在Hangfire.SqlServer.SqlCommandBatch.ExecuteNonQuery() 在Hangfire.SqlServer.SqlServerWriteOnlyTransaction.b__17_0(DbConnection connection, DbTransaction transaction) 在Hangfire.SqlServer.SqlServerStorage.<>c__DisplayClass39_0.b__0(DbConnection connection, DbTransaction transaction) 在Hangfire.SqlServer.SqlServerStorage.<>c__DisplayClass40_0`1.<UseTransaction>b__0(DbConnection connection) 在Hangfire.SqlServer.SqlServerStorage.UseConnection[T](DbConnection dedicatedConnection, Func`2 func) 在Hangfire.SqlServer.SqlServerStorage.UseTransaction[T](DbConnection dedicatedConnection, Func`3 func, Nullable`1 isolationLevel) 在Hangfire.SqlServer.SqlServerStorage.UseTransaction(DbConnection dedicatedConnection, Action`2 action) 在Hangfire.SqlServer.SqlServerWriteOnlyTransaction.Commit() 在Hangfire.RecurringJobManager.TriggerJob(String recurringJobId) 在Hangfire.RecurringJobManager.Trigger(String recurringJobId) 在Hangfire.Dashboard.DashboardRoutes.<>c.<.cctor>b__4_27(IRecurringJobManager manager, String jobId) 在Hangfire.Dashboard.RouteCollectionExtensions.<>c__DisplayClass4_0.b__0(DashboardContext context, String jobId) 在Hangfire.Dashboard.BatchCommandDispatcher.Dispatch(DashboardContext context) 在Hangfire.Dashboard.AspNetCoreDashboardMiddleware.Invoke(HttpContext httpContext)
根本原因
手动触发周期性作业时,Hangfire会执行一批SQL操作更新该作业的元数据(包括哈希表中的执行记录、集合中的作业标识)。当同一时间存在多个触发请求(比如用户连续点击触发按钮,或后台调度器刚好在更新该作业元数据),就会引发并发写入冲突:第一个事务提交后,第二个事务尝试插入已存在的主键记录,触发SQL唯一约束异常。
解决方案
- 避免重复触发:提醒用户不要连续点击仪表盘触发按钮,确保同一时间只有一个手动触发请求
- 升级Hangfire版本:升级到1.8.x系列后续补丁版本,部分版本优化了并发场景下的事务处理逻辑,可减少此类冲突
- 自定义触发逻辑:若需频繁手动触发,直接使用
BackgroundJob.Enqueue<IWorkOrderBackgroundService>(x => x.WatchInProgressWorkOrders())添加一次性作业,绕过RecurringJobManager.Trigger对周期性作业元数据的修改操作 - 调整事务隔离级别:在Hangfire SQL存储配置中,尝试调整事务隔离级别(例如改为
ReadCommitted),但需注意此调整可能影响其他作业性能,需在测试环境验证后再上线
内容的提问来源于stack exchange,提问作者LP13
相关产品推荐
相关产品推荐

