簡介:本文是一個簡單的demo用於展示利用StackExchange.Redis和Log4Net構建日誌隊列,為高併發日誌處理提供一些思路。 0、先下載安裝Redis服務,然後再服務列表裡啟動服務(Redis的預設埠是6379,貌似還有一個故事)(https://github.com/Micros ...
簡介:本文是一個簡單的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".log""/> <!--日誌文件名是否是固定不變的--> <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 #######