一、需求
需求很簡單,就是在C#開發中高速寫日誌。好比在高併發,高流量的地方須要寫日誌。咱們知道程序在操做磁盤時是比較耗時的,因此咱們把日誌寫到磁盤上會有必定的時間耗在上面,這些並非咱們想看到的。html
二、解決方案
2.一、簡單原理說明
使用列隊先緩存到內存,而後咱們一直有個線程再從列隊中寫到磁盤上,這樣就能夠高速高性能的寫日誌了。由於速度慢的地方咱們分離出來了,也就是說程序在把日誌扔給列隊後,程序的日誌部分就算完成了,後面操做磁盤耗時的部分程序是不須要關心的,由另外一個線程操做。git
俗話說,魚和熊掌不可兼得,這樣會有一個問題,就是若是日誌已經到列隊了這個時候程序崩潰或者電腦斷電都會致使日誌部分丟失,可是有些地方爲了高性能的寫日誌,是否能夠忽略一些狀況,請各位根據狀況而定。github
2.二、示例圖
三、關鍵代碼部分
這裏寫日誌的部分LZ選用了比較經常使用的log4net,固然也能夠選擇其餘的日誌組件,好比nlog等等。apache
3.一、日誌至列隊部分
第一步咱們首先須要把日誌放到列隊中,而後才能從列隊中寫到磁盤上。緩存
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信號,ManualResetEvent是用來通知線程列隊中有新的日誌,能夠從列隊中寫入磁盤了。當從列隊中寫完日誌後,從新設置信號,在等待下次有新的日誌到來。asp.net
3.二、列隊到磁盤
從列隊到磁盤咱們須要有一個線程從列隊寫入磁盤,也就是說咱們在程序啓動時就要加載這個線程,好比asp.net中就要在global中的Application_Start中加載。ide
/// <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.三、完整代碼
![](http://static.javashuo.com/static/loading.gif)
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.一、性能對比
通過測試發現高併發
使用原始的log4net寫入日誌100000條數據須要:19104毫秒。post
一樣數據使用列隊方式只須要251毫秒。
4.二、應用
4.2.一、須要在程序啓動時註冊,如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.二、在須要寫入日誌的地方直接調用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"));
五、代碼開源
https://github.com/Emrys5/Emrys.FlashLog
最後望對各位有所幫助,本文原創,歡迎拍磚和推薦。