zoukankan      html  css  js  c++  java
  • 利用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 #######

  • 相关阅读:
    asp.net2.0系列视频教程
    Android入门教程(三十一)SQLite分页读取(转)
    sql语句大全
    判断是不是ie浏览器 加上ie11
    字节面试题
    泛型
    线程和
    sysbench工具
    自定义集合类
    类型通配符
  • 原文地址:https://www.cnblogs.com/dissun/p/10558817.html
Copyright © 2011-2022 走看看