自定义.NET Core数据库日志提供程序注入DbContext后出现死锁问题
解决.NET Core自定义数据库日志提供程序的死锁问题
问题根源分析
你遇到的请求挂起、代码执行中断问题,核心原因是同步日志写入阻塞了请求线程,进而引发死锁:
Log方法是同步执行的,直接调用SaveChanges()同步写入数据库,会占用ASP.NET Core的请求线程池线程;- 高并发场景下,线程池资源被耗尽,或当前请求的DbContext与日志操作的DbContext产生资源竞争,最终导致死锁,请求无法响应。
- 另外代码里
scope.Dispose()属于冗余操作,using var scope会自动释放作用域。
解决方案
将日志写入逻辑改为异步后台处理:把日志消息先放入队列,由独立的后台服务异步写入数据库,避免阻塞请求线程。
1. 实现后台日志处理服务
创建一个后台服务负责异步消费日志队列:
public class DatabaseLogProcessor : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ConcurrentQueue<ActivityLog> _logQueue = new(); private readonly SemaphoreSlim _signal = new(0); public DatabaseLogProcessor(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } // 供日志记录器调用,将日志加入队列 public void EnqueueLog(ActivityLog log) { _logQueue.Enqueue(log); _signal.Release(); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await _signal.WaitAsync(stoppingToken); // 批量处理队列中的日志 while (_logQueue.TryDequeue(out var log)) { try { using var scope = _serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); context.ActivityLogs.Add(log); await context.SaveChangesAsync(stoppingToken); } catch (Exception ex) { Console.WriteLine($"日志写入失败: {ex.Message}"); } } } } public override void Dispose() { _signal.Dispose(); base.Dispose(); } }
2. 修改DatabaseLogger实现
移除直接的数据库操作,改为将日志加入后台队列:
internal class DatabaseLogger(string category, Func<DatabaseLoggerOptions> getCurrentConfiguration, DatabaseLogProcessor logProcessor) : ILogger { private class ScopeDisposable(Action disposeAction) : IDisposable { private readonly Action _disposeAction = disposeAction; public void Dispose() { _disposeAction.Invoke(); } } private readonly AsyncLocal<DatabaseLoggerParameters?> _scopeData = new(); private readonly DatabaseLogProcessor _logProcessor = logProcessor; public IDisposable? BeginScope<TState>(TState state) where TState : notnull { if (state is DatabaseLoggerParameters scopeData) _scopeData.Value = scopeData; return new ScopeDisposable(() => _scopeData.Value = default); } public bool IsEnabled(LogLevel logLevel) { return (getCurrentConfiguration().CategoryToLogLevel.TryGetValue(category, out LogLevel value) ? value : getCurrentConfiguration().CategoryToLogLevel.GetValueOrDefault("Default")) <= logLevel; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { try { if (!IsEnabled(logLevel)) return; var parameters = _scopeData.Value; string logMessage = $"Category: {category}{Environment.NewLine}"; logMessage += $"Message: {formatter(state, exception)}{Environment.NewLine}"; if (exception != null) { logMessage += $"Exception: {exception.Message}{Environment.NewLine}"; logMessage += $"Inner exception: {exception.InnerException?.Message ?? string.Empty}{Environment.NewLine}"; logMessage += $"StackTrace: {exception.StackTrace}{Environment.NewLine}"; } var logEntry = new ActivityLog { ActivityType = ActivityType.Unknown, UserId = parameters?.UserId ?? 1, Message = logMessage, On = DateTime.UtcNow }; // 加入后台队列,不阻塞当前线程 _logProcessor.EnqueueLog(logEntry); } catch (Exception ex) { Console.WriteLine($"日志入队失败: {ex.Message}"); } } }
3. 调整DatabaseLoggerProvider依赖
注入后台日志处理器,替代原有的IServiceProvider:
[ProviderAlias("Database")] public class DatabaseLoggerProvider : ILoggerProvider { private readonly IDisposable? _onChangeToken; private DatabaseLoggerOptions _currentConfig; private readonly ConcurrentDictionary<string, DatabaseLogger> _loggers = new(StringComparer.OrdinalIgnoreCase); private readonly DatabaseLogProcessor _logProcessor; public DatabaseLoggerProvider(IOptionsMonitor<DatabaseLoggerOptions> config, DatabaseLogProcessor logProcessor) { _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); _currentConfig = config.CurrentValue; _logProcessor = logProcessor; } public ILogger CreateLogger(string categoryName) => _loggers.GetOrAdd(categoryName, name => new(name, GetCurrentConfig, _logProcessor)); private DatabaseLoggerOptions GetCurrentConfig() => _currentConfig; public void Dispose() { _onChangeToken?.Dispose(); GC.SuppressFinalize(this); } }
4. 更新服务注册代码
添加后台服务的注册:
builder.AddConfiguration(); // 注册后台日志处理服务 builder.Services.AddHostedService<DatabaseLogProcessor>(); // 注册自定义日志提供程序 builder.Services.AddSingleton<ILoggerProvider, DatabaseLoggerProvider>(); LoggerProviderOptions.RegisterProviderOptions<DatabaseLoggerOptions, DatabaseLoggerProvider>(builder.Services);
方案优势
- 彻底避免请求线程被数据库操作阻塞,消除死锁风险;
- 后台服务异步处理日志,不占用请求线程池资源,提升系统并发能力;
- 队列机制保证日志不丢失,即使数据库临时不可用,日志也会在后续重试写入。
内容的提问来源于stack exchange,提问作者Vladimir99
相关产品推荐
相关产品推荐

