问题背景
在高并发场景下,每次日志都直接写入数据库会导致:
- 大量小事务,数据库压力大
- 日志写入阻塞业务逻辑
- 网络 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 倍,同时保证了日志的可靠性(失败重试)和完整性(优雅关闭)。