Mangos之异步数据库查询


原文:

http://blog.sina.com.cn/s/blog_6c23c66c0100o7xh.html

 

为什么需要异步数据库查询?

来看一下如果两个执行顺序:

顺序1:

执行sql语句1;

对应sql语句1结果执行的动作;

执行sql语句2;

对应sql语句2结果执行的动作;

。。。。。。。。。。。

顺序2:

在线程1中

执行sql语句1;

执行sql语句2;

.........

在线程2中

添加sql语句1到线程1;

添加sql语句2到线程1;

.......... // 线程循环

对应sql语句2结果执行的动作; //次序可以是随机的,只要sql语句结果返回就对其做相应的动作

对应sql语句1结果执行的动作;

可见,顺序2有一下优点

性能更

高类似的操作集中执行

响应更快

顺序1中执行一次sql语句后紧跟着相应的处理动作,如果当前sql语句耗时很长,下一个sql语句耗时又很短,那么下一个耗时不长的sql语句的相应方法就必须等待当前sql语句及相应的处理动作执行完后才能执行.

在Mangos中,对数据库characters的操作就使用了异步sql,大概是因为mangos对于该数据库的操作比较平凡且响应速度要求比较高,设想上千个玩家登陆服务器,每时每刻的变化都要保存到characters数据库中,并且其他玩家变化及时地反映到游戏世界中。

先来看一下mangos中异步sql的使用方法:

void WorldSession::HandleCharEnumOpcode( WorldPacket & )
{
//get all the data necessary for loading all characters (along with their pets) on the account
//第一个参数&chrHanler是包含一组回调方法的类对象

//第二个参数则是该类中某个回调方法,这个回调方法以sql语句执行的结果QueryResult作为参数

//第三个参数用作回调方法的第二个参数

//第四个参数是sql语句的format形式

//下面的多个参数是sql语句format中用到的变量

CharacterDatabase.AsyncPQuery(&chrHandler, &CharacterHandler::HandleCharEnumCallback, GetAccountId(),"sql format", PET_SAVE_AS_CURRENT, GetAccountId());
}

来看一下这个专门用来处理角色信息的handler类:

class CharacterHandler
{
public:

//传入AsyncPQuery回调方法的参数有两个: sql的执行结果,和用于定位会话的accountId;

//当sql语句执行完后返回结果准备好后,该方法在某时刻将得到调用
void HandleCharEnumCallback(QueryResult * result, uint32 account)
{
WorldSession * session = sWorld.FindSession(account);
if(!session)
{
delete result;
return;
}

//由于这类操作都是会话相关的,实际方法放在Session中更为简便
session->HandleCharEnum(result);
}
void HandlePlayerLoginCallback(QueryResult * , SqlQueryHolder * holder)
{
if (!holder) return;
WorldSession *session = sWorld.FindSession(((LoginQueryHolder*)holder)->GetAccountId());
if(!session)
{
delete holder;
return;
}
session->HandlePlayerLogin((LoginQueryHolder*)holder);
}
} chrHandler;

具体回调运行过程在WorldSession中:

void WorldSession::HandleCharEnum(QueryResult * result)
{
WorldPacket data(SMSG_CHAR_ENUM, 100); // we guess size

uint8 num = 0;

data << num;

if( result )
{
do
{
uint32 guidlow = (*result)[0].GetUInt32();
sLog.outDetail("Loading char guid %u from account %u.", guidlow, GetAccountId());

//构建角色信息
if(Player::BuildEnumData(result, &data))
++num;
}
while( result->NextRow() );

delete result;
}

data.put<uint8>(0, num);

SendPacket( &data ); //以该会话发送数据
}

总体上的调用流程是上这样的:

在游戏主线程的循环体中会调用世界对象sWorld的update方法,其中做了下面这些事

/// <li> Handle session updates when the timer has passed
if (m_timers[WUPDATE_SESSIONS].Passed())
{
m_timers[WUPDATE_SESSIONS].Reset();

UpdateSessions(diff); //循环每个会话,调用session的update方法,若有数据包,会解密数据包头,拿到opCode,找到并掉用相应的处理方法,比如HandleCharEnumOpcode(WorldPacket& ),它运行了AsyncPQuery将sql语句放入执行队列,CharacterDatabase中的异步sql执行线程会执行该sql,并把执行结果法如结果队列中。
}

。。。。。。。。。。。。。。。。。。。

// execute callbacks from sql queries that were queued recently

//在主线程中此处完成一系列的sql语句执行后的反馈方法
UpdateResultQueue();//这个方法很简单,就执行了m_resultQueue->Update();

来看一下结果队列的update方法

void SqlResultQueue::Update()
{
/// execute the callbacks waiting in the synchronization queue
MaNGOS::IQueryCallback* callback;
while (next(callback)) //循环每个结果队列,拿出对应的反馈方法
{
callback->Execute(); //执行反馈方法,这里很神奇吧,是怎么做到的呢?稍后会讨论
delete callback;
}
}

CharacterDatabase中的异步sql实现:

上面大致介绍了异步sql的执行流程,现在看一下它是如何实现的

1.异步sql执行线程

CharacterDatabase在初始化时会开启一个专门用于执行异步sql的线程

void DatabaseMysql::InitDelayThread()
{
assert(!m_delayThread);

//New delay thread for delay execute
m_threadBody = new

(this); //异步sql执行线程的方法体
m_delayThread = new ACE_Based::Thread(m_threadBody); //这个构造函数将参数赋给Thread的m_task,并启动线程
}

下面是这个线程的类图设计:

Mangos之异步数据库查询

类Thread是对ACE线程操作的wrapper,它包含了启动,关闭线程等功能,其构造函数为要开启的Thread赋予线程执行体并开启线程,具体实现这里就不讨论了,都是ACE Developemnt Guide上找的到的。

关键是SqlDelayThread,线程的方法体。

成员m_dbEngine是DatabaseMysql的实例,DatabaseMysql对mysqlapi做了封装,其中除提供基本sql操作功能外,还包含了像AsyncQuery,transaction相关的方法,这里需要这个引用是为了能执行sql基本操作。

成员m_sqlQueue是个线程安全的sql操作队列,sql语句被封装成SqlOperation之后放入该队列。

方法 Delay(SqlOperation* sql)将sql操作置入操作队列中

线程方法体定义:

void SqlDelayThread::run()
{
#ifndef DO_POSTGRESQL
mysql_thread_init();
#endif

while (m_running)
{
// if the running state gets turned off while sleeping
// empty the queue before exiting
ACE_Based::Thread::Sleep(10);
SqlOperation* s;

//从队列中获取下一个sqlOperation并执行
while (m_sqlQueue.next(s))
{
s->Execute(m_dbEngine);
delete s;
}
}

#ifndef DO_POSTGRESQL
mysql_thread_end();
#endif
}

SqlOperation的类图设计

Mangos之异步数据库查询
SqlOperation是抽象类,具体操作如查询操作SqlQuery实现了它。

成员m_callback是回调函数接口

m_queue则是sql语句执行完毕后生成的结果队列,同样是线程安全的

前面已经提到过,显而易见,sql执行线程中主要用到SqlOperation的Execute方法。

void SqlQuery::Execute(Database *db)
{
if(!m_callback || !m_queue)
return;
/// execute the query and store the result in the callback
m_callback->SetResult(db->Query(m_sql)); //调用sql基本操作,并将结果放入回调对象中
/// add the callback to the sql result queue of the thread it originated from
m_queue->add(m_callback);//将回调对象放入结果队列中。
}

前文不知不觉中多次用了两个概念: 回调对象,结果队列

其实回调对象是真正的精华所在,而结果队列也只是建立在回调对象之上的东东,其设置和使用也值得一提

看一下SqlQuery的构造函数
SqlQuery(const char *sql, MaNGOS::IQueryCallback * callback, SqlResultQueue * queue)

后面二个对象分别就是回调对象,结果队列。

回调对象内容比较多,先来看看结果队列:

Mangos之异步数据库查询

LockedQueue是个模板类,用于多线程的queue,SqlResultQueue继承LockedQueue以IQueryCallback为容器对象,ACE_Thread_Mutex为锁类型的模板实例,它还包含一个Update方法。

void SqlResultQueue::Update()
{
/// execute the callbacks waiting in the synchronization queue
MaNGOS::IQueryCallback* callback;
while (next(callback)) //从结果队列中获取每个callback对象并执行
{
callback->Execute();
delete callback;
}
}

之前提到过主线程中某处调用了UpdateResultQueue();

对就是在这里调用SqlResultQueue::Update方法

也许聪明的你会发现,主线程调用UpdateResultQueue,往ResultQueue放回调对象的是sqlDelayThread,

多个线程共用了ResultQueue,那么另一个线程(非主线程和sqlDelayThread)也许也有需要使用异步sql,而sqlDelayThread是否可以被共享呢?

有了这样的需求,DataBaseMysql有如下的设计

 


Mangos之异步数据库查询

Database为抽象类,DatabaseMysql用mysql api实现了Database

注意成员m_queryQueues是个线程指针和结果队列SqlResultQueue,也即是说DatabaseMysql保存来自各个线程的结果队列,而每个sqlOperation也知道自己所属的结果队列,如

SqlQuery(const char *sql, MaNGOS::IQueryCallback * callback, SqlResultQueue * queue) //第三个参数

所以SqlOperation执行结果会被放入它所属的结果队列中,而sqlDelayThread不区分结果队列地执行每个SqlOperation。

这样就构成了sqlDelayThread为多个线程所共享,每个线程负责创建自己的结果队列,在创建SqlOperation时指定自己的结果队列,并在自己线程的适当地方调用UpdateResultQueue。

看一下World世界对象对结果队列的操作。

void World::InitResultQueue()
{
m_resultQueue = new SqlResultQueue; //创建输入自己线程的结果队列
CharacterDatabase.SetResultQueue(m_resultQueue); //将结果队列设置到CharacterDatabase
}

void Database::SetResultQueue(SqlResultQueue * queue)
{
m_queryQueues[ACE_Based::Thread::current()] = queue; //thread*,resultQueue的map

}

回过头来看一下AsyncQuery这个方法

template<class Class, typename ParamType1>
bool Database::AsyncQuery(Class *object, void (Class::*method)(QueryResult*, ParamType1), ParamType1 param1, const char *sql)

方法体中使用了预定义宏

#define ASYNC_QUERY_BODY(sql, queue_itr) \
if (!sql) return false; \
\
QueryQueues::iterator queue_itr; \
\
{ \
ACE_Based::Thread * queryThread = ACE_Based::Thread::current(); \ //获取当前线程指针
queue_itr = m_queryQueues.find(queryThread); \ //找到改线程指针的结果队列
if (queue_itr == m_queryQueues.end()) return false; \
}

回调对象是最为精髓的地方,因为它能将一个包含一些回调方法的类(它继承任何其他类)封装为回调接口MaNGOS::IQueryCallback * callback 的实例。

template<class Class, typename ParamType1>
bool
Database::AsyncQuery(Class *object, void (Class::*method)(QueryResult*, ParamType1), ParamType1 param1, const char *sql)
{
ASYNC_QUERY_BODY(sql, itr)
return m_threadBody->Delay(new SqlQuery(sql, new MaNGOS::QueryCallback<Class, ParamType1>(object, method, (QueryResult*)NULL, param1), itr->second));
}

AsyncQuery的重载方法是个template方法

第一个参数根据模板的不同而不同, 但使用是不用指定<class Class>,第一个参数直接放入某个类的执政,

c++会在编译时生成用这个类的执政作为第一个参数的方法代码。

第二个参数是相应第一个参数指定的类型域的某个方法,它以QueryResult作为第一个参数,后面可以是多个模板参数ParamType1,ParamType2,也可以没有模板参数

这里列出的只有ParamType1,这是由于本文开头例子用的是

//ParamType1

CharacterDatabase.AsyncPQuery(&chrHandler, &CharacterHandler::HandleCharEnumCallback, GetAccountId(),"sql format", PET_SAVE_AS_CURRENT, GetAccountId());

因为CharacterHandler::HandleCharEnumCallback需要ParamType1这个参数来找到属于自己的会话

void HandleCharEnumCallback(QueryResult * result, uint32 account)
{
WorldSession * session = sWorld.FindSession(account);

当有如上两条语句是,c++根据模板类自动生成相应的代码。

下面解读AsyncQuery中的这条语句:

m_threadBody->Delay(new SqlQuery(sql, new MaNGOS::QueryCallback<Class, ParamType1>(object, method, (QueryResult*)NULL, param1), itr->second));

关键是new SqlQuery的第二个参数

看一下其原型

SqlQuery(const char *sql, MaNGOS::IQueryCallback * callback, SqlResultQueue * queue)

第二个参数是IQueryCallback接口

AsyncQuery中为它赋的是,所以重点解释一下这条语句

new MaNGOS::QueryCallback<Class, ParamType1>(object, method, (QueryResult*)NULL, param1)

为方便,再看一下相应的AsyncQuery原型
template<class Class, typename ParamType1>

bool Database::AsyncQuery(Class *object, void (Class::*method)(QueryResult*, ParamType1), ParamType1 param1, const char *sql)

放入MaNGOS::QueryCallback的第一个参数object

类型为Class *object

值为&chrHandler //class CharacterHandler的对象

具体类型为CharacterHandler

放入MaNGOS::QueryCallback的第二个参数method

类型是void (Class::*method)(QueryResult*, ParamType1)

值为 &CharacterHandler::HandleCharEnumCallback

具体类型为HandleCharEnumCallback的原型:

void HandleCharEnumCallback(QueryResult * result, uint32 account)

放入MaNGOS::QueryCallback的第三个参数NULL

它不是模板参数,只有具体类型为QueryResult,值为NULL

放入MaNGOS::QueryCallback的第四个参数param1

类型ParamType1 param1,值为GetAccountId()返回结果,具体类型是uint32

看一下QueryCallback 的原型:

template < class Class, typename ParamType1 >
class QueryCallback < Class, ParamType1 > :public _IQueryCallback< _Callback < Class, QueryResult*, ParamType1 > >{
private:
typedef _Callback < Class, QueryResult*, ParamType1 > QC1;
public:
QueryCallback(Class *object, typename QC1::Method method, QueryResult* result, ParamType1 param1): _IQueryCallback< QC1 >(QC1(object, method, result, param1)) {}
};

那上面的模板参数类型与原型参数类型比对

第一个参数类型:

Class *object Class *object

第二个参数类型:

void (Class::*method)(QueryResult*, ParamType1) _Callback < Class, QueryResult*, ParamType1 >::Method

第三个是具体参数类型:

QueryResult* result QueryResult* result

第四个参数类型:

ParamType1 param1 ParamType1 param1

只有第二个参数类型需要研究一下, _Callback 的定义

template < class Class, typename ParamType1, typename ParamType2 >
class _Callback < Class, ParamType1, ParamType2 >
{
protected:
typedef void (Class::*Method)(ParamType1, ParamType2);
Class *m_object;
Method m_method;
ParamType1 m_param1;
ParamType2 m_param2;
void _Execute() { (m_object->*m_method)(m_param1, m_param2); }
public:
_Callback(Class *object, Method method, ParamType1 param1, ParamType2 param2)
: m_object(object), m_method(method), m_param1(param1), m_param2(param2) {}
_Callback(_Callback < Class, ParamType1, ParamType2 > const& cb)
: m_object(cb.m_object), m_method(cb.m_method), m_param1(cb.m_param1), m_param2(cb.m_param2) {}
};

_Callback < Class, QueryResult*, ParamType1 >::Method

以实例化模板中类型

void (Class::*Method)(ParamType1, ParamType2)

所以Method 就是 void (Class::*method)(QueryResult*, ParamType1) 这个类型。

这里复杂性在于它用一个模板参数实例化另一个模板

讲的有点乱,画个图说明一下:

Mangos之异步数据库查询

首先模板实例化的是QueryCallback
Class为CharacterHandler
ParamType1为uint32

QueryCallback中又模板实例化_Callback

这样CharacterHandler与_Callback进行了绑定,它是能真正执行代码的类

void _Execute(){(m_object->*m_method)(m_param1,m_param2);}//执行callback方法

QueryCallback继承_IqueryCallback
_IqueryCallback也需要模板实例化,用的模板参数是刚才实例化的callback QC1

_IqueryCallback继承 QC1 和 接口IQueryCallback
其中实现了IQueryCallback的方法

void Execute()(CB:_Execute();) //CB为模板参数,被赋值QC1
void SetResult(QueryResult* result){CB::m_param1 = result;}
QueryResult* GetResult(){return CB::m_param1;}

这样一来QueryCallback就等同于用CharacterHandler的方法实现了IQueryCallback

AsyncPQuery方法体中

new MaNGOS::QueryCallback<Class, ParamType1>(object, method)这个语句后,就等同于new了一个

封装了CharacterHandler的IQueryCallback接口的实例。

在调用时可利用多态的特性调用每个IQueryCallback所绑定的Handler方法了。

MaNGOS::IQueryCallback* callback;
while (next(callback))
{
callback->Execute(); // 怎样,现在来看不再神奇了吧。 若callback绑定的是CharacterHandler则会调用它的回调方法。

delete callback;
}

写这个真累,虽然知道是写给自己看的。。。。。。。。。。。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值