简介:本文是一个简单的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)
////// StackExchange Redis ConnectionMultiplexer对象管理帮助类 /// 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>(); /// /// 单例获取 /// public static ConnectionMultiplexer Instance { get { if (_instance == null) { lock (Locker) { if (_instance == null || !_instance.IsConnected) { _instance = GetManager(); } } } return _instance; } } /// /// 缓存获取 /// /// /// 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; } }
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(Func func) { var database = _conn.GetDatabase(DbNum); return func(database); } private string ConvertJson (T value) { string result = value is string ? value.ToString() : JsonConvert.SerializeObject(value); return result; } private T ConvertObj (RedisValue value) { Type t = typeof(T); if (t.Name == "String") { return (T)Convert.ChangeType(value, typeof(string)); } return JsonConvert.DeserializeObject (value); } private List ConvetList (RedisValue[] values) { List result = new List (); foreach (var item in values) { var model = ConvertObj (item); result.Add(model); } return result; } private RedisKey[] ConvertRedisKeys(List<string> redisKeys) { return redisKeys.Select(redisKey => (RedisKey)redisKey).ToArray(); } /// /// 入队 /// /// /// public void ListRightPush (string key, T value) { key = AddSysCustomKey(key); Do(db => db.ListRightPush(key, ConvertJson(value))); } /// /// 出队 /// /// /// /// public T ListLeftPop (string key) { key = AddSysCustomKey(key); return Do(db => { var value = db.ListLeftPop(key); return ConvertObj (value); }); } /// /// 获取集合中的数量 /// /// /// public long ListLength(string key) { key = AddSysCustomKey(key); return Do(redis => redis.ListLength(key)); } }
4、创建log4net的配置文件log4net.config。设置属性为:始终复制、内容。
xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/> configSections> <log4net> <root> <level value="ALL" /> <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"/> <param name= "DatePattern" value= "yyyyMMdd/Error_yyyy_MM_dd".log""/> <param name= "StaticLogFileName" value= "false"/> <layout type="log4net.Layout.PatternLayout,log4net"> <param name="ConversionPattern" value="========[Begin]========%n%d [线程%t] %-5p %c 日志正文如下- %n%m%n%n" /> layout> <param name="lockingModel" type="log4net.Appender.FileAppender+MinimalLock" /> <filter type="log4net.Filter.LevelMatchFilter"> <LevelToMatch value="ERROR" /> filter> <filter type="log4net.Filter.DenyAllFilter" /> appender> log4net> configuration>
5、创建日志类LoggerFunc、日志工厂类LoggerFactory
////// 日志单例工厂 /// 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; } }
////// 日志类实体 /// public class LoggerFunc { public FileInfo logCfg; public log4net.ILog errorLogger; public string QueueName; /// /// 保存错误日志 /// /// 日志内容 public void SaveErrorLogTxT(string title) { RedisHelper redis = new RedisHelper(); //塞进队列的右边,表示从队列的尾部插入。 redis.ListRightPush<string>(QueueName, title); } /// /// 日志队列是否为空 /// /// public bool IsEmptyLogQueue() { RedisHelper redis = new RedisHelper(); if (redis.ListLength(QueueName) > 0) { return false; } return true; } }
6、创建本章最核心的日志队列设置类LogQueueConfig。
ThreadPool是线程池,通过这种方式可以减少线程的创建与销毁,提高性能。也就是说每次需要用到线程时,线程池都会自动安排一个还没有销毁的空闲线程,不至于每次用完都销毁,或者每次需要都重新创建。但其实我不太明白他的底层运行原理,在内部while,是让这个线程一直不被销毁一直存在么?还是说sleep结束后,可以直接拿到一个线程池提供的新线程。为什么不是在ThreadPool.QueueUserWorkItem之外进行循环调用?了解的童鞋可以给我留下言。
////// 日志队列设置类 /// 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秒 } } }); } }
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 #######