利用StackExchange.Redis和Log4Net构建日志队列

简介:本文是一个简单的demo用于展示利用StackExchange.Redis和Log4Net构建日志队列,为高并发日志处理提供一些思路。

0、先下载安装Redis服务,然后再服务列表里启动服务(Redis的默认端口是6379,貌似还有一个故事)(https://github.com/MicrosoftArchive/redis/releases)

 

1、nuget中安装Redis:Install-Package StackExchange.Redis -version 1.2.6
2、nuget中安装日志:Install-Package Log4Net -version 2.0.8

3、创建RedisConnectionHelp、RedisHelper类,用于调用Redis。由于是Demo我不打算用完整类,比较完整的可以查阅其他博客(例如:https://www.cnblogs.com/liqingwen/p/6672452.html)

/// <summary>
    /// StackExchange Redis ConnectionMultiplexer对象管理帮助类
    /// </summary>
    public class RedisConnectionHelp
    {
        //系统自定义Key前缀
        public static readonly string SysCustomKey = ConfigurationManager.AppSettings["redisKey"] ?? "";
        private static readonly string RedisConnectionString = ConfigurationManager.AppSettings["seRedis"] ?? "127.0.0.1:6379";

        private static readonly object Locker = new object();
        private static ConnectionMultiplexer _instance;
        private static readonly ConcurrentDictionary<string, ConnectionMultiplexer> ConnectionCache = new ConcurrentDictionary<string, ConnectionMultiplexer>();

        /// <summary>
        /// 单例获取
        /// </summary>
        public static ConnectionMultiplexer Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock (Locker)
                    {
                        if (_instance == null || !_instance.IsConnected)
                        {
                            _instance = GetManager();
                        }
                    }
                }
                return _instance;
            }
        }

        /// <summary>
        /// 缓存获取
        /// </summary>
        /// <param name="connectionString"></param>
        /// <returns></returns>
        public static ConnectionMultiplexer GetConnectionMultiplexer(string connectionString)
        {
            if (!ConnectionCache.ContainsKey(connectionString))
            {
                ConnectionCache[connectionString] = GetManager(connectionString);
            }
            return ConnectionCache[connectionString];
        }

        private static ConnectionMultiplexer GetManager(string connectionString = null)
        {
            connectionString = connectionString ?? RedisConnectionString;
            var connect = ConnectionMultiplexer.Connect(connectionString);       
            return connect;
        }
    }
View Code
public class RedisHelper
    {
        private int DbNum { get; set; }
        private readonly ConnectionMultiplexer _conn;
        public string CustomKey;

        public RedisHelper(int dbNum = 0)
            : this(dbNum, null)
        {
        }

        public RedisHelper(int dbNum, string readWriteHosts)
        {
            DbNum = dbNum;
            _conn =
                string.IsNullOrWhiteSpace(readWriteHosts) ?
                RedisConnectionHelp.Instance :
                RedisConnectionHelp.GetConnectionMultiplexer(readWriteHosts);
        }

       

        private string AddSysCustomKey(string oldKey)
        {
            var prefixKey = CustomKey ?? RedisConnectionHelp.SysCustomKey;
            return prefixKey + oldKey;
        }

        private T Do<T>(Func<IDatabase, T> func)
        {
            var database = _conn.GetDatabase(DbNum);
            return func(database);
        }

        private string ConvertJson<T>(T value)
        {
            string result = value is string ? value.ToString() : JsonConvert.SerializeObject(value);
            return result;
        }

        private T ConvertObj<T>(RedisValue value)
        {
            Type t = typeof(T);
            if (t.Name == "String")
            {                
                return (T)Convert.ChangeType(value, typeof(string));
            }

            return JsonConvert.DeserializeObject<T>(value);
        }

        private List<T> ConvetList<T>(RedisValue[] values)
        {
            List<T> result = new List<T>();
            foreach (var item in values)
            {
                var model = ConvertObj<T>(item);
                result.Add(model);
            }
            return result;
        }

        private RedisKey[] ConvertRedisKeys(List<string> redisKeys)
        {
            return redisKeys.Select(redisKey => (RedisKey)redisKey).ToArray();
        }

      

        /// <summary>
        /// 入队
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void ListRightPush<T>(string key, T value)
        {
            key = AddSysCustomKey(key);
            Do(db => db.ListRightPush(key, ConvertJson(value)));
        }      
 

        /// <summary>
        /// 出队
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public T ListLeftPop<T>(string key)
        {
            key = AddSysCustomKey(key);
            return Do(db =>
            {
                var value = db.ListLeftPop(key);
                return ConvertObj<T>(value);
            });
        }

        /// <summary>
        /// 获取集合中的数量
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public long ListLength(string key)
        {
            key = AddSysCustomKey(key);
            return Do(redis => redis.ListLength(key));
        }


    }
View Code

4、创建log4net的配置文件log4net.config。设置属性为:始终复制、内容。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>

  <log4net>
    <root>
      <!--(高) OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL (低) -->
      <!--级别按以上顺序,如果level选择error,那么程序中即便调用info,也不会记录日志-->
      <level value="ALL" />
      <!--appender-ref可以理解为某种具体的日志保存规则,包括生成的方式、命名方式、展示方式-->
      <appender-ref ref="MyErrorAppender"/>
    </root>

    <appender name="MyErrorAppender" type="log4net.Appender.RollingFileAppender">
      <!--日志路径,相对于项目根目录-->
      <param name= "File" value= "Log\\"/>
      <!--是否是向文件中追加日志-->
      <param name= "AppendToFile" value= "true"/>
      <!--日志根据日期滚动-->
      <param name= "RollingStyle" value= "Date"/>
      <!--日志文件名格式为:日期文件夹/Error_2019_3_19.log,前面的yyyyMMdd/是指定文件夹名称-->
      <param name= "DatePattern" value= "yyyyMMdd/Error_yyyy_MM_dd&quot;.log&quot;"/>
      <!--日志文件名是否是固定不变的-->
      <param name= "StaticLogFileName" value= "false"/>
      <!--日志文件大小,可以使用"KB", "MB" 或 "GB"为单位-->
      <!--<param name="MaxFileSize" value="500MB" />-->
      <layout type="log4net.Layout.PatternLayout,log4net">
        <!--%n 回车-->
        <!--%d 当前语句运行的时刻,格式%date{yyyy-MM-dd HH:mm:ss,fff}-->
        <!--%t 引发日志事件的线程,如果没有线程名就使用线程号-->
        <!--%p 日志的当前优先级别-->
        <!--%c 当前日志对象的名称-->
        <!--%m 输出的日志消息-->
        <!--%-数字 表示该项的最小长度,如果不够,则用空格 -->
        <param name="ConversionPattern" value="========[Begin]========%n%d [线程%t] %-5p %c 日志正文如下- %n%m%n%n" />
      </layout>
      <!-- 最小锁定模型,可以避免名字重叠。文件锁类型,RollingFileAppender本身不是线程安全的,-->
      <!-- 如果在程序中没有进行线程安全的限制,可以在这里进行配置,确保写入时的安全。-->
      <!-- 文件锁定的模式,官方文档上他有三个可选值“FileAppender.ExclusiveLock, FileAppender.MinimalLock and FileAppender.InterProcessLock”,-->
      <!-- 默认是第一个值,排他锁定,一次值能有一个进程访问文件,close后另外一个进程才可以访问;第二个是最小锁定模式,允许多个进程可以同时写入一个文件;第三个目前还不知道有什么作用-->
      <!-- 里面为什么是一个“+”号。。。问得好!我查了很久文件也不知道为什么不是点,而是加号。反正必须是加号-->
      <param name="lockingModel"  type="log4net.Appender.FileAppender+MinimalLock" />

      <!--日志过滤器,配置可以参考其他人博文:https://www.cnblogs.com/cxd4321/archive/2012/07/14/2591142.html -->
      <filter type="log4net.Filter.LevelMatchFilter">
        <LevelToMatch value="ERROR" />
      </filter>
      <!-- 上面的过滤器,其实可以写得很复杂,而且可以多个以or的形式并存。如果符合过滤条件就会写入日志,如果不符合条件呢?不是不要了-->
      <!-- 相反是不符合过滤条件也写入日志,所以最后加一个DenyAllFilter,使得不符合上面条件的直接否决通过-->
      <filter type="log4net.Filter.DenyAllFilter" />
    </appender>
  </log4net>
</configuration>
View Code

5、创建日志类LoggerFunc、日志工厂类LoggerFactory

/// <summary>
    /// 日志单例工厂
    /// </summary>
    public class LoggerFactory
    {
        public static string CommonQueueName = "DisSunQueue";
        private static LoggerFunc log;
        private static object logKey = new object();
        public static LoggerFunc CreateLoggerInstance()
        {
            if (log != null)
            {
                return log;
            }

            lock (logKey)
            {
                if (log == null)
                {
                    string log4NetPath = AppDomain.CurrentDomain.BaseDirectory + "Config\\log4net.config";
                    log = new LoggerFunc();
                    log.logCfg = new FileInfo(log4NetPath);
                    log.errorLogger = log4net.LogManager.GetLogger("MyError");
                    log.QueueName = CommonQueueName;//存储在Redis中的键名
                    log4net.Config.XmlConfigurator.ConfigureAndWatch(log.logCfg);    //加载日志配置文件S                
                }
            }

            return log;
        }
    }
View Code
/// <summary>
    /// 日志类实体
    /// </summary>
    public class LoggerFunc
    {
        public FileInfo logCfg;
        public log4net.ILog errorLogger;
        public string QueueName;       

        /// <summary>
        /// 保存错误日志
        /// </summary>
        /// <param name="title">日志内容</param>
        public void SaveErrorLogTxT(string title)
        {
            RedisHelper redis = new RedisHelper();
            //塞进队列的右边,表示从队列的尾部插入。
            redis.ListRightPush<string>(QueueName, title);           
        }

        /// <summary>
        /// 日志队列是否为空
        /// </summary>
        /// <returns></returns>
        public bool IsEmptyLogQueue()
        { 
            RedisHelper redis = new RedisHelper();
            if (redis.ListLength(QueueName) > 0)
            {
                return false;
            }
            return true;        
        }

    }
View Code

6、创建本章最核心的日志队列设置类LogQueueConfig。

ThreadPool是线程池,通过这种方式可以减少线程的创建与销毁,提高性能。也就是说每次需要用到线程时,线程池都会自动安排一个还没有销毁的空闲线程,不至于每次用完都销毁,或者每次需要都重新创建。但其实我不太明白他的底层运行原理,在内部while,是让这个线程一直不被销毁一直存在么?还是说sleep结束后,可以直接拿到一个线程池提供的新线程。为什么不是在ThreadPool.QueueUserWorkItem之外进行循环调用?了解的童鞋可以给我留下言。

/// <summary>
    /// 日志队列设置类
    /// </summary>
    public class LogQueueConfig
    {
        public static void RegisterLogQueue()
        {
            ThreadPool.QueueUserWorkItem(o =>
            {
                while (true)
                {
                    RedisHelper redis = new RedisHelper();
                    LoggerFunc logFunc = LoggerFactory.CreateLoggerInstance();
                    if (!logFunc.IsEmptyLogQueue())
                    {
                        //从队列的左边弹出,表示从队列头部出队
                        string logMsg = redis.ListLeftPop<string>(logFunc.QueueName);

                        if (!string.IsNullOrWhiteSpace(logMsg))
                        {
                            logFunc.errorLogger.Error(logMsg);
                        }
                    }
                    else
                    {
                        Thread.Sleep(1000); //为避免CPU空转,在队列为空时休息1秒
                    }
                }
            });
        }
    }
View Code

7、在项目的Global.asax文件中,启动队列线程。本demo由于是在winForm中,所以放在form中。
 

        public Form1()
        {
            InitializeComponent();
            RedisLogQueueTest.CommonFunc.LogQueueConfig.RegisterLogQueue();//启动日志队列
        }

8、调用日志类LoggerFunc.SaveErrorLogTxT(),插入日志。

            LoggerFunc log = LoggerFactory.CreateLoggerInstance();
            log.SaveErrorLogTxT("您插入了一条随机数:"+longStr);

9、查看下入效果

 

 

10、完整源码(winForm不懂?差不多的啦,打开项目直接运行就可以看见界面):

https://gitee.com/dissun/RedisLogQueueTest

 

#### 原创:DisSun ##########

#### 时间:2019.03.19 #######

posted @ 2019-03-19 16:05  第三皇族DisSun  阅读(755)  评论(0编辑  收藏  举报