ET服务器框架学习笔记(十八)

ET服务器框架学习笔记(十八)

文章目录


前言

本篇主要梳理ET服务器框架中的数据库相关组件。ET服务器框架所用的数据库为MongoDB,内部库包含了很多好用的东西,比如Bson序列化,自动排序等,非常适合作为游戏数据库使用。


一、DBTaskQueue

这个类需要用于管理所有DB任务相关的队列。

1.DBTask

抽象类,所有DB任务的基类,具有一个Run抽象方法,带有的返回值是ETTASK,可以异步执行。

2.DBTaskQueue

  • Queue queue:用于管理DBTask的队列,先进先出。由于ET框架使用的是单线程,所以需要使用这种方式对DB任务进行处理。
  • Add(DBTask task):往队列里面增加一个task
  • ETTask Get():从队列里面获取一个task.
    备注:
    1.当使用ADD时,会先判断一下this.tcs是否为空,如果不为空,那么说明有其他地方在queue队列为空时get一个任务,get为一个异步操作,如果队列为空,那么返回一个本实例tcs的ETTASK,让get的函数异步等待。
    2.当后续有任务add时,会判断本实例tcs是否为空,不为空则有地方在等待get一个任务。那么直接将新增的task设置为tcs的结果,这样异步get会被唤醒,获取最新的Task。
    这里不得不说一句,ETTASK真好用,如果按照往常的写法,要写好几个回调函数,而且需要传来传去。

3.DBTaskQueue的扩展StartAsync方法

在DBTaskQueue组件添加时,awake方法中调用。主要逻辑就是开启循环,然后内部使用Get方法获取DBTask,然后调用task的Run进行处理。这里并没有使用定时器进行定时调用,那么意味着,如果有多条数据库任务,他将优先执行。

while (true)
			{
				if (self.InstanceId != instanceId)
				{
					return;
				}

				DBTask task = await self.Get();

				try
				{
					await task.Run();
				}
				catch (Exception e)
				{
					Log.Error(e);
				}

				task.Dispose();
			}

二、DBComponent

DBComponent用于处理所有任务队列。

1.MongoClient

这个类是由Mongo库提供的一个连接MongoDB的类,使用起来非常方便,具体使用方式请看:
https://www.cnblogs.com/axel10/p/8459996.html

2.IMongoDatabase database

用于获取当前在Mongo中正在使用的数据库,后续的数据库操作基本上都在这个类上执行。MongoClient.GetDatabase(config.DBName)中获取这个类的引用。

3.List tasks

任务队列的列表,ET中一共定有32条DBTaskQueue,如果数量不够的话,有些数据库操作耗时很长,这样其他数据库任务会等待太久才能执行。而数据库本身是多线程的操作,所以可以将任务分为32条,防止全部等待某一条操作耗时太久。

4.DBComponent中的方法

  1. IMongoCollection GetCollection(string name):用于在数据中,通过名字获取某一类集合的所有数据操作对象。
  2. Add(ComponentWithId component, string collectionName = “”):将某个component实例,存入到数据库集合中。如果没有集合名,则以实例类型名作为集合名
  3. AddBatch(List components, string collectionName),批量存入实例。
  4. Get(string collectionName, long id):获取某个集合中某个id的component实例
  5. GetBatch(string collectionName, List idList):批量获取
  6. GetJson(string collectionName, string json):通过JSON字串查询条件获取实例集合。

上面所有方式,内部都是通过构造一个对应的DBTask并将其根据ID取余的方式分配到不同的DBQueue中,下面对这些不同的DBTASK分别熟悉。

5.DBSaveTask

保存一个实例数据到数据库,对应了上面的Add方法
核心方法:await dbComponent.GetCollection(this.CollectionName).ReplaceOneAsync(s => s.Id == this.Component.Id, this.Component, new UpdateOptions {IsUpsert = true});获取DBComponent获取集合实例,然后调用API:ReplaceOneAsync,设置好参数与内容即可。
DBSaveBatchTask与之类似,多了一个For循环

6.DBQueryTask

查询数据库一个实例。
核心方法:IAsyncCursor<ComponentWithId> cursor = await dbComponent.GetCollection(this.CollectionName).FindAsync((s) => s.Id == this.Id); ComponentWithId component = await cursor.FirstOrDefaultAsync();
首先调用API-FindAsync获取一个数据游标,然后调用第二个API-FirstOrDefaultAsync获取真正的数据类。

DBQueryBatchTask与之类似

7.DBQueryJsonTask

通过JSON字符串,传递一个带条件的查询,并返回结果。
核心方法:

FilterDefinition<ComponentWithId> filterDefinition = new JsonFilterDefinition<ComponentWithId>(this.Json);
				IAsyncCursor<ComponentWithId> cursor = await dbComponent.GetCollection(this.CollectionName).FindAsync(filterDefinition);
				List<ComponentWithId> components = await cursor.ToListAsync();

先通过JSon字符串构造一个FilterDefinition过滤定义,然后调用API-FindAsync将过滤定义传进去,获取数据集合游标,再通过游标的.ToListAsync()获取到一个数据实例List。

以上就是所有DBComponent的内容,核心就是对数据库的增删改查,有人可能疑问怎么没有改和删除。其实都可以包含在Add中,调用的ReplaceOneAsync即可保存和修改一个数据库中的值。

三、DBProxyComponent

上面的都是关于DBComponent怎么操作数据库的操作,下面熟悉下DBProxyComponent,这类是封装了所有数据库操作的类,便于其他服务模块直接调用,内部原理是给DB服务模块,发送Session,DB服务模块收到对应的协议,进行各种Handle中,调用DBComponent进行处理实现。

1.保存

  • DBProxyComponent的Save方法

方法比较简单,就是生成一个DBSaveRequest,通过DB服务模块的地址,获取一个与之连接Session,然后发送出去。

Session session = Game.Scene.GetComponent<NetInnerComponent>().Get(self.dbAddress);
await session.Call(new DBSaveRequest { Component = component });
  • DBSaveRequestHandler:对应收到协议的处理方法,
DBComponent dbComponent = Game.Scene.GetComponent<DBComponent>();
	if (string.IsNullOrEmpty(request.CollectionName))
	{
		request.CollectionName = request.Component.GetType().Name;
	}

await dbComponent.Add(request.Component, request.CollectionName);
reply();

SaveBatch就不过多分析了,与上面类似,增加一个循环处理。
还封装了带Session取消的Save,也不过多解释了,详细可以看Session相关的方法。

  • SaveLog:这个方法比较特别,是存入日志的方法,使用了自己定义的Log数据集合名,那么所有相关的东西都会存入到日志集合中。

2.查询

  • Query
string key = typeof (T).Name + id;
			ETTaskCompletionSource<T> tcs = new ETTaskCompletionSource<T>();
			if (self.TcsQueue.ContainsKey(key))
			{
				self.TcsQueue.Add(key, tcs);
				return tcs.Task;
			}
			
			self.TcsQueue.Add(key, tcs);
			self.QueryInner<T>(id, key).Coroutine();
			return tcs.Task;

QueryInner方法内容:

try
			{
				Session session = Game.Scene.GetComponent<NetInnerComponent>().Get(self.dbAddress);
				DBQueryResponse dbQueryResponse = (DBQueryResponse)await session.Call(new DBQueryRequest { CollectionName = typeof(T).Name, Id = id });
				T result = (T)dbQueryResponse.Component;

				object[] tcss = self.TcsQueue.GetAll(key);
				self.TcsQueue.Remove(key);
			
				foreach (ETTaskCompletionSource<T> tcs in tcss)
				{
					tcs.SetResult(result);
				}
			}
			catch (Exception e)
			{
				object[] tcss = self.TcsQueue.GetAll(key);
				self.TcsQueue.Remove(key);
			
				foreach (ETTaskCompletionSource<T> tcs in tcss)
				{
					tcs.SetException(e);
				}
			}

查询使用了一点节省性能的技巧,那就是保存了一个查询的MultiMap<string, object> TcsQueue。当发起一个对某个ID的查询时,又发起了另外一个相同的查询(当结果还没返回时),这时会进入这个TcsQueue,如果服务器返回结果,那么可以同时返回给这两个查询,而不用再去找服务器查询一次。

查询多个,以及带表达式查询,也比较类似,也不详细说明了。

总结

这篇简单梳理了ET中有关服务器框架的内容,下一篇将梳理下ET中HTTP相关内容,最后会使用一个实例贯穿这两块内容的实际用法。

上一篇:ET框架服务端--升级3.1


下一篇:ACCV2020国际细粒度网络图像识别冠军方案解读、经验总结