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

简介: 游戏服务器中都需要用到Log模块,log模块存在的意义第一个是将log输出到控制台又或者是写入到log文件中,出了BUG方便定位;第二是常用于将用户的数据(例如玩家登录、道具购买量)将这种log统计到数据库中,方便统计用户留存信息、数据分析等。

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

相关文章
|
10月前
|
SQL 关系型数据库 数据库
Python SQLAlchemy模块:从入门到实战的数据库操作指南
免费提供Python+PyCharm编程环境,结合SQLAlchemy ORM框架详解数据库开发。涵盖连接配置、模型定义、CRUD操作、事务控制及Alembic迁移工具,以电商订单系统为例,深入讲解高并发场景下的性能优化与最佳实践,助你高效构建数据驱动应用。
1085 7
|
存储 Oracle 关系型数据库
服务器数据恢复—光纤存储上oracle数据库数据恢复案例
一台光纤服务器存储上有16块FC硬盘,上层部署了Oracle数据库。服务器存储前面板2个硬盘指示灯显示异常,存储映射到linux操作系统上的卷挂载不上,业务中断。 通过storage manager查看存储状态,发现逻辑卷状态失败。再查看物理磁盘状态,发现其中一块盘报告“警告”,硬盘指示灯显示异常的2块盘报告“失败”。 将当前存储的完整日志状态备份下来,解析备份出来的存储日志并获得了关于逻辑卷结构的部分信息。
|
10月前
|
存储 NoSQL 前端开发
【赵渝强老师】MongoDB的分布式存储架构
MongoDB分片通过将数据分布到多台服务器,实现海量数据的高效存储与读写。其架构包含路由、配置服务器和分片服务器,支持水平扩展,结合复制集保障高可用性,适用于大规模生产环境。
652 1
|
存储 NoSQL MongoDB
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
461 8
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
|
SQL 存储 关系型数据库
MySQL功能模块探秘:数据库世界的奇妙之旅
]带你轻松愉快地探索MySQL 8.4.5的核心功能模块,从SQL引擎到存储引擎,从复制机制到插件系统,让你在欢声笑语中掌握数据库的精髓!
389 26
|
11月前
|
存储 数据库 开发者
Python SQLite模块:轻量级数据库的实战指南
本文深入讲解Python内置sqlite3模块的实战应用,涵盖数据库连接、CRUD操作、事务管理、性能优化及高级特性,结合完整案例,助你快速掌握SQLite在小型项目中的高效使用,是Python开发者必备的轻量级数据库指南。
881 0
|
Oracle 关系型数据库 数据库
数据库数据恢复—服务器异常断电导致Oracle数据库报错的数据恢复案例
Oracle数据库故障: 某公司一台服务器上部署Oracle数据库。服务器意外断电导致数据库报错,报错内容为“system01.dbf需要更多的恢复来保持一致性”。该Oracle数据库没有备份,仅有一些断断续续的归档日志。 Oracle数据库恢复流程: 1、检测数据库故障情况; 2、尝试挂起并修复数据库; 3、解析数据库文件; 4、导出并验证恢复的数据库文件。
|
数据库
【YashanDB知识库】数据库获取时间和服务器时间不一致
【YashanDB知识库】数据库获取时间和服务器时间不一致
|
监控 关系型数据库 MySQL
如何解决 MySQL 数据库服务器 CPU 飙升的情况
大家好,我是 V 哥。当 MySQL 数据库服务器 CPU 飙升时,如何快速定位和解决问题至关重要。本文整理了一套实用的排查和优化套路,包括使用系统监控工具、分析慢查询日志、优化 SQL 查询、调整 MySQL 配置参数、优化数据库架构及检查硬件资源等步骤。通过一个电商业务系统的案例,详细展示了从问题发现到解决的全过程,帮助你有效降低 CPU 使用率,提升系统性能。关注 V 哥,掌握更多技术干货。
2268 0
|
NoSQL MongoDB 数据库
数据库数据恢复—MongoDB数据库数据恢复案例
MongoDB数据库数据恢复环境: 一台操作系统为Windows Server的虚拟机上部署MongoDB数据库。 MongoDB数据库故障: 工作人员在MongoDB服务仍然开启的情况下将MongoDB数据库文件拷贝到其他分区,数据复制完成后将MongoDB数据库原先所在的分区进行了格式化操作。 结果发现拷贝过去的数据无法使用。管理员又将数据拷贝回原始分区,MongoDB服务仍然无法使用,报错“Windows无法启动MongoDB服务(位于 本地计算机 上)错误1067:进程意外终止。”