在C#应用开发中,日志记录是排查问题、监控系统状态的重要手段,但传统的同步逐条写入文件的方式,在高并发场景下会频繁触发磁盘IO操作,不仅会降低日志写入效率,还可能阻塞业务线程,影响应用的整体性能。采用先写入内存队列再批量写入文件的异步批处理方案,可以有效减少磁盘操作次数,提升日志处理的吞吐量。

核心实现思路
整个方案的核心分为三个部分:内存日志队列、日志生产端、日志消费端。生产端负责将日志信息写入内存队列,消费端异步从队列中取出批量日志,统一写入文件,两者通过线程安全的内存队列解耦,避免直接操作文件带来的性能问题。
内存队列选型
内存队列需要支持多线程并发写入,同时要保证消费端可以安全地批量取出元素,这里选择ConcurrentQueue<string>作为日志队列容器,它是.NET提供的线程安全队列,无需额外加锁即可支持多线程操作。
日志生产端实现
生产端只需要将格式化后的日志字符串放入内存队列即可,操作非常轻量,不会阻塞业务线程。示例代码如下:
using System;
using System.Collections.Concurrent;
using System.Threading;
public class LogProducer
{
// 全局共享的日志内存队列
private static readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>();
// 批处理阈值,队列中日志数量达到该值就触发写入
private static readonly int _batchThreshold = 10;
// 定时器,用于定期触发批处理写入,避免队列中日志长时间不写入
private static readonly Timer _batchTimer;
// 定时器触发间隔,单位毫秒
private static readonly int _timerInterval = 2000;
static LogProducer()
{
// 初始化定时器,回调方法为批处理写入逻辑
_batchTimer = new Timer(FlushLogBatch, null, _timerInterval, _timerInterval);
}
/// <summary>
/// 写入日志到内存队列
/// </summary>
/// <param name="logContent">日志内容</param>
public static void WriteLog(string logContent)
{
// 格式化日志,添加时间戳
string formattedLog = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {logContent}";
// 放入内存队列
_logQueue.Enqueue(formattedLog);
// 如果队列长度达到批处理阈值,主动触发一次批处理
if (_logQueue.Count >= _batchThreshold)
{
FlushLogBatch(null);
}
}
/// <summary>
/// 批量刷新日志到文件
/// </summary>
private static void FlushLogBatch(object state)
{
// 如果队列为空,直接返回
if (_logQueue.IsEmpty)
{
return;
}
// 临时存储待写入的日志集合
var logBatch = new System.Collections.Generic.List<string>();
// 从队列中取出所有日志,最多取100条避免单次写入过多
int takeCount = 0;
while (_logQueue.TryDequeue(out string log) && takeCount < 100)
{
logBatch.Add(log);
takeCount++;
}
// 如果有日志需要写入,调用文件写入方法
if (logBatch.Count > 0)
{
LogFileWriter.WriteLogsToFile(logBatch);
}
}
}
日志消费端实现
消费端负责将批量日志写入文件,这里需要注意文件写入的线程安全,避免多个线程同时操作同一个日志文件导致内容错乱。示例代码如下:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
public class LogFileWriter
{
// 日志文件存放目录
private static readonly string _logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
// 写入文件时的锁对象,保证同一时间只有一个线程写入文件
private static readonly object _fileWriteLock = new object();
static LogFileWriter()
{
// 如果日志目录不存在,自动创建
if (!Directory.Exists(_logDirectory))
{
Directory.CreateDirectory(_logDirectory);
}
}
/// <summary>
/// 批量写入日志到文件
/// </summary>
/// <param name="logs">待写入的日志集合</param>
public static void WriteLogsToFile(List<string> logs)
{
// 按日期生成日志文件名,避免单个文件过大
string logFileName = $"log_{DateTime.Now:yyyyMMdd}.txt";
string logFilePath = Path.Combine(_logDirectory, logFileName);
// 拼接日志内容,每个日志占一行
StringBuilder logContentBuilder = new StringBuilder();
foreach (var log in logs)
{
logContentBuilder.AppendLine(log);
}
// 加锁写入文件,保证线程安全
lock (_fileWriteLock)
{
// 使用追加模式写入,编码为UTF8
File.AppendAllText(logFilePath, logContentBuilder.ToString(), Encoding.UTF8);
}
}
}
使用示例
在业务代码中调用日志写入方法非常简单,不需要关心批处理和文件写入的细节:
class Program
{
static void Main(string[] args)
{
// 模拟高并发场景下的日志写入
for (int i = 0; i < 50; i++)
{
int logIndex = i;
// 启动多个线程模拟并发写入
System.Threading.Tasks.Task.Run(() =>
{
LogProducer.WriteLog($"这是第{logIndex}条测试日志,线程ID:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
});
}
// 等待所有日志处理完成
System.Threading.Thread.Sleep(5000);
Console.WriteLine("日志写入完成");
}
}
注意事项
- 内存队列的大小需要合理控制,如果应用产生日志的速度远快于消费端写入速度,队列会持续膨胀占用内存,可以增加批处理阈值或者优化文件写入性能。
- 定时器触发间隔和批处理阈值需要根据实际业务场景调整,平衡日志实时性和性能。
- 文件写入时需要处理异常,比如磁盘空间不足、文件被占用等情况,避免异常导致应用崩溃,可以在
WriteLogsToFile方法中添加try-catch逻辑。 - 如果应用需要重启,内存队列中未写入的日志会丢失,如果需要保证日志不丢失,可以在应用关闭前主动调用一次
FlushLogBatch方法,将队列中剩余日志全部写入文件。