当前位置: 移动技术网 > IT编程>开发语言>.net > 利用StackExchange.Redis和Log4Net构建日志队列

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

2019年03月20日  | 移动技术网IT编程  | 我要评论

吉备宏纯,宁波交通网,易建联cba扣篮大赛

简介:本文是一个简单的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 #######

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网