>1、需求 需求很简单,就是在C#开发中高速写日志。比如在高并发,高流量的地方需要写日志。我们知道程序在操作磁盘时是比较耗时的,所以我们把日志写到磁盘上会有一定的时间耗在上面,这些并不是我们想看到的。
2、解决方案
2.1、简单原理说明
使用列队 先缓存到内存,然后我们一直有个线程 再从列队中写到磁盘上,这样就可以高速高性能的写日志了。
因为速度慢的地方我们分离出来了,也就是说程序在把日志扔给列队后,程序的日志部分就算完成了,后面操作磁盘耗时的部分程序是不需要关心的,由另一个线程操作。
俗话说,鱼和熊掌不可兼得,这样会有一个问题,就是如果日志已经到列队了这个时候程序崩溃或者电脑断电都会导致日志部分丢失,但是有些地方为了高性能的写日志,是否可以忽略一些情况,请各位根据情况而定。
2.2、示例图
3、关键代码部分 这里写日志的部分LZ选用了比较常用的log4net,当然也可以选择其他的日志组件,比如nlog等等。
3.1、日志至列队部分
第一步我们首先需要把日志放到列队中,然后才能从列队中写到磁盘上。
public void EnqueueMessage (string message, FlashLogLevel level, Exception ex = null ) { if ((level == FlashLogLevel.Debug && _log.IsDebugEnabled) || (level == FlashLogLevel.Error && _log.IsErrorEnabled) || (level == FlashLogLevel.Fatal && _log.IsFatalEnabled) || (level == FlashLogLevel.Info && _log.IsInfoEnabled) || (level == FlashLogLevel.Warn && _log.IsWarnEnabled)) { _que.Enqueue(new FlashLogMessage { Message = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff" ) + "]\r\n" + message, Level = level, Exception = ex }); // 通知线程往磁盘中写日志 _mre.Set(); } }
_ log是log4net日志组件的ILog,其中包含了写日志,判断日志等级等功能,代码开始部分的if判断就是判断等级和现在的日志等级做对比,看是否需要写入列队,这样可以有效的提高日志的性能。
其中的_que是ConcurrentQueue列队。_ mre是ManualResetEvent信号,ManualResetEven是用来通知线程列队中有新的日志,可以从列队中写入磁盘了。当从列队中写完日志后,重新设置信号,在等待下次有新的日志到来。
3.2、列队到磁盘
从列队到磁盘我们需要有一个线程从列队写入磁盘,也就是说我们在程序启动时就要加载这个线程,比如asp.net中就要在global 中的Application_Start 中加载。
/// <summary> /// 另一个线程记录日志,只在程序初始化时调用一次/// </summary> public void Register () { Thread t = new Thread(new ThreadStart(WriteLog)); t.IsBackground = false ; t.Start(); }/// <summary> /// 从队列中写日志至磁盘/// </summary> private void WriteLog () { while (true ) { // 等待信号通知 _mre.WaitOne(); FlashLogMessage msg; // 判断是否有内容需要如磁盘 从列队中获取内容,并删除列队中的内容 while (_que.Count > 0 && _que.TryDequeue(out msg)) { // 判断日志等级,然后写日志 switch (msg.Level) { case FlashLogLevel.Debug: _log.Debug(msg.Message, msg.Exception); break ; case FlashLogLevel.Info: _log.Info(msg.Message, msg.Exception); break ; case FlashLogLevel.Error: _log.Error(msg.Message, msg.Exception); break ; case FlashLogLevel.Warn: _log.Warn(msg.Message, msg.Exception); break ; case FlashLogLevel.Fatal: _log.Fatal(msg.Message, msg.Exception); break ; } } // 重新设置信号 _mre.Reset(); Thread.Sleep(1 ); } }
3.3、完整代码 using log4net;using log4net.Config;using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.IO;using System.Linq;using System.Text;using System.Threading;using System.Threading.Tasks;namespace Emrys.FlashLog { public sealed class FlashLogger { /// <summary> /// 记录消息Queue /// </summary> private readonly ConcurrentQueue<FlashLogMessage> _que; /// <summary> /// 信号 /// </summary> private readonly ManualResetEvent _mre; /// <summary> /// 日志 /// </summary> private readonly ILog _log; /// <summary> /// 日志 /// </summary> private static FlashLogger _flashLog = new FlashLogger(); private FlashLogger () { var configFile = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config" )); if (!configFile.Exists) { throw new Exception("未配置log4net配置文件!" ); } // 设置日志配置文件路径 XmlConfigurator.Configure(configFile); _que = new ConcurrentQueue<FlashLogMessage>(); _mre = new ManualResetEvent(false ); _log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); } /// <summary> /// 实现单例 /// </summary> /// <returns></returns> public static FlashLogger Instance () { return _flashLog; } /// <summary> /// 另一个线程记录日志,只在程序初始化时调用一次 /// </summary> public void Register () { Thread t = new Thread(new ThreadStart(WriteLog)); t.IsBackground = false ; t.Start(); } /// <summary> /// 从队列中写日志至磁盘 /// </summary> private void WriteLog () { while (true ) { // 等待信号通知 _mre.WaitOne(); FlashLogMessage msg; // 判断是否有内容需要如磁盘 从列队中获取内容,并删除列队中的内容 while (_que.Count > 0 && _que.TryDequeue(out msg)) { // 判断日志等级,然后写日志 switch (msg.Level) { case FlashLogLevel.Debug: _log.Debug(msg.Message, msg.Exception); break ; case FlashLogLevel.Info: _log.Info(msg.Message, msg.Exception); break ; case FlashLogLevel.Error: _log.Error(msg.Message, msg.Exception); break ; case FlashLogLevel.Warn: _log.Warn(msg.Message, msg.Exception); break ; case FlashLogLevel.Fatal: _log.Fatal(msg.Message, msg.Exception); break ; } } // 重新设置信号 _mre.Reset(); Thread.Sleep(1 ); } } /// <summary> /// 写日志 /// </summary> /// <param name="message"> 日志文本</param> /// <param name="level"> 等级</param> /// <param name="ex"> Exception</param> public void EnqueueMessage (string message, FlashLogLevel level, Exception ex = null ) { if ((level == FlashLogLevel.Debug && _log.IsDebugEnabled) || (level == FlashLogLevel.Error && _log.IsErrorEnabled) || (level == FlashLogLevel.Fatal && _log.IsFatalEnabled) || (level == FlashLogLevel.Info && _log.IsInfoEnabled) || (level == FlashLogLevel.Warn && _log.IsWarnEnabled)) { _que.Enqueue(new FlashLogMessage { Message = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff" ) + "]\r\n" + message, Level = level, Exception = ex }); // 通知线程往磁盘中写日志 _mre.Set(); } } public static void Debug (string msg, Exception ex = null ) { Instance().EnqueueMessage(msg, FlashLogLevel.Debug, ex); } public static void Error (string msg, Exception ex = null ) { Instance().EnqueueMessage(msg, FlashLogLevel.Error, ex); } public static void Fatal (string msg, Exception ex = null ) { Instance().EnqueueMessage(msg, FlashLogLevel.Fatal, ex); } public static void Info (string msg, Exception ex = null ) { Instance().EnqueueMessage(msg, FlashLogLevel.Info, ex); } public static void Warn (string msg, Exception ex = null ) { Instance().EnqueueMessage(msg, FlashLogLevel.Warn, ex); } } /// <summary> /// 日志等级 /// </summary> public enum FlashLogLevel { Debug, Info, Error, Warn, Fatal } /// <summary> /// 日志内容 /// </summary> public class FlashLogMessage { public string Message { get ; set ; } public FlashLogLevel Level { get ; set ; } public Exception Exception { get ; set ; } } }
4、性能对比和应用 4.1、性能对比 经过测试发现
使用原始的log4net写入日志100000条数据需要:19104毫秒。
同样数据使用列队方式只需要251毫秒。
4.2、应用 4.2.1、需要在程序启动时注册,如asp.net 程序中在Global.asax中的Application_Start注册。 public class MvcApplication : System .Web .HttpApplication { protected void Application_Start () { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); FlashLogger.Instance().Register(); } }
4.2.2、在需要写入日志的地方直接调用FlashLogger的静态方法即可。 FlashLogger.Debug("Debug" ); FlashLogger.Debug("Debug" , new Exception("testexception" )); FlashLogger.Info("Info" ); FlashLogger.Fatal("Fatal" ); FlashLogger.Error("Error" ); FlashLogger.Warn("Warn" , new Exception("testexception" ));
5、代码开源 GitHub: https://github.com/Emrys5/Emrys.FlashLog