0.注意事项
- log库和db库分开。
- 缓存队列:减少对硬盘的访问次数,提高效率。
- 异步存储:不能卡游戏主线程。
- 存盘策略:不要所有数据定时一起存,容易出现longtick。
- 避免回档:重要数据立即存盘。
- 大数据量:如何高效存储数据。
- 性能监控:在运行时出现cpu使用率高等情况,要能快速分析。
异常处理
:- 内存爆了。
- 磁盘满了。
- mysql宕机、redis宕机,如何重连?
- db宕机,能否重启?
- redis数据\mysql数据异常时,如何
数据回档
?
1.引用的第3方库
rapidjson:http://rapidjson.org/zh-cn(在组装和解析操作中,使用上很方便。)
hiredis:C++上很好用的访问、操作redis的库。
base64:因为json没法存放bytes类型,所以将bytes用base64转换为string来传递。
2.C++编写的db_sdk
1、提供接口,支持游戏层序列化二进制数据,存储到redis命令中。例如:
const char* sqlStr = "insert t_role(_id,mgr_data,game_data) values('123456789','@','@')";
其中’@'是我实现的占位符,最终结果是将sqlStr和@对应的二进制数据,一起存入到redis的命令队列中。
// 伪代码;
// 1、填充上层游戏数据;
PssSave::Save_Copy copy_data;
copy_data.set_copy_id(101);
copy_data.set_isunlock(true);
copy_data.set_fight_cnt(5);
PssSave::Save_Forge forge_data;
forge_data.set_item_id(20200709);
forge_data.set_isbind(false);
forge_data.set_forge_lev(5);
// 2、序列化到m_buff数组中;
BYTE m_buff[65535] = {0};
int copyLen= copy_data.ByteSize();
copy_data.SerializeToArray(m_buff,copyLen);
int forgeLen = forge_data.ByteSize();
copy_data.SerializeToArray(m_buff+copyLen,forgeLen);
// 3、将二进制数据的地址和长度,记录下来,传参到执行接口;
Params param_pb;
param_pb.bind("mgr_data",(BYTE*)m_buffer,copyLen);
param_pb.bind("game_data",(BYTE*)m_buffer+copyLen,forgeLen);
// 3.1执行驱动接口;
ApiExecuteParamSql(sqlStr,param_pb,NULL,[](bool isSuc,DataTable* result,const char* msg){
// 异步取出结果;
});
// 4、执行接口取出参数,填充占位符,将sqlStr和二进制数据,一起打包成二进制数据,存入redis中(通过%b来存入);
// 总长度=二进制数据长度+字符串长度-减去@所占长度;
bool ApiExecuteParamSql(const char* sqlStr,Params* param_pb,Tags* tag,CallBack cbFunc){
std::vector<GString> sqlVec;
SplitString(sqlStr,"@",sqlVec); // 按照@字符切割sqlStr;
if(sqlVec.size()!=param_pb.size()+1){RETURN_FALSE;} // 数量不能对不上,否则就是游戏层占位符号数量与参数数量不匹配;
int m_SaveLen = strlen(sqlStr) - (sqlVec.size()-1); // 除去@字符的sql语句长度;
for(auto& iter:param_pb){
if(!iter){RETURN_FALSE;}
m_SaveLen += iter->m_len;
}
BYTE* m_SaveBuff = new BYTE[m_Savelen]; // 申请一个总长度的数组,用来存放组装后的二进制数据;
int offset = 0;
for(int idx=0;idx<sqlVec.size();++idx){
if(m_SaveLen <= offset){RETURN_FALSE;} // 偏移量不可能大于总长度,否则就有问题;等于的话,也应该结束了;
int sqlLen = strlen(sqlVec[idx].c_str();
memcpy_s(m_SaveBuff +offset,m_SaveLen-offset,sqlVec[idx].c_str(),sqlLen ));
offset += sqlLen ;
if(idx < param_pb.size()){
memcpy_s(m_SaveBuff +offset,m_SaveLen-offset,param_pb[idx]->m_buff,param_pb[idx]->m_len));
offset += param_pb[idx]->m_len;
}
}
if(offset != m_SaveLen){
SafeDelete(m_SaveBuff); // 长度对不上,本次数据有问题,释放内存;
RETURN_FALSE;
}
const char* redisCmd = "zadd t_role_queue %llu %b";
UINT64 cmdOId = CreateCmdObjID();
m_RedisReply = (redisReply)::redisCommand(m_RedisConnectPtr,redisCmd,cmdOId,m_SaveBuff,m_SaveLen);
//5、解析redis的执行结果;
...
...
//6、命令oid和回调函数映射起来;
// 6.1:这里只是把命令放入到redis命令队列中
// 6.2:后续由python从命令队列中取出命令sql语句,调用mysql执行以后,python将结果集放入到redis结果队列中
// 6.3:SDK层在定时器中判断redis结果队列中是否有内容,有内容就取出结果,调用回调函数,回调结果到游戏层
cmdOId_cbfunc_map[cmdOId] = cbFunc;
}
这里一共提供了3个接口:
ApiExecuteSql() :直接执行一个sql语句。
ApiExecuteParamSql() :执行一个带params的sql语句。
ApiExecuteCache() :执行带缓存功能的sql语句(会先从缓存中进行CRUD)。
2、多线程
- 根据表名,取出对应的线程名
- 根据线程名,取出对应的队列名,本次cmd会被放入该队列中。
- 该队列中不同表的操作cmd,会互相影响。一个阻塞了,队列后的cmd需要等待。
- 队列间的cmd,互不影响,t_role队列的cmd卡住了,不影响t_guild队列。
3.py脚本
多线程运行,从命令队列中取出数据
插件1.pymysql
1.execute()
1.导入头文件:from pymysql.constants import CLIENT
2.在pymysql.connect()的时候,加上client_flag=CLIENT.MULTI_STATEMENTS标记就可以执行多行sql。
- executemany()
- 支持对同一条sql语句,传输一组参数。这样在批量执行的时候,效率非常高。因为mysql会预先解析sql语句,后续的传输,只传入参数。相当于进行了一次预编译。
4.redis集群,一致性hash算法
- 增加节点
- 删除节点
结果:
{
"_id": "639876213569856",
"BD": "2020-12-18 19:25:36",
"game_data": "LK3AJ3G57HG8KJKI9EXCBO5YGNHMOPJUG=="
}
5.缓存
缓存队列
:t_player表在redis的缓存。
缓存最大长度
:例如配置1000,则缓存1000个活跃玩家的数据。
等待同步队列
:dbsdk在执行cache的update操作时,会往该队列加入player_oid
活跃队列
:记录了数据player_oid和数据加入队列的时间戳,方便计算出最旧的那条数据
- 缓存有两个来源:
- py脚本的Select、Insert操作
- dbsdk的CacheUpdate操作
- py组装了一条新的缓存数据,此时需要判断一下
[缓存队列]
是否超过[缓存最大长度]
。- 超过
[缓存最大长度]
,并且最旧的那条数据,不在[等待同步队列]
中,那么把最旧的数据移除掉。 - 未超过
[缓存最大长度]
,那么直接把新数据写入到[缓存队列]
中,并且往[活跃队列]
中添加一条记录。
- 超过
- dbsdk在执行CacheUpdate操作时,在命中缓存的情况下,会将最新数据更新到redis
[缓存队列]
中,同时往[等待同步队列]
中插入一条记录。 - 在
[等待同步队列]
中的数据,表示还没写过mysql。后续由python脚本定时写入到mysql表中。写成功以后,从[等待同步队列]
中移除。
2.写缓存
6.相关机制
多线程并发
业务请求太多了,处理不过来怎么办?- 线程可配置
业务复杂度不一样,日志、战报数据量巨大,请求非常频繁。养成系统的请求非常少。
- 线程可配置
缓存
a. 业务请求太多了,处理不过来怎么办?
b. 需求尖刺,在某个瞬间发生大量业务请求。- 请求缓存
- 结果缓存
- 数据缓存
异常处理
- 重入
难免会遇到error、异常,那么命令应该怎么处理? - 重连
和mysql、redis断开连接后,怎么重连。
- 重入
集群
设计目标是为了支持:全球同服。- 多进程响应服务(115服、116服可以都连同一个db处理)
- 分布式处理队列(115服的业务请求,可以分发到不同线程处理)
性能分析
写完以后,效率怎么样?容灾
[(game ~ dbsdk.lib
) >>redis
] >>python
>>mysql
[(game ~ dbsdk.lib
) <<redis
] <<python
<<mysql
- mysql挂掉了
- python是最先感知到的,返回一个错误码到redis队列中。game通过dbsdk.lib取出错误码。开始处理异常:先逐渐踢掉在线玩家,避免产生新数据。将当前的最新数据写入到redis中,此时python也要退出搬运工作状态,等待mysql恢复后,再消化redis命令。
- mysql出问题了,python脚本如果继续工作的话,会造成数据丢失。所以当pymysql调用execute()执行失败时,需要将命令重新push到redis中。支持重入的功能。
- python挂掉了
- 这种情况反而是所有挂掉中,最容易处理的。只需要重启python脚本就可以了。但是如果是执行某个命令时,python触发异常,突发暴毙。那么这一条命令理论上会丢失,所以取命令时,必须使用
brpoplpush
接口。等命令成功被处理后,再从备份队列中移除命令。 - python脚本启动时,优先检查备份队列中是否有命令。如果有,表明之前发生了异常,那么优先处理备份队列中的命令。
- 这种情况反而是所有挂掉中,最容易处理的。只需要重启python脚本就可以了。但是如果是执行某个命令时,python触发异常,突发暴毙。那么这一条命令理论上会丢失,所以取命令时,必须使用
- redis挂掉了
- 那么dbsdk.lib会第一时间感知到,返回错误码给到game。同样的,game开始做踢人处理。但此时game不能立即退出,因为最新的数据在game内存中,还没有到redis缓存中。所以game需要保持运行,等待redis重启。
- python脚本也会感知到redis宕掉了,只要做好重连功能即可。
- game挂掉了
-
这就属于上层业务策略了,存盘分为3个队列。
高优先级,立即存盘。
(充值的)中优先级,5分钟存盘1次。
(在线的,存home+player)低优先级,10分钟存盘1次。
(离线的,存home)
目的就是保证存盘数据完整性,不能丢失玩家数据。
-
- mysql挂掉了