游戏服务器中都需要用到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
{
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
{
///
/// 日志等级
///
public LoggerLevel Level;
///
/// 日志分类
///
public int Category;
///
/// 日志内容
///
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
{
///
/// 日志策略
///
public class LoggerTactics
{
///
/// 数量
///
public int Count;
///
/// 间隔(秒)
///
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
{
///
/// 日志管理器
///
public sealed class LoggerMgr
{
private static object lock_obj = new object();
///
/// 日志队列
///
private static Queue m_LoggerQueue;
///
/// 日志临时列表
///
private static List m_LoggerList;
///
/// 上次保存时间
///
private static DateTime m_PrevSaveTime;
///
/// 保存策略
///
private static LinkedList m_LoggerTactics;
///
/// 初始化
///
public static void Init()
{
m_PrevSaveTime = DateTime.UtcNow;
m_LoggerQueue = new Queue();
m_LoggerList = new List();
m_LoggerTactics = new LinkedList();
//队列里>=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;
///
/// 当前的MongoClient
///
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 添加日志
///
/// 添加日志
///
/// 日志等级
/// 分类
/// 消息体
/// 参数
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 记录日志
///
/// 记录日志
///
/// 日志等级
/// 分类
/// 消息体
/// 参数
public static void Log(LoggerLevel level = LoggerLevel.Log, int category = 0, string message = null, params object[] args)
{
Log(level, category, true, message, args);
}
///
/// 记录日志
///
/// 日志等级
/// 分类
/// 是否异步
/// 消息体
/// 参数
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
///
/// 停服的时候写入DB
///
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
///
/// 检查是否可以写入DB
///
///
private static bool CheckCanSave()
{
LinkedListNode 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
{
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的情况,后来查了一会修好了。