分布式服务器框架之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的情况,后来查了一会修好了。

相关文章
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
2093 0
分布式爬虫框架Scrapy-Redis实战指南
|
NoSQL MongoDB 数据库
数据库数据恢复—MongoDB数据库数据恢复案例
MongoDB数据库数据恢复环境: 一台操作系统为Windows Server的虚拟机上部署MongoDB数据库。 MongoDB数据库故障: 工作人员在MongoDB服务仍然开启的情况下将MongoDB数据库文件拷贝到其他分区,数据复制完成后将MongoDB数据库原先所在的分区进行了格式化操作。 结果发现拷贝过去的数据无法使用。管理员又将数据拷贝回原始分区,MongoDB服务仍然无法使用,报错“Windows无法启动MongoDB服务(位于 本地计算机 上)错误1067:进程意外终止。”
|
缓存 NoSQL Linux
在CentOS 7系统中彻底移除MongoDB数据库的步骤
以上步骤完成后,MongoDB应该会从您的CentOS 7系统中被彻底移除。在执行上述操作前,请确保已经备份好所有重要数据以防丢失。这些步骤操作需要一些基本的Linux系统管理知识,若您对某一步骤不是非常清楚,请先进行必要的学习或咨询专业人士。在执行系统级操作时,推荐在实施前创建系统快照或备份,以便在出现问题时能够恢复到原先的状态。
1366 79
|
存储 缓存 监控
【YashanDB数据库】数据库运行正常,日志出现大量错误metadata changed
数据库运行正常,日志出现大量错误metadata changed
|
11月前
|
SQL 存储 监控
SQL日志优化策略:提升数据库日志记录效率
通过以上方法结合起来运行调整方案, 可以显著地提升SQL环境下面向各种搜索引擎服务平台所需要满足标准条件下之数据库登记作业流程综合表现; 同时还能确保系统稳健运行并满越用户体验预期目标.
497 6
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
1954 5
|
存储 NoSQL MongoDB
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
499 8
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
|
存储 消息中间件 Kafka
聊一聊日志背后的抽象
本文从思考日志的本质开始,一览业界对日志使用的最佳实践,然后尝试给出分布式存储场景下对日志模块的需求抽象,最后是技术探索路上个人的一点点感悟。
897 81
|
运维 NoSQL 容灾
告别运维噩梦:手把手教你将自建 MongoDB 平滑迁移至云数据库
程序员为何逃离自建MongoDB?扩容困难、运维复杂、高可用性差成痛点。阿里云MongoDB提供分钟级扩容、自动诊断与高可用保障,助力企业高效运维、降本增效,实现数据库“无感运维”。
|
存储 关系型数据库 数据库
【赵渝强老师】PostgreSQL数据库的WAL日志与数据写入的过程
PostgreSQL中的WAL(预写日志)是保证数据完整性的关键技术。在数据修改前,系统会先将日志写入WAL,确保宕机时可通过日志恢复数据。它减少了磁盘I/O,提升了性能,并支持手动切换日志文件。WAL文件默认存储在pg_wal目录下,采用16进制命名规则。此外,PostgreSQL提供pg_waldump工具解析日志内容。
1156 0

热门文章

最新文章

推荐镜像

更多