分布式服务器框架之Servers.Core中 实现Log模块设计 写入MongoDB数据库

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的情况,后来查了一会修好了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值