原文:
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
{
} chrHandler;
具体回调运行过程在WorldSession中:
void WorldSession::HandleCharEnum(QueryResult * result)
{
}
总体上的调用流程是上这样的:
在游戏主线程的循环体中会调用世界对象sWorld的update方法,其中做了下面这些事
来看一下结果队列的update方法
void SqlResultQueue::Update()
{
}
CharacterDatabase中的异步sql实现:
上面大致介绍了异步sql的执行流程,现在看一下它是如何实现的
1.异步sql执行线程
CharacterDatabase在初始化时会开启一个专门用于执行异步sql的线程
void DatabaseMysql::InitDelayThread()
{
(this);
}
下面是这个线程的类图设计:
类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()
{
}
SqlOperation的类图设计
SqlOperation是抽象类,具体操作如查询操作SqlQuery实现了它。
成员m_callback是回调函数接口
m_queue则是sql语句执行完毕后生成的结果队列,同样是线程安全的
前面已经提到过,显而易见,sql执行线程中主要用到SqlOperation的Execute方法。
void SqlQuery::Execute(Database *db)
{
}
前文不知不觉中多次用了两个概念: 回调对象,结果队列
其实回调对象是真正的精华所在,而结果队列也只是建立在回调对象之上的东东,其设置和使用也值得一提
看一下SqlQuery的构造函数
后面二个对象分别就是回调对象,结果队列。
回调对象内容比较多,先来看看结果队列:
LockedQueue是个模板类,用于多线程的queue,SqlResultQueue继承LockedQueue以IQueryCallback为容器对象,ACE_Thread_Mutex为锁类型的模板实例,它还包含一个Update方法。
void SqlResultQueue::Update()
{
}
之前提到过主线程中某处调用了UpdateResultQueue();
对就是在这里调用SqlResultQueue::Update方法
也许聪明的你会发现,主线程调用UpdateResultQueue,往ResultQueue放回调对象的是sqlDelayThread,
多个线程共用了ResultQueue,那么另一个线程(非主线程和sqlDelayThread)也许也有需要使用异步sql,而sqlDelayThread是否可以被共享呢?
有了这样的需求,DataBaseMysql有如下的设计
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()
{
}
void Database::SetResultQueue(SqlResultQueue * queue)
{
}
回过头来看一下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) \
回调对象是最为精髓的地方,因为它能将一个包含一些回调方法的类(它继承任何其他类)封装为回调接口MaNGOS::IQueryCallback * callback 的实例。
template<class Class, typename ParamType1>
bool
Database::AsyncQuery(Class *object, void (Class::*method)(QueryResult*, ParamType1), ParamType1 param1, const char *sql)
{
}
AsyncQuery的重载方法是个template方法
第一个参数根据模板的不同而不同, 但使用是不用指定<class Class>,第一个参数直接放入某个类的执政,
c++会在编译时生成用这个类的执政作为第一个参数的方法代码。
第二个参数是相应第一个参数指定的类型域的某个方法,它以QueryResult作为第一个参数,后面可以是多个模板参数ParamType1,ParamType2,也可以没有模板参数
这里列出的只有ParamType1,这是由于本文开头例子用的是
CharacterDatabase.AsyncPQuery(&chrHandler, &CharacterHandler::HandleCharEnumCallback, GetAccountId(),"sql format", PET_SAVE_AS_CURRENT, GetAccountId());
因为CharacterHandler::HandleCharEnumCallback需要ParamType1这个参数来找到属于自己的会话
void HandleCharEnumCallback(QueryResult * result, uint32 account)
当有如上两条语句是,c++根据模板类自动生成相应的代码。
下面解读AsyncQuery中的这条语句:
m_threadBody->Delay(new SqlQuery(sql, new MaNGOS::QueryCallback<Class, ParamType1>(object, method, (QueryResult*)NULL, param1), itr->second));
关键是new SqlQuery的第二个参数
看一下其原型
第二个参数是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的原型:
放入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 > >{
};
那上面的模板参数类型与原型参数类型比对
第一个参数类型:
Class *object
第二个参数类型:
void (Class::*method)(QueryResult*, ParamType1)
第三个是具体参数类型:
QueryResult* result
第四个参数类型:
ParamType1 param1
只有第二个参数类型需要研究一下, _Callback 的定义
template < class Class, typename ParamType1, typename ParamType2 >
class _Callback < Class, ParamType1, ParamType2 >
{
};
_Callback < Class, QueryResult*, ParamType1 >::Method
以实例化模板中类型
void (Class::*Method)(ParamType1, ParamType2)
所以Method 就是 void (Class::*method)(QueryResult*, ParamType1)
这里复杂性在于它用一个模板参数实例化另一个模板
讲的有点乱,画个图说明一下:
首先模板实例化的是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方法了。
写这个真累,虽然知道是写给自己看的。。。。。。。。。。。