.NET 日志系统设计:缓冲 + 批量写入

2026年01月06日5 min read
dotnet
logging
performance
buffer

问题背景

在高并发场景下,每次日志都直接写入数据库会导致:

  1. 大量小事务,数据库压力大
  2. 日志写入阻塞业务逻辑
  3. 网络 I/O 成为瓶颈

设计方案

采用 缓冲区 + 批量写入 + 定时刷新 三重机制。

核心实现

缓冲区定义

public static class LogHelper {
    // 日志缓冲区
    private static readonly List<ApplicationLog> s_logBuffer = [];
    private static readonly object s_bufferLock = new();
    
    // 批量写入阈值
    private const int BatchSize = 5;
    
    // 定时刷新
    private static CancellationTokenSource? s_flushTimerCts;
    private static Task? s_flushTimerTask;
}

写入日志

日志先进入缓冲区,达到阈值时触发批量写入:

public static async Task LogToDbAsync(
    LogLevel level,
    string message,
    string? category = null,
    Exception? exception = null,
    object? contextData = null,
    [CallerMemberName] string? memberName = null,
    [CallerFilePath] string? sourceFilePath = null) {

    if (!ShouldLog(level)) return;

    // 从文件路径提取类名
    string? source = null;
    if (!string.IsNullOrEmpty(sourceFilePath)) {
        var fileName = Path.GetFileNameWithoutExtension(sourceFilePath);
        source = $"{fileName}.{memberName}";
    }

    // 构建日志对象
    var log = new ApplicationLog {
        LogLevel = level.ToString(),
        Category = category,
        Message = message,
        Source = source,
        Exception = exception?.ToString(),
        ContextData = contextData != null 
            ? JsonSerializer.Serialize(contextData) 
            : null,
        CreatedAt = DateTime.Now
    };

    // 添加到缓冲区
    lock (s_bufferLock) {
        s_logBuffer.Add(log);

        // 达到阈值,触发异步批量写入
        if (s_logBuffer.Count >= BatchSize) {
            _ = Task.Run(() => FlushBufferAsync());
        }
    }

    // 同时输出到控制台(方便调试)
    Console.WriteLine($"[{level}][{category}] {message}");
}

批量刷新

private static async Task FlushBufferAsync() {
    List<ApplicationLog> logsToInsert;

    // 取出缓冲区数据
    lock (s_bufferLock) {
        if (s_logBuffer.Count == 0) return;
        
        logsToInsert = new List<ApplicationLog>(s_logBuffer);
        s_logBuffer.Clear();
    }

    try {
        // 批量插入
        if (logsToInsert.Count > 0) {
            await DbHelper.GetDb()
                .Insertable(logsToInsert)
                .ExecuteCommandAsync();
        }
    }
    catch (Exception ex) {
        // 写入失败,放回缓冲区
        lock (s_bufferLock) {
            s_logBuffer.InsertRange(0, logsToInsert);
            Console.WriteLine($"[LogHelper] 批量写入失败,已放回缓冲区: {ex.Message}");
        }
    }
}

定时刷新

防止日志量少时长时间不写入:

public static void Initialize(LogConfig config) {
    s_config = config;
    
    // 启动定时刷新任务(每5秒)
    s_flushTimerCts = new CancellationTokenSource();
    s_flushTimerTask = StartFlushTimerAsync(s_flushTimerCts.Token);
}

private static async Task StartFlushTimerAsync(CancellationToken cancellationToken) {
    while (!cancellationToken.IsCancellationRequested) {
        await Task.Delay(5000, cancellationToken);
        await FlushBufferAsync();
    }
}

优雅关闭

程序退出时确保缓冲区日志全部写入:

public static async Task ShutdownAsync() {
    // 停止定时任务
    s_flushTimerCts?.Cancel();
    if (s_flushTimerTask != null) {
        await s_flushTimerTask;
    }

    // 最后一次刷新
    await FlushBufferAsync();
    
    Console.WriteLine("[LogHelper] 日志助手已关闭");
}

自动提取调用信息

利用 [CallerMemberName][CallerFilePath] 自动获取调用者信息:

// 调用示例
await LogHelper.LogToDbAsync(LogLevel.Info, "用户登录成功", "认证模块");

// 自动生成的 Source: "AuthServiceImpl.LoginAsync"

自动提取用户 ID

从 HttpContext 中自动提取当前登录用户:

private static long? GetUserIdFromContext() {
    var context = s_httpContextAccessor?.HttpContext;
    if (context == null) return null;

    var authHeader = context.Request.Headers["Authorization"].ToString();
    if (!authHeader.StartsWith("Bearer ")) return null;

    var token = authHeader.Replace("Bearer ", "");
    return JwtHelper.GetUserIdFromToken(token);
}

日志级别过滤

private static bool ShouldLog(LogLevel level) {
    if (s_config == null) return false;
    
    var minLevel = Enum.Parse<LogLevel>(s_config.MinimumLogLevel);
    return level >= minLevel;
}

使用示例

// 普通日志
await LogHelper.LogToDbAsync(LogLevel.Info, "算法计算完成", "算法模块");

// 带上下文数据
await LogHelper.LogToDbAsync(
    LogLevel.Warning,
    "CO浓度超过阈值",
    "报警记录",
    contextData: new { value = 150, threshold = 100, unit = "ppm" }
);

// 异常日志
try {
    // ...
} catch (Exception ex) {
    await LogHelper.LogToDbAsync(LogLevel.Error, "数据采集失败", "DMS模块", ex);
}

性能对比

| 方案 | 1000条日志耗时 | 数据库事务数 | |-----|--------------|-------------| | 逐条写入 | 5200ms | 1000 | | 批量写入(5条) | 420ms | 200 | | 批量写入(50条) | 180ms | 20 |

总结

通过缓冲区 + 批量写入 + 定时刷新的组合策略,日志系统性能可以提升 10-30 倍,同时保证了日志的可靠性(失败重试)和完整性(优雅关闭)。