C# 超高速高性能写日志

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信号,ManualResetEvent是用来通知线程列队中有新的日志,可以从列队中写入磁盘了。当从列队中写完日志后,重新设置信号,在等待下次有新的日志到来。

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、代码开源

https://github.com/Emrys5/Emrys.FlashLog

 源码分析:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Emrys.FlashLog
{

    /// <summary>
    /// 日志等级
    /// </summary>
    public enum FlashLogLevel
    {
        Debug,
        Info,
        Error,
        Warn,
        Fatal
    }
}

首先定义了一个枚举,列举日志的级别,Debug(调试),Info(信息),Error(错误),Warn(报警),Fatal (致命)五个级别

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Emrys.FlashLog
{


    /// <summary>
    /// 日志内容
    /// </summary>
    public class FlashLogMessage
    {
        public string Message { get; set; }
        public FlashLogLevel Level { get; set; }
        public Exception Exception { get; set; }

    }
}

FlashLogMessage类,包含message(消息)、FlashLogLevel(级别)、Exception(异常)三个属性

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);
        }

    }

 FlashLogger类采用单例的形式,初始化时会创建一个队列

 ConcurrentQueue<FlashLogMessage> _que = new ConcurrentQueue<FlashLogMessage>();用于存放消息

private readonly ManualResetEvent _mre = new ManualResetEvent(false);表示线程同步事件,收到信号时,必须手动重置该事件。

 private readonly ILog  _log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);记录日志类

/// <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);
            }
        }

上面的方法是为了阻塞线程  _mre.WaitOne();等待消息,当消息不为空时,根据消息的级别调用日志,记录。

  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A_nanda

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值