離前一篇《一步步開發自己的博客 .NET版(6、手機端的兼容)》都個把月了。
當時寫完第六篇,很多人問“劇終”了?你還有好多實現沒有講解分析呢。我說沒呢,后期還會有第二版、第三版...至于還沒有分析到,后期補上。你看,我今天不就來了么。
前段時間寫代碼,手都寫的酸痛酸痛的,歇息了幾天,好多了。然后,又搗鼓了一下webapi。這也是個沒有接觸過的知識。跌跌撞撞的整了點東西出來。有興趣寫移動端的同學可以聯系我,大家一起學習。API文檔和測試地址:http://haojima.net/SwaggerUI
其他的就不多說了,進入今天的主題,異步記錄日志和文章閱讀量統計。
我們常用日志記錄,無非就是,數據庫記錄和文本日志記錄。而今天我要說的是,文本日志記錄。
最簡單的文本記錄: File.WriteAllText(path,messg); 使用靜態類File的WriteAllText 如果文件存在則覆蓋,傳入文件路徑和消息內容。ok,完事。
當然,我們不能每次都覆蓋上一次的記錄。 File.AppendAllText(path,messg); 那么我們可是在原有內容追加。這里,我們不用關系文件流是否關閉,使用靜態類File的這兩個方法都會自動幫我們關閉。
如果,我們是使用的winfrom單線程。那么,基本的日志記錄就這個兩個方法 完全可以搞定。
但是,如果是web程序就不一樣了,天生的多線程。多個線程同時訪問一個文件,肯定是會報錯的。不信你試試。
那我們怎么解決這個問題?有人會說,加鎖唄。鎖肯定是要加,不過要看怎么加了。如果加到寫文件內容的時候肯定是不合適的。因為寫文件要打開文件流,比較耗時。我們可以先把要寫的日志,統一存內存,然后單線程從內存取數據,寫到文本。當然,寫內存也可能會多線程并發,這個時候,我們就可以把鎖加到寫內存的地方。這里大家就不用擔心了,寫內存的速度是非??斓?,和直接寫文件那差的可不是一兩個檔次的問題了。
我們剛才說存內存,怎么存?當然是存集合了。有個數據類型 Queue 為什么要用它。因為它是隊列,有個特點:先進先出。我們取數據的時候就是去的最早存進去的數據了。
使用:存數據 Queue myQ = new Queue(); myQ.Enqueue("The");//入隊 取數據 var t = myQ.Dequeue(); 直接在取值的時候就把值在隊列中移除了。這樣正好免了我手動移除。
那么,很簡單。我們記日志的時候就先把日志往 Queue 里存,然后單獨開個進程取值存值寫文件里。ok,完事。
剛才說了,我們要加鎖。是的,要加鎖。因為 Queue 并不是線程安全數據。我們在寫數據和讀數據的時候都要加鎖。
static object myLock= new object();...lock (myLock) logQueue.Enqueue(logmede);//存...lock (myLock) var m = logQueue.Dequeue();//取
我之前在網上查資料說不能多線程同時寫入隊列,經測試其實是不能同時讀和寫隊列。所以在Dequeue取的時候也要鎖定同一個對象
思路大體就是這樣了。當然,我們還可以擴展很多的東西。如:定時刪除指定過期日志、分文件大小存儲日志、自動增加的日志文件命名...等等。
寫到這里,估計又有大把大把的人要來批判我了。又在造輪子。日志框架那么多,干嘛還要自己寫。浪費時間.....等。
沒錯,我確實是在造輪子。我不想解釋太多了。累... 大神請略過....
下面給出,我的具體實現代碼。分為四個文件 LogMode 包含文件名、日志內容 LogHelper 存隊列、寫文件 LogConfig 讀取相關配置 LogSave 外部直接調用
using System;using System.Collections.Generic;using System.ComponentModel;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Web;namespace CommonLib.HiLog{ /// <summary> /// 日志模型 /// </summary> internal class LogModel { #region logFileName PRivate string _logFileName; /// <summary> /// 日志文件名字 /// </summary> public string logFileName { get { return _logFileName + "_" + DateTime.Now.ToString("yyyyMMdd"); } set { _logFileName = value; } } #endregion #region logMessg private string _logMessg; /// <summary> /// 日志內容 /// </summary> public string logMessg { get { return "----begin-------" + DateTime.Now.ToString() + "----Queue.Count:" + LogHelper.LogQueue.Count + "-----------------------------------/r/n/r/n" + _logMessg + "/r/n/r/n----end----------" + DateTime.Now.ToString() + "----Queue.Count:" + LogHelper.LogQueue.Count + "-----------------------------------" + "/r/n/r/n/r/n"; } set { _logMessg = value; } } #endregion }}
using System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Text;using System.Threading;using System.Threading.Tasks;namespace CommonLib.HiLog{ /// <summary> /// 日志操作輔助類 /// zhaopeiym@163.com /// 創建20150104 修改20151003 /// </summary> internal class LogHelper { /// <summary> /// 消息隊列 /// </summary> private static Queue<LogModel> logQueue = new Queue<LogModel>(); /// <summary> /// 消息隊列 對外只讀 /// </summary> public static Queue<LogModel> LogQueue { get { return LogHelper.logQueue; } } /// <summary> /// 標志鎖 /// </summary> static string myLock = "true"; /// <summary> /// 寫入日志文件(異步單線程 記錄日志) /// </summary> /// <param name="logmede"></param> public static void logWrite(LogModel logmede) { // 這里需要鎖上 不然會出現:源數組長度不足。請檢查 srcIndex 和長度以及數組的下限。異常 //網上有資料說 http://blog.csdn.net/greatbody/article/details/26135057 不能多線程同時寫入隊列 //其實 不僅僅 不能同時寫入隊列 也不能同時讀和寫如隊列 所以 在Dequeue 取的時候也要鎖定一個對象 lock (myLock) logQueue.Enqueue(logmede); logStartWrite(); } /// <summary> /// 文件編碼格式 /// </summary> public static Encoding encoding = Encoding.Default; /// <summary> /// 是否開始自動記錄日志 /// </summary> private static bool isStart = false; /// <summary> /// 用來 標識 最好一次 檢測是否 需要 清理 日志文件 時間 /// </summary> private static DateTime time = DateTime.MinValue; /// <summary> /// 每個日志文件夾 對應的文件下標 /// </summary> private static Dictionary<string, int> logFileNum = new Dictionary<string, int>(); /// <summary> /// 開始把隊列消息寫入文件 /// </summary> private static void logStartWrite() { if (isStart) return; isStart = true; Task.Run(() => { while (true) { if (LogHelper.logQueue.Count >= 1) { LogModel m = null; lock (myLock) m = LogHelper.logQueue.Dequeue(); if (m == null) continue; if (string.IsNullOrEmpty(LogConfig.logFilePath)) throw new Exception("請先初始化日志保存路徑LogModel._logFilePath"); TestingInvalid(); if (!Directory.Exists(LogConfig.logFilePath + m.logFileName + @"/")) Directory.CreateDirectory(LogConfig.logFilePath + m.logFileName + @"/"); // int i = m.logFileNum; if (!logFileNum.Keys.Contains(m.logFileName)) logFileNum.Add(m.logFileName, 0); //部分 日志 文件路徑 string SectionfileFullName = LogConfig.logFilePath + m.logFileName + @"/" + m.logFileName + "_" + logFileNum[m.logFileName].ToString("000") + ".txt"; //最新的寫了內容的 部分 日志文件路徑 string TopSectionfileFullName = SectionfileFullName; // 需要實時更新的 最新日志文件 路徑 string LogfileFullNqme = LogConfig.logFilePath + m.logFileName + @"/" + m.logFileName + ".txt"; FileInfo file = new FileInfo(SectionfileFullName); while (file.Exists && file.Length >= LogConfig.SectionlogFileSize) { TopSectionfileFullName = SectionfileFullName; logFileNum[m.logFileName]++; SectionfileFullName = LogConfig.logFilePath + m.logFileName + @"/" + m.logFileName + "_" + logFileNum[m.logFileName].ToString("000") + ".txt"; file = new FileInfo(SectionfileFullName); } try { if (!file.Exists)//如果不存在 這個文件 就說明需要 創建新的部分日志文件了 { //因為SectionfileFullName路徑的文件不存在 所以創建 File.WriteAllText(SectionfileFullName, m.logMessg, encoding); FileInfo Logfile = new FileInfo(LogfileFullNqme); if (Logfile.Exists && Logfile.Length >= LogConfig.FileSize) //先清空 然后加上 上一個部分文件的內容 File.WriteAllText(LogfileFullNqme, File.ReadAllText(TopSectionfileFullName, encoding), encoding);//如果存在則覆蓋 } else File.AppendAllText(SectionfileFullName, m.logMessg, encoding);//累加 //追加這次內容 到動態更新的日志文件 File.AppendAllText(LogfileFullNqme, m.logMessg, encoding); } catch (Exception ex) { throw ex; } } else { isStart = false;//標記下次可執行 break;//跳出循環 } } }); } /// <summary> /// 檢測 并刪除 之前之外的 日志文件 /// </summary> public static void TestingInvalid() { #region 檢測 并刪除 之前之外的 日志文件 if (time.AddMinutes(LogConfig.TestingInterval) <= DateTime.Now)// 時間內 檢測一次 { try { time = DateTime.Now; List<string> keyNames = new List<string>(); foreach (var logFileName in logFileNum.Keys) { CreatePath(LogConfig.logFilePath + logFileName + @"/"); DirectoryInfo dir = new DirectoryInfo(LogConfig.logFilePath + logFileName + @"/"); if (dir.CreationTime.AddMinutes(LogConfig.DelInterval) <= DateTime.Now)//刪除 設定時間 之前的日志 foreach (var fileInfo in dir.GetFiles()) { if (fileInfo.LastWriteTime.AddMinutes(LogConfig.DelInterval) <= DateTime.Now)//最后修改時間算起 File.Delete(fileInfo.FullName); } if (dir.GetFiles().Length == 0) keyNames.Add(logFileName);//臨時存儲沒有日志文件的文件夾 } foreach (var key in keyNames)//刪除沒有日志文件的文件夾 { logFileNum.Remove(key); Directory.Delete(LogConfig.logFilePath + key + @"/", false); } } catch (Exception ex) { LogSave.ErrLogSave("手動捕獲[檢測并刪除日志出錯!]", ex, "記錄日志出錯"); } } #endregion } #region 創建路徑 /// <summary> /// 創建路徑 /// </summary> /// <param name="paht"></param> /// <returns></returns> public static bool CreatePath(string paht) { if (!Directory.Exists(paht)) { Directory.CreateDirectory(paht); return true; } return false; } #endregion }}
using System;using System.Collections.Generic;using System.Configuration;using System.Data;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Web;namespace CommonLib.HiLog{ /// <summary> /// 日志相關配置 /// </summary> public static class LogConfig { #region 輔助方法 /// <summary> /// GetAppSettings /// </summary> /// <param name="key"></param> /// <returns></returns> public static string GetAppSettings(string key) { if (ConfigurationManager.AppSettings.AllKeys.Contains(key)) return ConfigurationManager.AppSettings[key].ToString(); return string.Empty; } /// <summary> /// 計算字符串 轉 計算結果 /// </summary> /// <param name="v"></param> /// <returns></returns> public static string toCompute(this string v) { return new DataTable().Compute(v, "").ToString(); } #endregion #region 靜態屬性和字段 #region logFilePath 路徑 /// <summary> /// 日志要存的路徑 默認路徑:網站根目錄 + Log 文件夾 /// 在程序第一次啟動是設置 /// </summary> private static string _logFilePath; /// <summary> /// 日志要存的路徑 默認路徑:網站根目錄 + Log 文件夾 /// 在程序第一次啟動是設置 /// </summary> public static string logFilePath { get { if (string.IsNullOrEmpty(_logFilePath)) { try { _logFilePath = HttpContext.Current.Server.MapPath("~/"); } catch (Exception) { try { _logFilePath = System.Windows.Forms.application.StartupPath + @"/"; } catch (Exception) { throw new Exception("請先初始化要保存的路徑:LogModel._logFilePath"); } } } return _logFilePath; } set { _logFilePath = value; } } #endregion #region 檢測間隔時間(分鐘) private static int _TestingInterval; /// <summary> /// 檢測間隔時間(分鐘) 默認:一天 /// 配置:appSettings->Log_TestingInterval 單位:秒 /// </summary> public static int TestingInterval { get { if (_TestingInterval <= 0) { var Log_TestingInterval = GetAppSettings("Log_TestingInterval"); if (string.IsNullOrEmpty(Log_TestingInterval)) _TestingInterval = 1 * 60 * 24; else _TestingInterval = Convert.ToInt32(Log_TestingInterval.toCompute()); } return _TestingInterval; } } #endregion #region 刪除 N分鐘(最后修改時間)之前的的日志 private static int _DelInterval; /// <summary> /// 刪除 N分鐘(最后修改時間)之前的的日志 默認:15天 /// 配置:appSettings->Log_DelInterval 單位:秒 /// </summary> public static int DelInterval { get { if (_DelInterval <= 0) { var Log_DelInterval = GetAppSettings("Log_DelInterval"); if (string.IsNullOrEmpty(Log_DelInterval)) _DelInterval = 1 * 60 * 24 * 15; else _DelInterval = Convert.ToInt32(Log_DelInterval.toCompute()); } return _DelInterval; } } #endregion #region 部分日志文件大小(Byte) private static int _SectionlogFileSize; /// <summary> /// 部分日志文件大小(Byte) 默認:1024Byte * 1024 * 1 = 1MB /// 配置:appSettings->Log_SectionlogFileSize 單位:Byte /// </summary> public static int SectionlogFileSize { get { if (_SectionlogFileSize <= 0) { var Log_SectionlogFileSize = GetAppSettings("Log_SectionlogFileSize"); if (string.IsNullOrEmpty(Log_SectionlogFileSize)) _SectionlogFileSize = 1024 * 1024 * 1; else _SectionlogFileSize = Convert.ToInt32(Log_SectionlogFileSize.toCompute()); } return _SectionlogFileSize; } } #endregion #region 變動文件大小(Byte) private static int _FileSize; /// <summary> /// 變動文件大小(Byte) 默認:1024 * 1024 * 4 = 4M /// 配置:appSettings->Log_FileSize 單位:Byte /// </summary> public static int FileSize { get { if (_FileSize <= 0) { var Log_FileSize = GetAppSettings("Log_FileSize"); if (string.IsNullOrEmpty(Log_FileSize)) _FileSize = 1024 * 1024 * 4; else _FileSize = Convert.ToInt32(Log_FileSize.toCompute()); } return _FileSize; } } #endregion #endregion }}
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Web;namespace CommonLib.HiLog{ /// <summary> /// 異步單線程 /// </summary> public class LogSave { /// <summary> /// 獲得Exception 的詳細信息 /// </summary> /// <param name="ex"></param> /// <returns></returns> public static string GetExceptionInfo(Exception ex) { StringBuilder str = new StringBuilder(); str.Append("錯誤信息:" + ex.Message); str.Append("/r/n錯誤源:" + ex.Source); str.Append("/r/n異常方法:" + ex.TargetSite); str.Append("/r/n堆棧信息:" + ex.StackTrace); return str.ToString(); } /// <summary> /// 系統 自動 捕捉異常 /// 保存異常詳細信息 /// 包括: 瀏覽器 瀏覽器版本 操作系統 頁面 Exception /// </summary> /// <param name="ex"></param> /// <param name="fileName">文件名 默認:SysErr</param> public static void SysErrLogSave(Exception ex, string fileName = null) { StringBuilder str = new StringBuilder(); string ip = ""; if (HttpContext.Current.Request.ServerVariables.Get("HTTP_X_FORWARDED_FOR") != null) ip = HttpContext.Current.Request.ServerVariables.Get("HTTP_X_FORWARDED_FOR").ToString().Trim(); else ip = HttpContext.Current.Request.ServerVariables.Get("Remote_Addr").ToString().Trim(); str.Append("Ip:" + ip); str.Append("/r/n瀏覽器:" + HttpContext.Current.Request.Browser.Browser.ToString()); str.Append("/r/n瀏覽器版本:" + HttpContext.Current.Request.Browser.MajorVersion.ToString()); str.Append("/r/n操作系統:" + HttpContext.Current.Request.Browser.Platform.ToString()); str.Append("/r/n頁面:" + HttpContext.Current.Request.Url.ToString()); str.Append("/r/n" + GetExceptionInfo(ex)); LogHelper.logWrite(new LogModel() { logFileName = "SysErr" + fileName ?? string.Empty, logMessg = str.ToString() }); } /// <summary> /// 異常日志記錄 /// </summary> /// <param name="strmes"></param> /// <param name="ex"></param> public static void ErrLogSave(string strmes, Exception ex, string fileName = null) { StringBuilder str = new StringBuilder(); str.Append(strmes); if (ex != null) str.Append("/r/n" + GetExceptionInfo(ex)); LogHelper.logWrite(new LogModel() { logFileName = fileName ?? "Err", logMessg = str.ToString() }); } /// <summary> /// 警告日志記錄 /// </summary> /// <param name="str"></param> public static void WarnLogSave(string str, string fileName = null) { if (str != null && !string.IsNullOrEmpty(str.Trim())) LogHelper.logWrite(new LogModel() { logFileName = fileName ?? "Warn", logMessg = str }); } /// <summary> /// 追蹤日志記錄 /// </summary> /// <param name="str"></param> public static void TrackLogSave(string str, string fileName = null) { if (str != null && !string.IsNullOrEmpty(str.Trim())) LogHelper.logWrite(new LogModel() { logFileName = fileName ?? "Track", logMessg = str }); } /// <summary> /// 追蹤日志記錄 /// </summary> /// <param name="str"></param> public static void TrackLogSave(string str) { if (!string.IsNullOrEmpty(str.Trim())) LogHelper.logWrite(new LogModel() { logFileName = "SqlTrack", logMessg = str }); } }}
寫好之后,下次我在別的項目里面就直接引用。
如果你使用的是EF,那么我再告訴你一個小秘密。 DbContext 中的 Database.Log 可以直接記錄所有EF執行的sql語句和參數。
使用如: dbContext.Database.Log = LogSave.TrackLogSave; 而LogSave.TrackLogSave我們在上面已經封裝過。
效果圖1 效果圖2
我在一開始就琢磨著怎么統計閱讀量。之前也在http://www.49028c.com/zhaopei/p/4744846.html的最后提出了這個疑問。
遺憾的是,并沒有誰告訴我更好的解決方案。
好吧,靠人不如靠己。還是自己瞎折騰吧。
但是,實現方式還是使用的我自己的提出的“如果實在是找不到好的解決方案,我打算用 IP+系統版本+瀏覽器版本號+.... 作為“聯合主鍵”,如果“主鍵”24小時內重復兩次以上,則不統計,如果cookie存在也不統計。”
1、我們在每次瀏覽器訪問的時候都種下cookie,并設置過期時間為24小時。下次,瀏覽器訪問的時候。我們檢測如果存在我們種下的cookie。則直接忽略。
2、如果沒有帶上我們的cookie。我們就先組合“聯合主鍵”。然后檢測24小時內的記錄有沒有這個“聯合主鍵”。如果有,則忽略,否則在原有閱讀量的基礎上加一,然后存入“聯合主鍵”。
這里的"聯合主鍵"有個小技巧。大家肯定都發現了,這個主鍵有點長。存數據庫有點浪費空間(我數據庫本來就只有50M),然后查詢檢索應該也會慢些吧(并不清楚)。我們想想,其實我們要的不是這么長一串東東。其實,我們只要得到這串東西代表的唯一性就可以了。那么我們可以用到md5,咱不管你是1G、2G還是高清或是無碼。統統給你返回一定長度字符串(我取的是16位小寫)。
隨著數據的增加,這個統計閱讀量的表數據,肯定是所有表中最大的。然而,我們統計閱讀量是在,點擊訪問文章的時候,然后在統計閱讀量這個環節卡太久,給人的感覺就是這個頁面訪問太慢,體驗不好。
然而,我們每次統計都需要檢測數據庫里面是否存在,且數據量還不小。那我們只有再開個進程來做統計。
具體實現代碼:
#region 判斷是否閱讀過 如果沒有 這在BlogReadInfo 插入一條標識信息private bool IsRead(Blogs.ModelDB.Blogs blogobj, string md5){ if (blogobj.BlogReadInfo.Where(t => t.MD5 == md5 && t.LastTime.AddHours(24) > DateTime.Now).Count() > 0) return true; else { //BLL. blogobj.BlogReadInfo.Add(new Blogs.ModelDB.BlogReadInfo() { MD5 = md5, IsDel = false, BlogsId = blogobj.Id, CreateTime = DateTime.Now, UpTime = DateTime.Now, LastTime = DateTime.Now }); return false; }}#endregion
#region 統計閱讀量 異步調用方法delegate void SaveReadDelegate(ModelDB.Blogs blogobj, string md5);private void SaveReadNum(ModelDB.Blogs blogobj, string md5){ LogSave.TrackLogSave(GetUserDistinguish(Request, false), "ReadBlogLog"); var isup = true; BLL.BlogsBLL blogbll = new BLL.BlogsBLL(); var blogtemp = blogbll.GetList(t => t.Id == blogobj.Id, isAsNoTracking: false).FirstOrDefault(); if (blogtemp.BlogReadNum == null) blogtemp.BlogReadNum = 1; else if (!IsRead(blogtemp, md5)) blogtemp.BlogReadNum++; else isup = false; if (isup) BLL.BlogCommentSetBLL.StaticSave();}
#region 獲取客戶端標識(偽)/// <summary>/// 獲取客戶端標識 用來判斷是否已經閱讀過此文章/// </summary>/// <param name="requestt"></param>/// <param name="IsMD5">是否已經md5加密</param>/// <returns></returns>private string GetUserDistinguish(HttpRequestBase requestt, bool IsMD5 = true){ //request StringBuilder str = new StringBuilder(); string ip = ""; if (requestt.ServerVariables.AllKeys.Contains("HTTP_X_FORWARDED_FOR") && requestt.ServerVariables.Get("HTTP_X_FORWARDED_FOR") != null) ip = requestt.ServerVariables.Get("HTTP_X_FORWARDED_FOR").ToString().Trim(); else ip = requestt.ServerVariables.Get("Remote_Addr").ToString().Trim(); str.Append("Ip:" + ip); str.Append("/r/n瀏覽器:" + requestt.Browser.Browser.ToString()); str.Append("/r/n瀏覽器版本:" + requestt.Browser.MajorVersion.ToString()); str.Append("/r/n操作系統:" + requestt.Browser.Platform.ToString()); str.Append("/r/n頁面:" + requestt.Url.ToString()); //str.Append("客戶端IP:" + requestt.UserHostAddress); str.Append("/r/n用戶信息:" + User); str.Append("/r/n瀏覽器標識:" + requestt.Browser.Id); str.Append("/r/n瀏覽器版本號:" + requestt.Browser.Version); str.Append("/r/n瀏覽器是不是測試版本:" + requestt.Browser.Beta); //str.Append("<br/>瀏覽器的分辨率(像素):" + Request["width"].ToString() + "*" + Request["height"].ToString());//1280/1024 str.Append("/r/n是不是win16系統:" + requestt.Browser.Win16); str.Append("/r/n是不是win32系統:" + requestt.Browser.Win32); if (IsMD5) return str.ToString().GetMd5_16(); else return str.ToString();}#endregion
(當然,這個方式統計也不一定準。請求頭信息改改就被偽造了。)
然后我們通過委托從線程池抓去線程異步調用
//........................異步調用....................new SaveReadDelegate(SaveReadNum).BeginInvoke(blogobj, GetUserDistinguish(Request), null, null);
ok,統計完事。
如果您對本篇文章感興趣,那就麻煩您點個贊,您的鼓勵將是我的動力。
當然您還可以加入QQ群:469075305討論。
如果您有更好的處理方式,希望不要吝嗇賜教。
一步步開發自己的博客 .NET版系列:http://www.49028c.com/zhaopei/tag/Hi-Blogs/
本文鏈接:http://www.49028c.com/zhaopei/p/4887573.html
朋友找工作,資深.NET高級工程師。博文鏈接:http://www.49028c.com/CreateMyself
“賣身契:本人正找工作中,工作地點:深圳,方向:.NET。欲知更多詳細內容請與我聯系QQ(2752154844)或留下你的聯系方式,非誠勿擾。”
新聞熱點
疑難解答