利用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)

  1. /// <summary>
  2. /// StackExchange Redis ConnectionMultiplexer对象管理帮助类
  3. /// </summary>
  4. public class RedisConnectionHelp
  5. {
  6. //系统自定义Key前缀
  7. public static readonly string SysCustomKey = ConfigurationManager.AppSettings["redisKey"] ?? "";
  8. private static readonly string RedisConnectionString = ConfigurationManager.AppSettings["seRedis"] ?? "127.0.0.1:6379";
  9.  
  10. private static readonly object Locker = new object();
  11. private static ConnectionMultiplexer _instance;
  12. private static readonly ConcurrentDictionary<string, ConnectionMultiplexer> ConnectionCache = new ConcurrentDictionary<string, ConnectionMultiplexer>();
  13.  
  14. /// <summary>
  15. /// 单例获取
  16. /// </summary>
  17. public static ConnectionMultiplexer Instance
  18. {
  19. get
  20. {
  21. if (_instance == null)
  22. {
  23. lock (Locker)
  24. {
  25. if (_instance == null || !_instance.IsConnected)
  26. {
  27. _instance = GetManager();
  28. }
  29. }
  30. }
  31. return _instance;
  32. }
  33. }
  34.  
  35. /// <summary>
  36. /// 缓存获取
  37. /// </summary>
  38. /// <param name="connectionString"></param>
  39. /// <returns></returns>
  40. public static ConnectionMultiplexer GetConnectionMultiplexer(string connectionString)
  41. {
  42. if (!ConnectionCache.ContainsKey(connectionString))
  43. {
  44. ConnectionCache[connectionString] = GetManager(connectionString);
  45. }
  46. return ConnectionCache[connectionString];
  47. }
  48.  
  49. private static ConnectionMultiplexer GetManager(string connectionString = null)
  50. {
  51. connectionString = connectionString ?? RedisConnectionString;
  52. var connect = ConnectionMultiplexer.Connect(connectionString);
  53. return connect;
  54. }
  55. }
  1. public class RedisHelper
  2. {
  3. private int DbNum { get; set; }
  4. private readonly ConnectionMultiplexer _conn;
  5. public string CustomKey;
  6.  
  7. public RedisHelper(int dbNum = 0)
  8. : this(dbNum, null)
  9. {
  10. }
  11.  
  12. public RedisHelper(int dbNum, string readWriteHosts)
  13. {
  14. DbNum = dbNum;
  15. _conn =
  16. string.IsNullOrWhiteSpace(readWriteHosts) ?
  17. RedisConnectionHelp.Instance :
  18. RedisConnectionHelp.GetConnectionMultiplexer(readWriteHosts);
  19. }
  20.  
  21. private string AddSysCustomKey(string oldKey)
  22. {
  23. var prefixKey = CustomKey ?? RedisConnectionHelp.SysCustomKey;
  24. return prefixKey + oldKey;
  25. }
  26.  
  27. private T Do<T>(Func<IDatabase, T> func)
  28. {
  29. var database = _conn.GetDatabase(DbNum);
  30. return func(database);
  31. }
  32.  
  33. private string ConvertJson<T>(T value)
  34. {
  35. string result = value is string ? value.ToString() : JsonConvert.SerializeObject(value);
  36. return result;
  37. }
  38.  
  39. private T ConvertObj<T>(RedisValue value)
  40. {
  41. Type t = typeof(T);
  42. if (t.Name == "String")
  43. {
  44. return (T)Convert.ChangeType(value, typeof(string));
  45. }
  46.  
  47. return JsonConvert.DeserializeObject<T>(value);
  48. }
  49.  
  50. private List<T> ConvetList<T>(RedisValue[] values)
  51. {
  52. List<T> result = new List<T>();
  53. foreach (var item in values)
  54. {
  55. var model = ConvertObj<T>(item);
  56. result.Add(model);
  57. }
  58. return result;
  59. }
  60.  
  61. private RedisKey[] ConvertRedisKeys(List<string> redisKeys)
  62. {
  63. return redisKeys.Select(redisKey => (RedisKey)redisKey).ToArray();
  64. }
  65.  
  66. /// <summary>
  67. /// 入队
  68. /// </summary>
  69. /// <param name="key"></param>
  70. /// <param name="value"></param>
  71. public void ListRightPush<T>(string key, T value)
  72. {
  73. key = AddSysCustomKey(key);
  74. Do(db => db.ListRightPush(key, ConvertJson(value)));
  75. }
  76.  
  77. /// <summary>
  78. /// 出队
  79. /// </summary>
  80. /// <typeparam name="T"></typeparam>
  81. /// <param name="key"></param>
  82. /// <returns></returns>
  83. public T ListLeftPop<T>(string key)
  84. {
  85. key = AddSysCustomKey(key);
  86. return Do(db =>
  87. {
  88. var value = db.ListLeftPop(key);
  89. return ConvertObj<T>(value);
  90. });
  91. }
  92.  
  93. /// <summary>
  94. /// 获取集合中的数量
  95. /// </summary>
  96. /// <param name="key"></param>
  97. /// <returns></returns>
  98. public long ListLength(string key)
  99. {
  100. key = AddSysCustomKey(key);
  101. return Do(redis => redis.ListLength(key));
  102. }
  103.  
  104. }

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

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. <configSections>
  4. <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  5. </configSections>
  6.  
  7. <log4net>
  8. <root>
  9. <!--(高) OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL (低) -->
  10. <!--级别按以上顺序,如果level选择error,那么程序中即便调用info,也不会记录日志-->
  11. <level value="ALL" />
  12. <!--appender-ref可以理解为某种具体的日志保存规则,包括生成的方式、命名方式、展示方式-->
  13. <appender-ref ref="MyErrorAppender"/>
  14. </root>
  15.  
  16. <appender name="MyErrorAppender" type="log4net.Appender.RollingFileAppender">
  17. <!--日志路径,相对于项目根目录-->
  18. <param name= "File" value= "Log\\"/>
  19. <!--是否是向文件中追加日志-->
  20. <param name= "AppendToFile" value= "true"/>
  21. <!--日志根据日期滚动-->
  22. <param name= "RollingStyle" value= "Date"/>
  23. <!--日志文件名格式为:日期文件夹/Error_2019_3_19.log,前面的yyyyMMdd/是指定文件夹名称-->
  24. <param name= "DatePattern" value= "yyyyMMdd/Error_yyyy_MM_dd&quot;.log&quot;"/>
  25. <!--日志文件名是否是固定不变的-->
  26. <param name= "StaticLogFileName" value= "false"/>
  27. <!--日志文件大小,可以使用"KB", "MB" 或 "GB"为单位-->
  28. <!--<param name="MaxFileSize" value="500MB" />-->
  29. <layout type="log4net.Layout.PatternLayout,log4net">
  30. <!--%n 回车-->
  31. <!--%d 当前语句运行的时刻,格式%date{yyyy-MM-dd HH:mm:ss,fff}-->
  32. <!--%t 引发日志事件的线程,如果没有线程名就使用线程号-->
  33. <!--%p 日志的当前优先级别-->
  34. <!--%c 当前日志对象的名称-->
  35. <!--%m 输出的日志消息-->
  36. <!--%-数字 表示该项的最小长度,如果不够,则用空格 -->
  37. <param name="ConversionPattern" value="========[Begin]========%n%d [线程%t] %-5p %c 日志正文如下- %n%m%n%n" />
  38. </layout>
  39. <!-- 最小锁定模型,可以避免名字重叠。文件锁类型,RollingFileAppender本身不是线程安全的,-->
  40. <!-- 如果在程序中没有进行线程安全的限制,可以在这里进行配置,确保写入时的安全。-->
  41. <!-- 文件锁定的模式,官方文档上他有三个可选值“FileAppender.ExclusiveLock, FileAppender.MinimalLock and FileAppender.InterProcessLock”,-->
  42. <!-- 默认是第一个值,排他锁定,一次值能有一个进程访问文件,close后另外一个进程才可以访问;第二个是最小锁定模式,允许多个进程可以同时写入一个文件;第三个目前还不知道有什么作用-->
  43. <!-- 里面为什么是一个“+”号。。。问得好!我查了很久文件也不知道为什么不是点,而是加号。反正必须是加号-->
  44. <param name="lockingModel" type="log4net.Appender.FileAppender+MinimalLock" />
  45.  
  46. <!--日志过滤器,配置可以参考其他人博文:https://www.cnblogs.com/cxd4321/archive/2012/07/14/2591142.html -->
  47. <filter type="log4net.Filter.LevelMatchFilter">
  48. <LevelToMatch value="ERROR" />
  49. </filter>
  50. <!-- 上面的过滤器,其实可以写得很复杂,而且可以多个以or的形式并存。如果符合过滤条件就会写入日志,如果不符合条件呢?不是不要了-->
  51. <!-- 相反是不符合过滤条件也写入日志,所以最后加一个DenyAllFilter,使得不符合上面条件的直接否决通过-->
  52. <filter type="log4net.Filter.DenyAllFilter" />
  53. </appender>
  54. </log4net>
  55. </configuration>

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

  1. /// <summary>
  2. /// 日志单例工厂
  3. /// </summary>
  4. public class LoggerFactory
  5. {
  6. public static string CommonQueueName = "DisSunQueue";
  7. private static LoggerFunc log;
  8. private static object logKey = new object();
  9. public static LoggerFunc CreateLoggerInstance()
  10. {
  11. if (log != null)
  12. {
  13. return log;
  14. }
  15.  
  16. lock (logKey)
  17. {
  18. if (log == null)
  19. {
  20. string log4NetPath = AppDomain.CurrentDomain.BaseDirectory + "Config\\log4net.config";
  21. log = new LoggerFunc();
  22. log.logCfg = new FileInfo(log4NetPath);
  23. log.errorLogger = log4net.LogManager.GetLogger("MyError");
  24. log.QueueName = CommonQueueName;//存储在Redis中的键名
  25. log4net.Config.XmlConfigurator.ConfigureAndWatch(log.logCfg); //加载日志配置文件S
  26. }
  27. }
  28.  
  29. return log;
  30. }
  31. }
  1. /// <summary>
  2. /// 日志类实体
  3. /// </summary>
  4. public class LoggerFunc
  5. {
  6. public FileInfo logCfg;
  7. public log4net.ILog errorLogger;
  8. public string QueueName;
  9.  
  10. /// <summary>
  11. /// 保存错误日志
  12. /// </summary>
  13. /// <param name="title">日志内容</param>
  14. public void SaveErrorLogTxT(string title)
  15. {
  16. RedisHelper redis = new RedisHelper();
  17. //塞进队列的右边,表示从队列的尾部插入。
  18. redis.ListRightPush<string>(QueueName, title);
  19. }
  20.  
  21. /// <summary>
  22. /// 日志队列是否为空
  23. /// </summary>
  24. /// <returns></returns>
  25. public bool IsEmptyLogQueue()
  26. {
  27. RedisHelper redis = new RedisHelper();
  28. if (redis.ListLength(QueueName) > 0)
  29. {
  30. return false;
  31. }
  32. return true;
  33. }
  34.  
  35. }

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

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

  1. /// <summary>
  2. /// 日志队列设置类
  3. /// </summary>
  4. public class LogQueueConfig
  5. {
  6. public static void RegisterLogQueue()
  7. {
  8. ThreadPool.QueueUserWorkItem(o =>
  9. {
  10. while (true)
  11. {
  12. RedisHelper redis = new RedisHelper();
  13. LoggerFunc logFunc = LoggerFactory.CreateLoggerInstance();
  14. if (!logFunc.IsEmptyLogQueue())
  15. {
  16. //从队列的左边弹出,表示从队列头部出队
  17. string logMsg = redis.ListLeftPop<string>(logFunc.QueueName);
  18.  
  19. if (!string.IsNullOrWhiteSpace(logMsg))
  20. {
  21. logFunc.errorLogger.Error(logMsg);
  22. }
  23. }
  24. else
  25. {
  26. Thread.Sleep(1000); //为避免CPU空转,在队列为空时休息1秒
  27. }
  28. }
  29. });
  30. }
  31. }

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

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

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

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

9、查看下入效果

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

https://gitee.com/dissun/RedisLogQueueTest

StackExchange.Redis和Log4Net构建日志的更多相关文章

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

    简介:本文是一个简单的demo用于展示利用StackExchange.Redis和Log4Net构建日志队列,为高并发日志处理提供一些思路. 0.先下载安装Redis服务,然后再服务列表里启动服务(R ...

  2. .netcore里使用StackExchange.Redis TimeOut 情况解决方法

    在用StackExchange.Redis这个组件时候,时不时会出现异常TimeOut解决方法如下, 解决方法: 在Program的Main入口方法里添加一句话: System.Threading.T ...

  3. RedisRepository封装—Redis发布订阅以及StackExchange.Redis中的使用

    本文版权归博客园和作者本人吴双共同所有,转载请注明本Redis系列分享地址.http://www.cnblogs.com/tdws/tag/NoSql/ Redis Pub/Sub模式 基本介绍 Re ...

  4. Lind.DDD.RedisClient~对StackExchange.Redis调用者的封装及多路复用技术

    回到目录 两雄争霸 使用StackExchange.Redis的原因是因为它开源,免费,而对于商业化的ServiceStack.Redis,它将一步步被前者取代,开源将是一种趋势,商业化也值得被我们尊 ...

  5. ELK+kafka构建日志收集系统

    ELK+kafka构建日志收集系统   原文  http://lx.wxqrcode.com/index.php/post/101.html   背景: 最近线上上了ELK,但是只用了一台Redis在 ...

  6. StackExchange.Redis 官方文档(三) Events

    事件 ConnectionMultiplexer类型提供了很多可以用来了解表面状态下正在发生着什么的事件.这对日志是很有用的. ConfigurationChanged - ConnectionMul ...

  7. StackExchange.Redis学习笔记(四) 事务控制和Batch批量操作

    Redis事物 Redis命令实现事务 Redis的事物包含在multi和exec(执行)或者discard(回滚)命令中 和sql事务不同的是,Redis调用Exec只是将所有的命令变成一个单元一起 ...

  8. StackExchange.Redis超时的问题

    最近公司有个项目,在请求量大的情况下,有大量的错误日志是关于redis超时的问题: Timeout performing SET XXX, inst: 27, mgr: ProcessReadQueu ...

  9. StackExchange.Redis性能调优

    大家经常出现同步调用Redis超时的问题,但改成异步之后发现错误非常少了,但却可能通过前后记日志之类的发现Redis命令非常慢. PS: 以后代码都在Windows bash中运行,StackExch ...

随机推荐

  1. office install problems

    regedit 0000 "00005"或"00002"开头的项 remove all regedit options

  2. python QMainWindow QWidget

    from PyQt5 import QtWidgetsfrom untitled import Ui_MainWindowfrom PyQt5.QtWidgets import QFileDialog ...

  3. ubuntu默认启动方式修改 psensor命令

    Check UUID sudo blkid Then sudo gedit /etc/default/grub & to pull up the boot loader configurati ...

  4. mac crontab时间断内随机时间执行定时任务

    首先需要了解crontab使用,这里不多,主要是时间断内随机时间: 然而crontab 并没有具体方法实现时间段内随机时间执行,我的办法如下: 这里测试一个例子: 执行一个数据存文件python脚本, ...

  5. Cyclic Components CodeForces - 977E(DFS)

    Cyclic Components CodeForces - 977E You are given an undirected graph consisting of nn vertices and  ...

  6. 【基础】火狐和谷歌在Selenium3.0上的启动(二)

    参考地址:http://www.cnblogs.com/fnng/p/5932224.html https://github.com/mozilla/geckodriver [火狐浏览器] 火狐浏览器 ...

  7. [转载] JAVA面试题和项目面试核心要点精华总结(想进大公司必看)

    JAVA面试题和项目面试核心要点精华总结(想进大公司必看) JAVA面试题和项目面试核心要点精华总结(想进大公司必看)

  8. 关于学习Vue的前置工作/技术储备

    关于学习Vue的前置工作/技术储备 1.GitBatch 2.Sublime Text 3.Node-----npm 命令 本人用的idea GitBatch: GitBatch是一个可以编写shel ...

  9. 5.5 C++重载赋值操作符

    参考:http://www.weixueyuan.net/view/6383.html 总结: 重载赋值操作符同重载类的是拷贝构造函数的原因是一样,将一个对象拷贝给另一个对象,同时当类中存在指针类型的 ...

  10. jdbc中Class.forName(driverName)的作用

    上次面试别人问我jdbc的过程: 我是这样回答的: Class.forName加载驱动 DriverManager.connect(url,username, password)获取连接对象 conn ...