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

自定义.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 17:05:00