Log模块设计
1.Log模块介绍
游戏服务器中都需要用到Log模块,log模块存在的意义第一个是将log输出到控制台又或者是写入到log文件中,出了BUG方便定位;第二是常用于将用户的数据(例如玩家登录、道具购买量)将这种log统计到数据库中,方便统计用户留存信息、数据分析等。
2.Log 数据库模型类设计
LoggerDBModel 存储MongoClient连接;主要职责是负责指明存放Log的具体数据库和具体数据库表。
LoggerDBModel.cs实现
using MongoDB.Driver; using System; using Servers.Core.YFMongoDB; namespace Servers.Core.Logger { public class LoggerDBModel : YFMongoDBModelBase<LoggerEntity> { protected override MongoClient Client => LoggerMgr.CurrClient; protected override string DatabaseName => "Logger"; protected override string CollectionName => string.Format("Log_{0}", DateTime.UtcNow.ToString("yyyy-MM-dd")); protected override bool CanLogError => false; } }
3.Log实体类设计
LoggerEntity是一个内存实体,存入数据里的时候会把类结构序列化成BSON格式存储到MongoDB中,当要从MongoDB取一个文档的时候,BSON工具会将二进制数据反序列化成一个LoggerEntity类的实例。
LoggerEntity.cs实现
using Servers.Core.YFMongoDB; namespace Servers.Core.Logger { public class LoggerEntity : YFMongoEntityBase { /// <summary> /// 日志等级 /// </summary> public LoggerLevel Level; /// <summary> /// 日志分类 /// </summary> public int Category; /// <summary> /// 日志内容 /// </summary> public string Message; } }
4.Log写入策略类的实现
Log写入DB的时候根据数量和时间双策略决定什么情况,多久写入一次数据库。这样可以最大程度的减缓数据库的写入压力。
可能会存在许多LOG写入DB的策略。例如有这样三条策略:1.当log>=1000条,立即写入;2.log>=100条,30秒写入;3.日志>=1条,120秒写入。
将这三个策略存到链表中,循环时以缓存的log数量优先,数量>=1000直接写入,否则需要等待一定的时间才会将log写入数据库。
LoggerTactics.cs实现
using System; using System.Collections.Generic; using System.Text; namespace Servers.Core.Logger { /// <summary> /// 日志策略 /// </summary> public class LoggerTactics { /// <summary> /// 数量 /// </summary> public int Count; /// <summary> /// 间隔(秒) /// </summary> public int Interval; } }
5.Log 唯一ID 数据库模型类设计
这个类的主要作用和3差不多,记录了连接的Mongo客户端;数据库名称;唯一ID表名称。写入DB时使用Model类缓存的MongoClient与MongoServer进行通信读写数据。
UniqueIDLogger.cs实现
using System; using MongoDB.Driver; using Servers.Core.YFMongoDB; namespace Servers.Core.Logger { public class UniqueIDLogger : YFUniqueIDBase { protected override MongoClient Client => LoggerMgr.CurrClient; protected override string DatabaseName => "Logger"; protected override string CollectionName => "UniqueIDLogger"; } }
6.日志管理器
日志管理器的是一个管理者,做的事情会比较多。会为外部用户提供统一的接口,会先将用户的log先缓存下来然后输出到控制台;根据策略决定是否要将log写入数据库;LoggerModel和UniqueIDLogger两个DB模型类的单例实现;停机写入;
using MongoDB.Driver; using System; using System.Collections.Generic; using Servers.Core.Utils; namespace Servers.Core.Logger { /// <summary> /// 日志管理器 /// </summary> public sealed class LoggerMgr { private static object lock_obj = new object(); /// <summary> /// 日志队列 /// </summary> private static Queue<LoggerEntity> m_LoggerQueue; /// <summary> /// 日志临时列表 /// </summary> private static List<LoggerEntity> m_LoggerList; /// <summary> /// 上次保存时间 /// </summary> private static DateTime m_PrevSaveTime; /// <summary> /// 保存策略 /// </summary> private static LinkedList<LoggerTactics> m_LoggerTactics; /// <summary> /// 初始化 /// </summary> public static void Init() { m_PrevSaveTime = DateTime.UtcNow; m_LoggerQueue = new Queue<LoggerEntity>(); m_LoggerList = new List<LoggerEntity>(); m_LoggerTactics = new LinkedList<LoggerTactics>(); //队列里>=1000条 立刻写入 m_LoggerTactics.AddLast(new LoggerTactics() { Count = 1000, Interval = 0 }); //队列里>=100条 并且 距离上次写入>=60秒 写入 m_LoggerTactics.AddLast(new LoggerTactics() { Count = 100, Interval = 60 }); //队列里>=1条 并且 距离上次写入>=300秒 写入 m_LoggerTactics.AddLast(new LoggerTactics() { Count = 1, Interval = 300 }); Console.WriteLine("LoggerMgr Init Complete"); } #region CurrClient private static MongoClient m_CurrClient = null; /// <summary> /// 当前的MongoClient /// </summary> public static MongoClient CurrClient { get { if (m_CurrClient == null) { lock (lock_obj) { if (m_CurrClient == null) { m_CurrClient = new MongoClient("mongodb://127.0.0.1"); } } } return m_CurrClient; } } #endregion #region LoggerDBModel private static LoggerDBModel m_LoggerDBModel; public static LoggerDBModel LoggerDBModel { get { if (m_LoggerDBModel == null) { lock (lock_obj) { if (m_LoggerDBModel == null) { m_LoggerDBModel = new LoggerDBModel(); } } } return m_LoggerDBModel; } } #endregion #region UniqueIDLogger private static UniqueIDLogger m_UniqueIDLogger; public static UniqueIDLogger UniqueIDLogger { get { if (m_UniqueIDLogger == null) { lock (lock_obj) { if (m_UniqueIDLogger == null) { m_UniqueIDLogger = new UniqueIDLogger(); } } } return m_UniqueIDLogger; } } #endregion #region AddLog 添加日志 /// <summary> /// 添加日志 /// </summary> /// <param name="level">日志等级</param> /// <param name="category">分类</param> /// <param name="message">消息体</param> /// <param name="args">参数</param> public static void AddLog(LoggerLevel level = LoggerLevel.Log, int category = 0, bool async = true, string message = null, params object[] args) { LoggerEntity loggerEntity = new LoggerEntity(); loggerEntity.YFId = UniqueIDLogger.GetUniqueID(category); loggerEntity.Level = level; loggerEntity.Category = category; loggerEntity.Message = args.Length == 0 ? message : string.Format(message, args); loggerEntity.CreateTime = loggerEntity.UpdateTime = DateTime.UtcNow; Console.WriteLine(string.Format("{0} {1} {2}", loggerEntity.Level, loggerEntity.Category, loggerEntity.Message)); if (async) { _ = LoggerDBModel.AddAsync(loggerEntity); } else { LoggerDBModel.Add(loggerEntity); } } #endregion #region Log 记录日志 /// <summary> /// 记录日志 /// </summary> /// <param name="level">日志等级</param> /// <param name="category">分类</param> /// <param name="message">消息体</param> /// <param name="args">参数</param> public static void Log(LoggerLevel level = LoggerLevel.Log, int category = 0, string message = null, params object[] args) { Log(level, category, true, message, args); } /// <summary> /// 记录日志 /// </summary> /// <param name="level">日志等级</param> /// <param name="category">分类</param> /// <param name="async">是否异步</param> /// <param name="message">消息体</param> /// <param name="args">参数</param> public static void Log(LoggerLevel level = LoggerLevel.Log, int category = 0, bool async = true, string message = null, params object[] args) { LoggerEntity loggerEntity = new LoggerEntity(); loggerEntity.YFId = UniqueIDLogger.GetUniqueID(category); loggerEntity.Level = level; loggerEntity.Category = category; loggerEntity.Message = args.Length == 0 ? message : string.Format(message, args); loggerEntity.CreateTime = loggerEntity.UpdateTime = DateTime.UtcNow; Console.WriteLine(string.Format("{0} {1} {2}", loggerEntity.Level, loggerEntity.Category, loggerEntity.Message)); //加入队列 m_LoggerQueue.Enqueue(loggerEntity); lock (lock_obj) { //检查是否可以写入DB if (CheckCanSave()) { m_LoggerList.Clear(); while (m_LoggerQueue.Count > 0) { //循环加入临时列表 m_LoggerList.Add(m_LoggerQueue.Dequeue()); } if (async) { _ = LoggerDBModel.AddManyAsync(m_LoggerList); } else { LoggerDBModel.AddMany(m_LoggerList); } m_PrevSaveTime = DateTime.UtcNow; Console.WriteLine("Logger写入DB完毕"); } } } #endregion #region SaveDBWithStopServer 停服的时候写入DB /// <summary> /// 停服的时候写入DB /// </summary> public static void SaveDBWithStopServer() { m_LoggerList.Clear(); while (m_LoggerQueue.Count > 0) { //循环加入临时列表 m_LoggerList.Add(m_LoggerQueue.Dequeue()); } LoggerDBModel.AddMany(m_LoggerList); m_PrevSaveTime = DateTime.UtcNow; Console.WriteLine("Logger 停服写入DB完毕"); } #endregion #region CheckCanSave 检查是否可以写入DB /// <summary> /// 检查是否可以写入DB /// </summary> /// <returns></returns> private static bool CheckCanSave() { LinkedListNode<LoggerTactics> curr = m_LoggerTactics.First; while (curr != null) { LoggerTactics loggerTactics = curr.Value; long invertal = YFDateTimeUtil.GetTimestamp(DateTime.UtcNow) - YFDateTimeUtil.GetTimestamp(m_PrevSaveTime); //毫秒 if (m_LoggerQueue.Count >= loggerTactics.Count && invertal >= loggerTactics.Interval * 1000) { return true; } curr = curr.Next; } return false; } #endregion } }
7.测试
代码写好了之后需要验证是否可以正常的写入数据,确保实现的功能是没有BUG存在的。测试中测试了两个策略:1.写1000条日志,测试是否立即存入数据里中;2.写100条,测试60秒后是否,测试结果能正常写入。
TestLogger.cs实现
using System; using System.Collections.Generic; using System.Text; using Servers.Core; using Servers.Common; namespace Servers.HotFix { public class TestLogger : Singleton<TestLogger> { public void TestAddLog() { LoggerMgr.AddLog(LoggerLevel.Log, 0, true, "这一AddLog测试"); Console.WriteLine(" TestAddLog execute complete"); } public void Test100Log() { for (int i = 0; i < 100; ++i) { LoggerMgr.Log(LoggerLevel.LogWarning, 0, "TestLog测试log:" + i); } ServerTimer timer = new ServerTimer(ServerTimerRunType.FixedInterval, ondoaction:()=> { LoggerMgr.Log(LoggerLevel.LogWarning, 0, "TestLog测试log:xx"); }, interval:60); TimerManager.RegisterServerTime(timer); Console.WriteLine("测试写入100条log成功,等待60秒后写入"); } public void Test1000Log() { for (int i = 0; i < 1000; ++i) { LoggerMgr.Log(LoggerLevel.LogWarning, 0, "TestLog测试log:" + i); } Console.WriteLine("测试写入1000条log成功,下一帧立即写入"); } public void Test() { //TestAddLog(); Test100Log(); } } }
8.总结
日志模块总共划分了5个类文件实现了Log功能。在实现的过程中,眼戳遇到了一个双重检验的第二层写成了属性了导致发生了StackOverflow的情况,后来查了一会修好了。