. . vim: fileencoding=utf-8 tabstop=2 softtabstop=2 shiftwidth=2 expandtab
15. ORM Part 4: 数据库处理¶
15.1. 事务¶
iFun Engine 的 ORM 通过事件线程处理并且捆绑在一个事件处理程序的数据库工作后使用一个事务来处理。
void OnMyEvent(const Ptr<const MyEvent> &event) {
// transaction 开始
MyFunction();
// transaction 结束
}
void MyFunction() {
// transaction 内
...
}
**iFun Engine 对数据库处理进行 non-blocking** 。 则发生 DB I/O 的时候不会 block 事件处理程序。
如果数据库处理中线程快要停的话,iFun Engine 回滚 正在处理的事务后,可以再数据库处理的时候才 再启动 事务。
回滚 当 Create(...)
/ Fetch(...)
/ Refresh()
处理时不能直接访问对象的时候发生。
在数据库读取对象或者其他事件线程已经使用该对象的情况都属于上面的情况。
Important
使用在 分片数据库服务器 提到的方式进行数据库分片的话,有可能会因发生数据库服务器或者有对象的游戏服务器崩溃而失败写对象的情况。 因此,为了避免这种情况,游戏服务器一定需要检查对象是否null的过程。
15.1.1. 事务回滚¶
下面参数由发生回滚来强制中断事务,以后准备完再实行的时候再进行。 如果事务中断的话,iFun Engine 把在事务里变更的对象回到之前的状态。
接口类的
Fetch()
方法接口类的
Create()
方法中有 Key 属性的方法接口类的
Refresh()
方法
Note
发生回滚的 iFun Engine 的参数是在 signature 上添加 ROLLBACK
的关键词。
Important
要记住发生回滚参数之前代码总是会在再启动。 最好的方法是把发生回滚的参数放在事件处理程序的最前面的位置。
15.1.1.1. 例子: 把代码移动到可以回滚的参数后边。¶
下面代码当中 FetchById(...)
和 Create(...)
发生回滚和再启动事务。
因此,g_character_create_count
是错误,是因为比实际创建的人物数字更多。
为了避免这种情况,最好把在全局变量写作的代码放在可以回滚的参数后边。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | int g_character_create_count;
void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
// 下面的代码会再启动,因为变更全局变量,所以是错误的。
++g_character_create_count;
// 下面的代会再启动。
// 但是,因更新局部变量或者只能读取而不会对功能有影响。
std::string id = message["id"].GetString();
std::string name = message["name"].GetString();
// 下面 Fetch/Create 呼叫有可能会发生回滚。
Ptr<User> user = User::FetchById(id);
Ptr<Character> new_character;
if (user) {
Ptr<Character> old_character = user->GetMyCharacter();
if (old_character) {
old_character->SetHp(0);
old_character->SetLevel(0);
}
new_character = Character::Create(name);
user->SetMyCharacter(new_character);
}
Json response;
if (new_character)
response["result"] = true;
else
response["result"] = false;
session->SendMessage("create_character", response);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | static int the_character_create_count;
void OnCreateCharacter(Session session, JObject message)
{
// 下面的代码会再启动,因为变更全局变量,所以是错误的。
++the_character_create_count;
// 下面的代会再启动。
// 但是,因更新局部变量或者只能读取而不会对功能有影响。
string id = (string) message ["id"];
string name = (string) message ["name"];
// 下面 Fetch/Create 呼叫有可能会发生回滚。
User user = User.FetchById (id);
Character new_character = null;
if (user)
{
Character old_character = user.GetMyCharacter ();
if (old_character)
{
old_character.SetHp (0);
old_character.SetLevel (0);
}
new_character = Character.Create (name);
user.SetMyCharacter (new_character);
}
JObject response = new JObject();
if (new_character)
response ["result"] = true;
else
response ["result"] = false;
session.SendMessage ("create_character", response);
}
|
下面是修改了上面错误的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | int g_character_create_count;
void OnCreateCharacter(const Ptr<Session> , const Json ) {
// 可以重复使用但是没问题。
std::string id = message["id"].GetString();
std::string name = message["name"].GetString();
Ptr<User> user = User::FetchById(id);
Ptr<Character> new_character;
if (user) {
Ptr<Character> old_character = user->GetMyCharacter();
if (old_character) {
old_character->SetHp(0);
old_character->SetLevel(0);
}
new_character = Character::Create(name);
user->SetMyCharacter(new_character);
}
++g_character_create_count;
Json response;
if (new_character)
response["result"] = true;
else
response["result"] = false;
session->SendMessage("create_character", response);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | int the_character_create_count;
void OnCreateCharacter(Session session, JObject message)
{
// 可以重复使用但是没问题。
string id = (string) message ["id"];
string name = (string) message ["name"];
User user = User.FetchById (id);
Character new_character = null;
if (user)
{
Character old_character = user.GetMyCharacter ();
if (old_character)
{
old_character.SetHp (0);
old_character.SetLevel (0);
}
new_character = Character.Create (name);
user.SetMyCharacter (new_character);
}
++the_character_create_count;
JObject response = new JObject ();
if (new_character)
response["result"] = true;
else
response["result"] = false;
session.SendMessage ("create_character", response);
}
|
15.1.1.2. 例子: 避免回滚分开事件¶
不会一直可以把代码移动到后边,避免 ROLLBACK 。 在这种情况下,考虑到 ROLLBACK 会按照事件单位发生,在这儿可以使用把事件分成两个事件的方法。
下面是前边的例子,把事件分成两个事件的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
++g_character_create_count;
std::string id = message["id"].GetString();
std::string name = message["name"].GetString();
function next_step =
bind(&OnCreateCharacter2, session, id, name);
Event::Invoke(next_step, session->id());
}
void OnCreateCharacter2(
const Ptr<Session> &session, const std::string &id, const string &name) {
Ptr<User> user = User::FetchById(id);
Ptr<Character> new_character;
if (user) {
Ptr<Character> old_character = user->GetMyCharacter();
if (old_character) {
old_character->SetHp(0);
old_character->SetLevel(0);
}
new_character = Character::Create(name);
user->SetMyCharacter(new_character);
}
Json response;
if (new_character)
response["result"] = true;
else
response["result"] = false;
session->SendMessage("create_character", response);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void OnCreateCharacter(Session session, JObject message)
{
// 下面的代码会再启动,因为变更全局变量,所以是错误的。
++the_character_create_count;
// 下面的代会再启动。
// 但是,因更新局部变量或者只能读取而不会对功能有影响。
string id = (string) message ["id"];
string name = (string) message ["name"];
Event.Invoke(() => {
OnCreateCharacter2 (session, id, name);
}, session.Id);
}
void OnCreateCharacter2(Session session, string id, string name)
{
User user = User.FetchById (id);
Character new_character = null;
if (user)
{
Character old_character = user.GetMyCharacter ();
if (old_character)
{
old_character.SetHp (0);
old_character.SetLevel (0);
}
new_character = Character.Create (name);
user.SetMyCharacter (new_character);
}
JObject response = new JObject();
if (new_character)
response ["result"] = true;
else
response ["result"] = false;
session.SendMessage ("create_character", response);
}
|
15.1.2. 觉察不愿意的回滚¶
发生出乎意料的回滚的话,致使找不到原因的结果。更严重的是分析原因需要很长时间。 iFun Engine 以调试为目的提供有关回滚的效用参数。
void AssertNoRollback()
调用本参数后发生回滚的话,跟日志一起被强制结束。 这时产生如下日志。
transaction rollback raised after 'AssertNoRollback()': event_name=on_create, model_name=User
Tip
使用 SetEventName() 的话可以如上表示事件名称。 没有指定名称的事件表示 event_name=(unnamed)
。
具体的内容请您参考 为事件赋予调试专用名称 。
Tip
AssertNoRollback()
可以在 MANIFEST.json 设置全体活化/非活化。 具体内容请您参考 设置 ORM 功能的参数 的 enable_assert_no_rollback
。
Tip
iFun Engine 的参数当中最后有 ASSERT_NO_ROLLBACK
标签的参数在内部实现中包含 AssertNoRollback()
。
该参数不能在可能发生回滚的地方调用。
该参数的例子如下。
AccountManager::CheckAndSetLoggedIn()
AccountManager::CheckAndSetLoggedInAsync()
AccountManager::SetLoggedOut()
AccountManager::SetLoggedOutAsync()
AccountManager::SetLoggedOutGlobal()
AccountManager::SetLoggedOutGlobalAsync()
AccountManager::Locate()
AccountManager::LocateAsync()
AccountManager::SendMessage()
AccountManager::BroadcastLocally()
AccountManager::BroadcastGlobally()
MatchmakingClient::StartMatchmaking()
MatchmakingClient::CancelMatchmaking()
Session::SendMessage()
Session::BroadcastLocally()
Session::BroadcastGlobally()
Rpc::Call()
Rpc::ReadyBack 类型的处理程序
ApiService::ResponseWriter 类型的处理程序
Timer::ExpireAt()
Timer::ExpireAfter()
Timer::ExpireRepeatedly()
Timer::Cancel()
15.1.2.1. 例子: 使用AssertNoRollback() 积极地察觉回滚¶
下面例子是使用 AssertNoRollback()
按 Character::Create(name)
察觉 ++g_character_create_count
代码被再启动的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int g_character_create_count;
void OnMyHandler(const Ptr<Session> &session, const Json &message) {
string id = message["id"].GetString();
string name = message["name"].GetString();
Ptr<User> user = User::FetchById(id);
AssertNoRollback();
++g_character_create_count;
if (not user->GetMyCharacter()) {
Ptr<Character> character = Character::Create(name);
}
...
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int the_character_create_count;
void OnMyHandler(Session session, JObject message)
{
string id = (string) message["id"];
string name = (string) message["name"];
User user = User.FetchById(id);
AssertNoRollback();
++the_character_create_count;
if (user.GetMyCharacter() == null) {
Character character = Character.Create (name);
}
...
}
|
15.1.2.2. 例子: 包含 AssertNoRollback() 的 iFun Engine 的参数¶
下面例子是包含 AssertNoRollback()
的 Session::SendMessage()
根据 Item::Create(item_id)
察觉回滚后防止重复传送消息的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void OnLogin(const Ptr<Session> &session, const Json &message) {
string id = message["id"].GetString();
Ptr<User> user = User::FetchById(id);
Ptr<Character> character = user->GetMyCharacter();
Json response;
response["result"] = true;
response["character_name"] = character->GetName();
session->SendMessage("login_reply", response);
// 支付签到报酬。
Uuid item_id = RandomGenerator::GenerateUuid();
Ptr<Item> gift = Item::Create(item_id);
ArrayRef<Ptr<Inventory> > inventory = character->GetInventory();
inventory.PushBack(gift);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void OnLogin(Session session, JObject message)
{
string id = (string) message ["id"];
User user = User.FetchById(id);
Character character = user.GetMyCharacter();
JObject response = new JObject();
response ["result"] = true;
response ["character_name"] = character.GetName ();
session.SendMessage ("login_reply", response);
// 发送签到奖励。
System.Guid item_id = RandomGenerator.GenerateUuid();
Item gift = Item::Create(item_id);
ArrayRef<Inventory> inventory = character.GetInventory();
inventory.PushBack(gift);
}
|
15.2. 管理数据库服务器¶
15.2.1. 贮藏数据¶
按照在 Fetch 的时候对象 cache 解释,iFun Engine 的 ORM Create(...)
对象或者使用 Fetch(...)
从数据库读取对象的时候自动贮藏。
贮藏的对象发生如下情况的时候会被删除。
在 设置 ORM 功能的参数 由
cache_expiration_in_ms
指定的时间中不能再Fetch(...)
的情况。对象被
Delete()
方法删除。
15.2.2. 分片数据库服务器¶
为了数据库分片像 MySQL Cluster 一样的解决方案,或者使用 iFun Engine 的分片功能。
在 MANIFEST.json 设置 key_database
部分, 在``object_databases``部分的话,按照 shard 调整 range_end
部分 iFun Engine 的 ORM 以对象 ID 为标准自动处理 sharding 。 下面是有两个 shard 服务器的时候使用的例子。
"Object": {
"cache_expiration_in_ms" : 3000,
"enable_database" : true,
// 下面 key_database 和 object_databases 只有以 enable_database 为 true 的时候,
// 才会运行。
// 设置 object 的 key 保存的 database
"key_database" : {
// 输入 key database 存在的 mysql server 的地址。 (默认值: tcp://127.0.0.1:3306)
"address": "tcp://127.0.0.1:3306",
"id": "funapi", // 输入 key database 存在的 mysql server 的 id。
"pw": "funapi", // 输入 key database 存在的 mysql server 的 password。
"database": "funapi_key" // 输入 key database 存在的 mysql server 的 database。
},
// 设置保存 object 的 database。
// 根据 range_end 值在多个 mysql server 上保存 object。
"object_databases": [
{
// 是为了分片 object 的 object id 的范围值。
// object id 值为 range_end 以下的话, 在该 database server 保存 object。
"range_end": "80000000000000000000000000000000",
"address": "tcp://127.0.0.1:3306", // 输入 object database 存在的 mysql server 的地址。
"id": "funapi", // 输入 object database 存在的 mysql server 的 id。
"pw": "funapi", // 输入 object database 存在的 mysql server 的 pw。
"database": "funapi1" // 输入保存 object 的 database。
},
{
"range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi2"
}
],
"db_read_threads_size" : 8,
"db_write_threads_size" : 16,
"enable_assert_no_rollback" : true
}
15.2.2.1. 当 Sharding 时在 DB 里产生的 table/procedure¶
当 Sharding 时在 object_databases
和 key_database
里产生的 DB table 和 procedure 如下。
15.2.2.1.1. 在 object_databases 产生的 table/procedure¶
Table
tb_Object_{{ObjectName}}
tb_Object_{{ObjectName}}_ArrayAttr_{{AttributeName}}
tb_Object_{{ObjectName}}_MapAttr_{{AttributeName}}
Procedure
sp_Object_Get_{{ObjectName}}
sp_Object_Insert_{{ObjectName}}
sp_Object_Update_{{ObjectName}}
sp_Object_Delete_{{ObjectName}}
sp_Object_Array_{{ObjectName}}_{{AttributeName}}
sp_Object_Map_{{ObjectName}}_{{AttributeName}}
15.2.2.1.2. 在 key_database 生成的 table/procedure¶
Table
tb_Key_{{ObjectName}}_{{KeyAttributeName}}
Procedure
sp_Object_Get_Object_Id_{{ObjectName}}By{{KeyAttributeName}}
sp_Object_Key_Insert_{{ObjectName}}_{{KeyAttributeName}}
sp_Object_Delete_Key_{{ObjectName}}
15.2.2.2. 当 Sharding 时数据迁移¶
添加或者删除数据库的时候,必须重新作分片数据库, 这时根据新分片规则需要进行数据迁移。
iFun Engine 为了简化这些过程,提供 object_db_migrate.py
的脚本。安装方法如下。
Ubuntu
$ sudo apt-get install python-funapi1-dev
CentOS
$ sudo yum install python-funapi1-devel
如下实行 object_db_migrator.py
。
$ cd /usr/lib/python2.7/dist-packages/funapi/object/
$ object_db_migrator.py --old_manifest='/home/test/OLD_MANIFEST.Json' --new_manifest='/home/test/NEW_MANIFEST.json'
实行后输出如下日志和在 /tmp
有了临时目录,
产生为了迁移该目录的 SQL script 文件。
$ cd /usr/lib/python2.7/dist-packages/funapi/object/
$ object_db_migrator.py --old_manifest='/home/test/OLD_MANIFEST.Json' --new_manifest='/home/test/NEW_MANIFEST.json'
Checks model fingerprint
Makes migration data
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_40000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi3_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi1_00000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates migration files to /tmp/tmp9eqI2T
Done
SQL script 文件的名称产生如下方式。
这意味着在
insert_into_{{shard1}}_from_{{shard2}}_{{range_start}}_{range_end}}.sql
: {{shard2}} 里从 {{range_start}} 到 {{range_end}} 把符合的对象复制到 {{shard1}} 。举个例子 insert_into_funapi2_from_funapi1_4000_8000.sql 在叫 funapi1 的 shard 把从对象 ID 4000 dao1 8000 的对象复制到 shard2 的文件。
delete_from_{{shard}}_{{range_start}}_{{range_end}}.sql
: 在 {{shard}} 服务器从 {{range_start}} 到 {{range_end}} 删除符合的对象。举个例子 delete_from_funapi1_0000_8000.sql 在叫 funapi1 的 shard 从对象 ID 0000 到 8000 的对象当中删除 migration 的记录
首先实行产生的 delete script 后再实行 insert script。如下两个方法都可以使用。
Note
如果数据库服务器都是分别各个服务器机器的话,适用顺序是无所谓。 但是,在同服务器机器中创建不同 DB 进行 sharding 的话, 首先适用 delete 文件后,再适用 insert 文件。 要不然 insert 后实行 delete, 肯定会发生删除前边适用的 insert 的内容。
方法1 : 访问数据库服务器使用 source 指令实行
mysql> source /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_4000_8000.sql
方法2: 在 Shell 直接实行 SQL script
$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_4000_8000.sql
Warning
insert 文件的话,重复实行出现 Error 请注意。
ERROR 1062 (23000): Duplicate entry '\x81b\xA7\x98z4E9\x8A\xE8\x9Fp\xF9\xEB\xEF\x99' for key 'PRIMARY'
Important
object_db_migrator.py
只能迁移数据,不产生目录表或者程序。
为了产生目录表和程序,根据 需要的 DB 权限 说明,
使用 export_db_schema
输出 schema script 然后,要实行 insert/delete script 之前适用。
例子: 从两个 shard 变成 3个
举个例子分片数据库的个数从2个增加到3个的话,每个 MANIFEST.json 当中 key_database
和 object_databases
肯定会如下。
OLD_MANIFEST.json (shard 为两个的时候的文件)
"Object": {
"key_database" : {
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi_key"
},
"object_databases": [
{
"range_end": "80000000000000000000000000000000",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi1"
},
{
"range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi2"
}
],
}
NEW_MANIFEST.json (shard 为三个的时候的文件)
"Object": {
"key_database" : {
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi_key"
},
"object_databases": [
{
"range_end": "40000000000000000000000000000000",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi1"
},
{
"range_end": "80000000000000000000000000000000",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi2"
},
{
"range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi3"
}
],
}
把变更的内容整理如下表。
DB |
OLD_MANIFEST.json |
NEW_MANIFEST.json |
---|---|---|
funapi1 |
0000… ~ 8000… |
0000… ~ 4000… |
funapi2 |
8000… ~ FFFF… |
4000… ~ 8000… |
funapi3 |
8000… ~ FFFF… |
按照上面的内容实行 object_db_migrator.py
。
$ cd /usr/lib/python2.7/dist-packages/funapi/object/
$ object_db_migrator.py --old_manifest='/home/test/OLD_MANIFEST.Json' --new_manifest='/home/test/NEW_MANIFEST.json'
Checks model fingerprint
Makes migration data
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_40000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi3_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi1_00000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates migration files to /tmp/tmp9eqI2T
Done
看到上面的结果可以确认反映变更的表后产生下面的4个脚本。
把从4000… 到 8000… 的内容从 funapi1 复制到 funapi2 的脚本
把从8000… 到 ffff… 的内容从 funapi2 复制到 funapi3 的脚本
在 funapi1 现有领域的从0000… 到 8000… 中删除迁移对象的脚本。
在 funapi2 现有领域的从 8000… 到 ffff… 中删除迁移对象的脚本。
那产生的实行 sql 文件。
$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/delete_from_funapi1_00000000000000000000000000000000_80000000000000000000000000000000.sql
$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/delete_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_40000000000000000000000000000000_80000000000000000000000000000000.sql
$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/insert_into_funapi3_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
15.2.3. 需要的 DB 权限¶
15.2.3.1. 最少权限¶
为了 iFun Engine 的 ORM 自动添加/删除/更新对象,需要如下最少权限。
DB |
权限 |
---|---|
information_schema |
SELECT |
각 object DB 와 key DB |
SELECT, INSERT, UPDATE, DELETE, EXECUTE |
15.2.3.2. DB 模式自动管理的时候添加权限。¶
游戏服务器启动的时候 ORM 可以自动产生 table 或者 procedure。为此需要如下权限。
对象 |
权限 |
---|---|
TABLE, INDEX, PROCEDURE |
CREATE, ALTER, DROP |
15.2.3.3. 当不自动管理 DB 模式时输出需要的 schema script¶
授权在 DB 模式自动管理的时候添加权限。 提到的权限比较难的话,可以输出需要的模式和以管理者权限手动产生该模式。
为了输出 DB 模式的 script 在 设置 ORM 功能的参数 把 export_db_schema
设置为 true
后实行游戏服务器。
产生 script 的话,服务器会结束。
script 文件生成为 /tmp/{{project_name}}_schema/{{project_name}}_schema.{{range_begin}}-{{range_end}}.sql
的形式。
Important
export_db_schema
选项根据 MANIFEST.json
上记录的 object DB 生成 SQL script 。
因此,把 MANIFEST.json
上记录的 object DB 和输出的 SQL script 适用的 DB 之间 必须 DB schema 是同一的状态。
Note
使用 export_db_schema
生成的 script 包含模式版本数据。
因此,不是使用 export_db_schema
而是使用 mysqldump
输出 schema script 的话,
iFun Engine 实行游戏服务器试图再生成模式,这时没有在 DB 模式自动管理的时候添加权限。 需要的权限致使错误。
Tip
如果在 shell 实行服务器的话, 可以在服务器实行脚本后边添加
--export_db_schema
, 而不是在MANIFEST.json
添加export_db_schema
$ ./my_project-local.sh --export_db_schema
15.2.4. DB 服务器实现 Failover¶
iFun Engine 的 ORM 跟 DB 服务器链接中断的时候,自动试图重新链接。
使用本特性,在 MySQL 设置 master-slave replication 的时候,MySQL master 服务器出现障碍时, 由给 slave 给予 master 的 IP 来实现 DB 服务器 fail-over。
简单说,按照如下顺序处理。
发生 My SQL master 服务器障碍 (游戏服务器跟 DB 服务器链接中断)
DBA 把 Slave 转换为新 master
DBA 给予新 master 之前 master 的 IP 地址
通过 iFun Engine ORM , 游戏服务器和 DB 服务器再链接。
Tip
DBA 通过脚本或者 Master HA 等的解决方案可以进行自动化2,3号的工作。
15.2.5. 从游戏服务器外部访问 DB 时注意事项¶
游戏运营中为了用户管理或者根据游戏活动输出中奖人 不是使用 iFun Engine 的对象接口,而是使用使用直接访问数据库 实行 SQL Script 的时候跟着如下指导。
下面是对跟 ORM 产生的模式冲突的情况的说明,跟 ORM 无关的目录或者专栏的话,在外部使用 SQL query 的时候没有任何拘束。
Tip
如果 iFun Engine 游戏服务器需要处理不是 ORM 产生的模式的话,请参考 DB访问Part 1: MySQL
15.2.5.1. 服务器不驱动的情况¶
像检验一样不是使用 iFun Engine 创建的服务器都不驱动的情况下 查看数据或者修改都没有任何拘束。
15.2.5.2. 服务器驱动中查看数据的情况¶
服务器驱动中也可以查看。但是,因为在 iFun Engine 对象 cache 处理以及对象的扩展处理等的原因,有可能会发生 在 iFun Engine 修改的对象数据和查看数据库之间数据不一致的情况。
15.2.5.3. 服务器驱动中添加/变更/删除数据的情况¶
服务器驱动中随意地变更数据的话,可能丢失数据以及出问题更严重的是不能回复的情况。 因此,服务区驱动中的时候,不要通过 MySQL Connector 或者 SQL Script 变更数据,必须检验后服务器不驱动的情况下才会进行。
如果在服务器驱动中的情况下要变更数据的话,为了安全,建议使用 服务器管理Part 1: 添加RESTful APIs 的管理功能变更对象。
15.3. ORM 性能改进¶
15.3.1. 访问对象指导¶
iFun Engine 每逢访问对象在内部自动锁定或者解锁。 为此,需要简化锁定的范围后访问的工作。 请跟着做如下方式。
Object 具有明确的所有权关系,最好在其中导入。
一次访问的对象是数保持少
非常频繁访问的对象被实现为单独的系统。(在世界阶段存在的 Object)
如果您需要访问同一类型的多个对象,不是每逢获取一个 而是将 object UUID 放在向量后,并以 向量性 Fetch 来同时获取全部。
如果不是多个更新不相关,即不是事务, 单独分开并在 Event::Invoke() 中处理它。
15.3.2. 频繁使用的 Object 处理¶
这是唯一的或者在游戏中非常少,并且使用非常频繁的对象 更好实现它作为一个单独的系统,比如 Redis, 而不是 ORM 或者使用 ORM 实现也在 Redis 保存后指定 expire 更好。
15.3.3. 多个对象进行一次 Fetch¶
当多个对象 Fetch 时, 使用向量性 Fetch 参数比每个循环处理更有效。
下面的的代码将在两个用户 Inventory 的 Item 都检索。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void ArrayToVector(const ArrayRef<Object::Id> &array,
std::vector<Object::Id> *vector) {
for (size_t i = 0; i < array.Size(); ++i) {
vector->push_back(array.GetAt(i));
}
}
void FetchTwoUsersItems() {
Ptr<User> user1 = User::Fetch(user1_uuid);
Ptr<User> user2 = User::Fetch(user2_uuid);
std::vector<Object::Id> id_list;
ArrayToVector(user1->GetInventory(), &id_list);
ArrayToVector(user2->GetInventory(), &id_list);
std::vector<std::pair<Object::Id, Ptr<Item>>> items;
Item::Fetch(id_list, &items);
...
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public static void ArrayToSortedSet(ArrayRef<System.Guid> array,
SortedSet<System.Guid> sorted_set)
{
foreach (System.Guid guid in array)
{
sorted_set.Add (guid);
}
}
void FetchTwoUsersItems()
{
User user1 = User.Fetch (user_guid);
User user2 = User.Fetch (user_guid);
SortedSet<Guid> id_set = new SortedSet<Guid> ();
ArrayToSortedSet (user1.GetInventory(), id_set);
ArrayToSortedSet (user2.GetInventory(), id_set);
Dictionary<System.Guid, Item> items = Item.Fetch (id_set);
...
}
|
15.3.4. 使用 Foreign 对象 fetch 最小化¶
指定为 Foreign
的属性在导入该对象时不会自动获取,因此它不加载或锁定不需要的其他对象,这有助于提高性能。
有问题的模型:
1 2 3 4 5 6 7 8 9 10 | {
"User": {
"Name": "String KEY",
"MailBox": "Mail[]"
},
"Mail": {
...
}
}
|
根据上面的 ORM,如果从 iFun Engine 加载 User 对象的话, MailBox 的数组也将总是被导入。 如果用户平均有100个 Mail ,这是一个非常的负担。
但是, MailBox 数组不需要访问,除了使用邮箱,所以不必在读取 User 时,一起读取它。
在这种情况下,通过将 Foreign
适用到 MailBox 的话,只需要的时候才能导入 MailBox 。
解决问题的模型:
1 2 3 4 5 6 7 8 9 10 | {
"User": {
"Name": "String KEY",
"MailBox": "Mail[] Foreign"
},
"Mail": {
...
}
}
|
15.3.4.1. 例子: 实现朋友目录¶
有问题的模型:
{
"User": {
"Name": "String KEY",
"Inventory": "Item[]",
"Friends": "User[]"
},
"Item": {
...
}
}
当在上述 JSON 模型中从 DB 读取 User 时,在 Friends 指定的 User 也会被导入
然后,朋友 User 的 Friends 也会链接地被导入。
这时一个坏的实现,因为在每次加载时,朋友的朋友都会被加载。因此,将 Friends 必须定义为 Foreign
。
解决问题的模型:
{
"User": {
"Name": "String KEY",
"Inventory": "Item[]",
"Friends": "User[] Foreign"
},
"Item": {
...
}
}
- 现在
Friends
是一个定义为Foreign
的数组,根据 定义为 Foreign 的数组或者Map 有两种 getter。 User::GetFriends() : 返回包含朋友 User 的对象 ID 的数组。
User::FetchFriends() : 将所有朋友 User 进行 Fetch 后,返回包含它们的数组
使用它的话,对朋友列表做以下两个事情。
从 DB 读取所有朋友对象
Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Ptr<User> > friends = user->FetchFriends();
User user = User.Fetch(user_guid);
ArrayRef<User> friends = user.FetchFriends();
不读取朋友数据,只返回朋友的 Object ID
Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Object::Id> friends = user->GetFriends();
User user = User.Fetch(user_guid);
ArrayRef<System.Guid> friends = user.GetFriends ();
15.3.4.2. 例子: 避免从朋友列表中读取不必要的仓库¶
当只想读取朋友列表中的名称时,可以使用上述 JSON 模型编写以下代码。
Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Ptr<User> > friends = user->FetchFriends();
std::vector<std::string> names;
for (size_t i = 0; i < friends.Size(); ++i) {
if (friends.GetAt(i)) {
names.push_back(friends.GetAt(i)->GetName());
}
}
User user = User.Fetch (user_guid);
ArrayRef<User> friends = user.FetchFriends();
SortedSet<string> names = new SortedSet<string>();
foreach (User friend in friends)
{
names.Add (friend.GetName ());
}
当执行 user->FetchFriends() 时,上述代码读取对应朋友列表的 User 对象,此时,Inventory attribute 也被导入,导致相当大的负荷。
为了防止这种情况,可以将 Inventory 指定为 Foreign
修改为不能自动读取仓库的模型:
{
"User": {
"Name": "String KEY",
"Inventory": "Item[] Foreign",
"Friends": "User[] Foreign"
},
"Item": {
...
}
}
这样修改后,读取 Inventory 的所有 Item 的时候,请您按照以下步骤处理。
Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Ptr<Item> > inventory = user->FetchInventory();
for (size_t i = 0; i < inventory.Size(); ++i) {
Ptr<Item> item = inventory.GetAt(i);
...
}
User user = User.Fetch(user_guid);
ArrayRef<Item> inventory = user.FetchInventory ();
foreach (Item item in inventory)
{
...
}
如果不导入整个对象,而只读取 Inventory 的特定 Item 的话, 可以按照以下步骤处理。
Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Object::Id> inventory = user->GetInventory();
Ptr<Item> item = Item::Fetch(inventory.GetAt(3));
User user = User.Fetch (user_guid);
ArrayRef<Guid> inventories = user.GetInventory();
Item my_item = Item.Fetch(inventories.GetAt(3));
15.3.5. 使用正确地锁定类型¶
根据在 当 Fetch 时 LockType 提到, iFun Engine 在访问对象时提供以下的锁定类型。
kReadLock
kWriteLock
kReadCopyNoLock
在 kReadLock
情况下,其他事件已经通过 kReadLock
读取对象也
导入该对象把它以阅读形式来使用
在 kWriteLock
的情况下,如果其他事件通过 kWriteLock 读取对象后使用它的话,使用结束之前对所有的锁定类型保持排他性。
因此,只想读取对象,使用 kReadLock
处理它
这有助于提高性能。
Tip
为了便于开发 Fetch(...)
使用 kWriteLock
作为默认参数值,但建议适当地改为 kReadLock
。
但是,通过 kReadLock
使用对象也时间会长的话,
到本使用结束, 为了使用 kReadLock
修改其对象
想导入的其他时间会延迟处理。
kReadCopyNoLock
不锁定只通过读取对象的副本防止 阅读导致的写作瓶颈 。
但是 kReadCopyNoLock
由副本来工作,会暂时数据不一致。
但是,在简单地显示诸如朋友列表,排名询问之类的数据的内容的情况下,即使数据被暂时不一致也不会出现问题。
一般在需要更新数据的情况下,关闭列表窗口后再打开或者通过更新按钮重新请求最新列表。
如果这只是简单表示的数据并可以负担一些不一致的话,建议使用 kReadCopyNoLock
。
以下代码是使用 kReadCopyNoLock 创建朋友列表。
Json response;
Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Ptr<User> > friends = user->FetchFriends(kReadCopyNoLock);
response["friends"].SetArray();
for (size_t i = 0; i < friends.Size(); ++i) {
Ptr<User> friend = friends.GetAt(i);
if (not friend) {
continue;
}
Json friend_json;
friend_json.SetObject();
friend_json["name"] = friend->GetName();
friend_json["level"] = friend->GetLevel(); // 假设有叫Level 的 Attribute
response["friends"].PushBack(friend_json);
}
JObject response = new JObject ();
JArray friends_json = new JArray ();
User user = User.Fetch (user_guid);
ArrayRef<User> friends = user.FetchFriends (funapi.LockType.kReadCopyNoLock);
foreach (User friend in friends)
{
JObject obj = new JObject ();
obj ["name"] = friend.GetName ();
obj ["level"] = friend.GetLevel (); // 假设有叫Level 的 Attribute
friends_json.Add(obj);
}
response ["friends"] = friends_json;
15.3.6. 添加 DB index¶
可以随便地添加 Table index 在 (高级) 根据 ORM 的 DB 模式 中说明的 procedure 的话, 除了 signature 之外可以变更。如果在通过添加 index 或者修改 procedure 可以提高性能的情况下,可以使用其特性。
15.4. (高级) 在 DB 检索对象¶
还不够的话,可以使用与 SQL 的 SELECT 相似的适合指定的检索条件的检索对象 ID 的功能。 为此, ORM 为每个对象模型和属性产生以下方法。
void ObjectName::SelectBy{{AttributeName}}(cond_type, cond_value, callback)
cond_type
:Object::kEqualTo
,Object::kLessThan
, 或者Object::kGreaterThan
cond_value
: 标准值。callback
:void(const Ptr<std::set<Object::Id> > &object_ids)
类型的参数。通过
object_ids
传达 object id, 如果出问题,由 object_ids 来返回 NULL。
使用检索到的对象 ID 通过 Fetch(...)
访问对象。
但是这段时间可能在其他地方变更或者删除 Object,如下面的例子要检查是否适合检索条件。
Important
要设置按照检索条件多要多个检索到 Object ID。
Important
在适合检索条件的 column 上直接添加 index。 是因为对 Engine 来说不会知道如何 colume 使用于检索条件。
Note
在多种情况下,本功能不是使用为了实现游戏内容,而是使用实现运营功能。
Note
如果本功能还不够的话,可以使用 iFun Engine 的 DB访问Part 1: MySQL 直接创建 SQL query 后检索。但是,在使用 SQL 直接访问的情况下,一定使用阅读专用。避免跟ORM冲突。
15.4.1. 例子: 检索级别 100以上的用户¶
用于运营道具的本例子服务器假设检索到超过 100级的用户后把他们降低到 99.
因为可以在``Character::SelectByLevel(…)`` 和 Character::Fetch(...)
之间在其他地方变更或者删除对象,所以需要再确认对象是否存在和检索条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void Select() {
Character::SelectByLevel(Object::kGreaterThan, 99, OnSelected);
}
void OnSelected(const Ptr<std::set<Object::Id> &object_ids) {
if (not object_ids) {
LOG(ERROR) << "error";
return;
}
auto level_down = [](const Object::Id &object_id) {
Ptr<Character> character = Character::Fetch(object_id);
if (not character || character->GetLevel() < 100) {
return;
}
character->SetLevel(99);
};
for (const Object::Id &object_id: *object_ids) {
Event::Invoke(bind(level_down, object_id));
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | void Select()
{
Character.SelectByLevel(Object.kGreaterThan, 99, OnSelected);
}
void OnSelected(SortedSet<Object.Id> object_ids)
{
if (object_ids = null)
{
Log.Error ("error");
return;
}
auto level_down = [] (Object.Id &object_id) {
Character character = Character.Fetch (object_id);
if (character == null || character.GetLevel () < 100)
{
return;
}
character.SetLevel (99);
};
for (Object.Id &object_id in object_ids)
{
Event.Invoke (() => { level_down(object_id); });
}
}
|
15.5. (高级) 根据 ORM 的 DB 模式¶
15.5.1. DB 模式被 ORM 自动变更¶
iFun Engine 按照使用者定义的对象模型保持 DB 模式。 如果变更对象模型当服务器驱动时 iFun Engine 比较模式变更事项 像以下似的自动修改 DB 模式。
对新添加的对象模型生成目录和 DB 程序。
可以自动修改的话,修改表专栏 (同时把 DB 程序也一起修改)
Tip
iFun Engine 的 ORM 当修改表时即使专栏被删除也不会做删除的工作。这是因为不小心删除专栏的话,会导致致命的后果。
iFun Engine ORM 在以下情况下会判断不能自动修改。
当定义模型的属性时,在已使用的
String(n)
n 比之前减少的情况。在 设置 ORM 功能的参数 说明的
db_string_length
或者db_key_string_length
比之前减少的情况。把 Primitive 类型属性换成别的类型。
如果不能自动修改的话,输出以下日志后中断驱动。 这时候,要看日志后直接手动地修改模式。
例子: 把 Character 对象的 Level 从 Integer 换成 String(24) 的情况
F1028 15:56:50.045100 3951 object_database.cc:996] Mismatches object_model=User, table_name=tb_Object_User, column_name=col_Cash, expected_model_column(type=VARCHAR(24), character_set=utf8), mismatched_db_column(type=bigint(8), character_set=)
15.5.2. 缩少 String 长度¶
在 String 长度拉长的情况下, iFun Engine 会自动修改长度, 在缩少的情况下要手动地修改。手动修改模式的方法 有两个。
方法 1: 如果可以删除 DB 的话,使用 DROP {{dbname}}
和 CREATE {{dbname}}
重新创建 DB 后重启服务器。
方法 2: 因要保持 DB 数据而不能删除 DB 的话,像以下一样创建 ALTER 脚本后 在 DB 实行。
-- key 表变更 ALTER TABLE tb_Key_{{Object_Name}}_{{KeyColumnName}} MODIFY col_{{KeyColumnName}} CHAR({{NewLength}}); -- object 表变更 ALTER TABLE tb_Object_{{ObjectName}} MODIFY col_{{KeyColumnName}} CHAR({{NewLength}});
以下是 key string 长度从 20 缩少到 12 的例子。
ALTER TABLE tb_Key_User_Name MODIFY col_Name CHAR(12); ALTER TABLE tb_Object_User MODIFY col_Name CHAR(12);
实行已创建的 sql script后重启服务器。 变更模式后重启服务器的话,iFun Engine 自动地再生成 DB 程序。
Note
如果根据 分片数据库服务器 , 在 key db 和 object db 分开的情况下,要把上述 sql script 单独在 key db 和 object db 实行。
Tip
如果要变更的专栏多的话,通过实行以下 sql script,可以创建要变更专栏的 ALTER … script 。
假设 key string 的长度从 20 缩少到 12, string 长度从 4096 缩少到 100。
-- 输入在 MANIFEST.json 输入的 database。
USE [database];
-- 输入变更对象 key string 专栏的目前类型。
SET @org_key_string_length = 'CHAR(20)';
-- 把 key string 专栏输入要变更的 type。
SET @key_string_length = 'CHAR(12)';
-- 输入变更对象 string 专栏的目前类型。
SET @org_string_length = 'VARCHAR(4096)';
-- 把 string 专栏输入要变更的 type。
SET @string_length = 'VARCHAR(100)';
-- 创建 ALTER ... sql script。
SELECT CONCAT(GROUP_CONCAT(sql_script separator '; '), ';') AS sql_script
FROM
(
SELECT GROUP_CONCAT('ALTER TABLE ', table_name, ' MODIFY ', column_name, ' ',
@key_string_length separator '; ') AS 'sql_script'
FROM information_schema.columns
WHERE table_schema = DATABASE() AND column_type = @org_key_string_length
UNION ALL
SELECT GROUP_CONCAT('ALTER TABLE ', table_name, ' MODIFY ', column_name, ' ',
@string_length separator '; ') AS 'sql_script'
FROM information_schema.columns
WHERE table_schema = DATABASE() AND column_type = @org_string_length
) AS A;
实行上述 sql script 的话, 像以下一样创建 ALTER … sql script。
ALTER TABLE tb_Key_Character_Name MODIFY col_Name CHAR(12); ALTER TABLE tb_Key_User_Id MODIFY col_Id CHAR(12); ALTER TABLE tb_Object_Character MODIFY col_Name CHAR(12); ALTER TABLE tb_Object_User MODIFY col_Id CHAR(12); ALTER TABLE tb_Object_Character MODIFY col_Name2 VARCHAR(100); ALTER TABLE tb_Object_Character MODIFY col__tag VARCHAR(100); ALTER TABLE tb_Object_User MODIFY col__tag VARCHAR(100);
15.5.3. ORM 的类型换成 SQL 类型的规则¶
ORM 的类型像以下一样换成 SQL 类型。
iFun Engine Type |
SQL Type |
---|---|
Bool |
tinyint(1) |
Integer |
bigint(8) |
Double |
double |
String |
varchar(4096). Key 的话 char(12) |
Object |
binary(16) |
User Defined Object |
binary(16) |
Note
上述 String 的 SQL Type 表示默认值,在 设置 ORM 功能的参数 修改 db_string_length
和 db_key_string_length
的话,可以变更长度。
15.5.4. ORM 的 table naming 规则¶
iFun Engine 的 ORM 产生如下 table。
Tip
以下说明 col__ObjectId_
的话,由 binary 来保存。可以由 hex()
参数来读取。
15.5.4.1. tb_Object_{{ObjectName}}¶
使用 JSON 定义的对象 table。产生在所有 shard 把属性保存为专栏。
Columns
说明
col__ObjectId_
为了 iFun Engine 内部识别 object,提供的 Uuid 类型的 objec id。
col_{{AttributeName}}
…
15.5.4.2. tb_Key_{{ObjectName}}_{{KeyAttributeName}}¶
使用 JSON 定义的对象的 key 属性保存的 table 。 在 Key Database 生成。 把 Key 属性保存为专栏。
Columns
说明
col__ObjectId_
为了 iFun Engine 内部识别 object,提供的 Uuid 类型的 objec id。
col_{{KeyAttributeName}}
对象的 Key 属性。 保存为 Primary Key。
15.5.4.3. tb_Object_{{ObjectName}}_ArrayAttr_{{AttributeName}}¶
在有数组类型的属性的情况下产生。在所有 shard 产生。
col__ObjectId_
专栏和col__Index_
专栏由复合 key 组成。
Columns
说明
col__ObjectId_
为了 iFun Engine 内部识别 object,提供的 Uuid 类型的 objec id。
col__Index_
Array 的 index. SQL type 为 bigint
col__Value_
该 index 的 value 值。
15.5.4.4. tb_Object_{{ObjectName}}_MapAttr_{{AttributeName}}¶
在有 Map 类型的属性的情况下产生。在所有 shard 产生。
col__ObjectId_
专栏和col__Key_
专栏由复合 key 组成。
Columns
说明
col__ObjectId_
为了 iFun Engine 内部识别 object,提供的 Uuid 类型的 objec id。
col__Key_
Map 的 key
col__Value_
该 key 的 value
15.5.5. ORM 产生的 DB Index 和 Constraint ORM¶
iFUn Engin 在产生 table 时,把 col__ObjectId_
column 设置为 Primary Key ,
在指定为 key 的属性里设置 Non-Clustred Index 以及 Unique Constraint 。
Tip
除了 iFun Engine 产生的 index 和 constraint 之外还可以添加 index 或者 constraint。
15.5.6. ORM 产生的 DB Procedure¶
iFun Engine 产生 object table 后,如下 procedure 一起产生, 只能通过该 procedure 访问 database 的数据。
sp_Object_Get_{{ObjectName}} : 为了使用指定的 object id 把 object 从 DB 读取使用。
sp_Object_Get_Object_Id_{{ObjectName}}By{{KeyAttributeName}} : 为了使用指定的 object 的 key 把 object id 从 DB 读取使用。
sp_Object_Key_Insert_{{ObjectName}}_{{KeyAttributeName}} : 为了把新 object 的 key 在 DB 保存使用。
sp_Object_Insert_{{ObjectName}} : 为了把新 object 在 DB 保存使用。
sp_Object_Update_{{ObjectName}} : 为了保存 object 的变更事项。
sp_Object_Delete_{{ObjectName}} : 为了删除 object 使用。
sp_Object_Delete_Key_{{ObjectName}} : 为了删除 object 的 key 使用。
sp_Object_Array_{{ObjectName}}_{{ArrayAttributeName}} : 添加/删除 array attribute 的 element 的时候使用。
Tip
像 index/constraint 一样,procedure 不会变更 signature 的话,能随意修改。但,从 procedure 返回的 rowset 的 column 要一致。
15.6. (高级) ORM 性能分析¶
在 设置 ORM 功能的参数 输入的
个别数据库算出咨询处理时间统计后提供。
使用本功能的话,需要如下选项活性化以及设置 port
。
设置 ORM 功能的参数 的
enable_database
事件功能设置参数 的
enable_event_profiler
服务器管理Part 1: 添加RESTful APIs 的
api_service_port
为了看统计调用提供的如下 API。
-
GET
http://{ip}:{api-service-port}/v1/counters/funapi/object_database_stat/
¶
按照每个数据库的 range_end
的值,统计结果各项母的意义
如下。
项目
all_time
积累的统计
last1min
1分钟之前统计
write_count
处理写作询问的次数
write_mean_in_sec
平均处理写作的时间
write_stdev_in_sec
处理写作时间标准偏差
write_max_in_sec
最多处理写作时间
read_count
处理读取询问的次数
read_mean_in_sec
平均处理读取的时间
read_stdev_in_sec
处理读取时间标准偏差
read_max_in_sec
最多处理读取时间
Note
range_end
值为 00000000-0000-0000-0000-000000000000
的结果
意味着 key_database
。 (即使没有分片也有同样的意义)
这结果算出导入 / 生成有 Key
的对象关联的处理询问时间的
统计。
结果例子
{
"00000000-0000-0000-0000-000000000000": {
"database": "funapi",
"address": "tcp://127.0.0.1:3306",
"all_time": {
"write_count": 4,
"write_mean_in_sec": 0.000141,
"write_stdev_in_sec": 0.000097,
"write_max_in_sec": 0.000286,
"read_count": 1000,
"read_mean_in_sec": 0.031476,
"read_stdev_in_sec": 0.033169,
"read_max_in_sec": 0.104138
},
"last1min": {
"write_count": 0,
"write_mean_in_sec": 0.0,
"write_stdev_in_sec": 0.0,
"write_max_in_sec": 0.0,
"read_count": 0,
"read_mean_in_sec": 0.0,
"read_stdev_in_sec": 0.0,
"read_max_in_sec": 0.0
}
},
"ffffffff-ffff-ffff-ffff-ffffffffffff": {
"database": "funapi",
"address": "tcp://127.0.0.1:3306",
"all_time": {
"write_count": 4,
"write_mean_in_sec": 0.000086,
"write_stdev_in_sec": 0.00006,
"write_max_in_sec": 0.000176,
"read_count": 19989,
"read_mean_in_sec": 0.057533,
"read_stdev_in_sec": 0.045418,
"read_max_in_sec": 0.198318
},
"last1min": {
"write_count": 0,
"write_mean_in_sec": 0.0,
"write_stdev_in_sec": 0.0,
"write_max_in_sec": 0.0,
"read_count": 0,
"read_mean_in_sec": 0.0,
"read_stdev_in_sec": 0.0,
"read_max_in_sec": 0.0
}
}
}
Note
不适用 数据库分片 也 跟上述一样的形式输出性能分析结果分析。是因为在 iFun Engine 跟分片一样处理。
如果跟以下一样适用数据库分片的话,结果如下。
适用分片 MANIFEST.json
"Object": {
"enable_database" : true,
"key_database" : {
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi_key"
},
"object_databases": [
{
"range_end": "80000000000000000000000000000000",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi_obj1"
},
{
"range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"address": "tcp://127.0.0.1:3306",
"id": "funapi",
"pw": "funapi",
"database": "funapi_obj2"
}
]
}
结果例子
{
"00000000-0000-0000-0000-000000000000": {
"database": "funapi_key",
"address": "tcp://127.0.0.1:3306",
"all_time": {
"write_count": 4,
"write_mean_in_sec": 0.000141,
"write_stdev_in_sec": 0.000097,
"write_max_in_sec": 0.000286,
"read_count": 1000,
"read_mean_in_sec": 0.031476,
"read_stdev_in_sec": 0.033169,
"read_max_in_sec": 0.104138
},
"last1min": {
"write_count": 0,
"write_mean_in_sec": 0.0,
"write_stdev_in_sec": 0.0,
"write_max_in_sec": 0.0,
"read_count": 0,
"read_mean_in_sec": 0.0,
"read_stdev_in_sec": 0.0,
"read_max_in_sec": 0.0
}
},
"80000000-0000-0000-0000-000000000000": {
"database": "funapi_obj1",
"address": "tcp://127.0.0.1:3306",
"all_time": {
"write_count": 4,
"write_mean_in_sec": 0.0001,
"write_stdev_in_sec": 0.000061,
"write_max_in_sec": 0.000191,
"read_count": 20011,
"read_mean_in_sec": 0.055629,
"read_stdev_in_sec": 0.046967,
"read_max_in_sec": 0.224221
},
"last1min": {
"write_count": 0,
"write_mean_in_sec": 0.0,
"write_stdev_in_sec": 0.0,
"write_max_in_sec": 0.0,
"read_count": 0,
"read_mean_in_sec": 0.0,
"read_stdev_in_sec": 0.0,
"read_max_in_sec": 0.0
}
},
"ffffffff-ffff-ffff-ffff-ffffffffffff": {
"database": "funapi_obj2",
"address": "tcp://127.0.0.1:3306",
"all_time": {
"write_count": 4,
"write_mean_in_sec": 0.000086,
"write_stdev_in_sec": 0.00006,
"write_max_in_sec": 0.000176,
"read_count": 19989,
"read_mean_in_sec": 0.057533,
"read_stdev_in_sec": 0.045418,
"read_max_in_sec": 0.198318
},
"last1min": {
"write_count": 0,
"write_mean_in_sec": 0.0,
"write_stdev_in_sec": 0.0,
"write_max_in_sec": 0.0,
"read_count": 0,
"read_mean_in_sec": 0.0,
"read_stdev_in_sec": 0.0,
"read_max_in_sec": 0.0
}
}
}
15.7. 设置 ORM 功能的参数¶
enable_database: 跟实际数据库联动活性化。在简单测试或者开法阶段的情况下,指定 false 的话,不用准备 DB . (type=bool, default=false)
db_mysql_server_address: ORM 使用的 DB 地址。(type=string, default=”tcp://127.0.0.1:3306”)
db_mysql_id: ORM 使用的 MySQL user id (type=string, default=””)
db_mysql_pw: ORM 使用的 MySQL password (type=string, default=””)
db_mysql_database: ORM 使用的 MySQL database 名称 (type=string, default=””)
cache_expiration_in_ms: 把对象从在 DB 读取后贮藏到不贮藏时候的毫秒 (type=int64, default=300000)
copy_cache_expiration_in_ms: 在从远程服务器复制对象的情况下,到该对象不贮藏的时候的毫秒 (type=int64, default=700)
enable_delayed_db_update: 不是每次就更新 DB 延迟后处理 batch 与否 (type=bool, default=false)
db_update_delay_in_second: 在不是每次就更新 DB 处理 batch 的情况下, 工作延迟的秒单位时间 (type=int64, default=10)
db_read_connection_count: 为了读取链接 DB 的个数 (type=int64, default=8)
db_write_connection_count: 为了写作链接 DB 的个数 (type=int64, default=16)
db_key_shard_read_connection_count: 在 object_subsystem_sharding 使用时,为了在 key database 读取链接 DB 个数 (type=int64, default=8)
db_key_shard_write_connection_count: 在 object_subsystem_sharding 使用时, 为了在 key database 写作链接 DB 个数 (type=int64, default=16)
db_character_set: DB 의 character set. iFun Engine 不是跟着设置 DB 值,而是跟着本设置值 (type=string, default=”utf8”)
export_db_schema: 如果 true 的话,输出 DB 模式生成脚本后结束。 参考 需要的 DB 权限 。 (type=bool, default=false)
没有直接修改设置的项目
db_string_length: 字符串属性的时候对应的 SQL VARCHAR 长度 (type=int32, default=4096)
db_key_string_length: Key 字符串属性的时候对应的 SQL CHAR 的长度 (type=int32, default=12)
use_db_stored_procedure: 不使用 RAW SQL 句子而使用 stored procedure 的与否 (type=bool, default=true)
use_db_stored_procedure_full_name: 축약된 이름 대신 긴 이름의 stored procedure 를 사용할지 여부 (type=bool, default=true)
use_db_char_type_for_object_id: 为了表示 Object ID 在 SQL DB 上使用CHAR(32)。就不使用 BINARY(16) (type=bool, default=false)
enable_assert_no_rollback: 在代码上
AssertNoRollback()
活性化。 参考 觉察不愿意的回滚 (type=bool, default=true)use_db_select_transaction_isolation_level_read_uncommitted: 实行 Select 询问的时候,
TRANSACTION ISOLATION LEVEL
设置为READ UNCOMMITTED
。 在 false 的情况下,使用 mysql 的默认值。(type=bool, default=true)
15.8. MySQL(MariaDB) 서버 설정 및 관리¶
이 가이드는 MySQL 또는 MariaDB에 익숙하지 않은 사용자를 위한 설정 방법을 제공합니다. 그 외에 성능 개선에 필요한 설정들도 설명합니다.
설정을 변경하려면 서비스를 종료한 후 설정 파일을 변경한 뒤 재시작 하는 일련의 과정이 필요합니다. 설정 파일은 운영체제마다 조금씩 다른 위치에 있습니다.
우분투의 경우는 다음 둘 중 한 곳에 위치해 있습니다.
/etc/mysql/my.cnf
/etc/mysql/conf.d/mysql.cnf
CentOS의 경우에는 아래 경로에 위치해 있습니다.
/etc/my.cnf
설정은 설정=값
형태로 기재해야 하며 반드시 [mysqld]
항목에 포함되어야만 적용이 됩니다.
잘못된 예
[mysqldump]
...
max_connections = 300
올바른 예
[mysqld]
...
max_connections = 300
15.8.1. 기본 설정 가이드¶
아래 설정들은 단일 머신에서 개발할 때는 특별히 확인할 필요가 없습니다. 하지만 테스트 또는 상용 환경과 같이 게임 서버와 DB 서버가 각각 다른 머신에서 실행되는 경우라면 확인해볼 필요가 있습니다.
bind-address:
MySQL이 연결을 주고 받을 네트워크 주소를 지정합니다. 우분투의 경우 이 값이 127.0.0.1
로
설정되어 로컬 상에서만의 접근을 허용하고 있습니다.
이 값을 0.0.0.0
으로 설정하거나 다음과 같이 주석 처리(#)할 경우 MySQL은 모든 곳으로부터
연결을 허용합니다.
[mysqld]
...
# bind-address = 127.0.0.1
max_connections:
MySQL이 연결을 주고 받는 최대 커넥션 개수를 지정합니다. 기본 값은 151로 상용 환경에서 사용하기엔
부족합니다. 권장되는 값은 8GB 램을 기준으로 300에서 500 정도이며
아래에서 설명할 open_files_limit
값과 함께 보는 것이 좋습니다.
아이펀 엔진에서 사용하는 커넥션 개수는 다음과 같은 공식에 따라 증가 합니다.
DB에 접속하는 서버 개수 x
MANIFEST.json
에 설정된 object_databases 개수 xMANIFEST.json
에 설정에 따른 커넥션 개수
MANIFEST.json
설정 중 커넥션 개수를 증가시키는 항목들은 다음과 같습니다.
db_read_connection_count
값db_write_connection_count
값db_key_shard_read_connection_count
값db_key_shard_write_connection_count
값
[mysqld]
...
max_connections = 300
open_files_limit:
운영체제에서 MySQL이 사용하는 파일 디스크립터를 몇 개까지 허용할 것인지 결정합니다.
이 값은 max_connections
값의 5배로 설정되므로 일반적으로 값을 변경할 필요는 없지만
보다 많은 테이블을 사용할 때는 더 높은 값을 할당해야 합니다.
가장 적절한 설정 방법은 10000으로 지정한 후 필요에 따라 늘리는 것입니다. 더 높은 값을 사용할 수록
많은 메모리가 사용되므로 메모리를 충분히 두는 것이 좋습니다.
[mysqld]
...
open_files_limit = 10000
Ubuntu 16.04 또는 CentOS 7에서는 MySQL 설정 파일이 아닌 systemd 설정 파일에 지정해야만 정상적으로 값이 적용됩니다. 적용 방법은 다음과 같습니다.
$ sudo vi /lib/systemd/system/mysql.service
[Service]
...
LimitNOFILE=10000
15.8.2. (고급)성능 개선 가이드¶
MySQL에서 제공하는 일부 설정들은 서버 자원을 보다 효율적으로 사용할 수 있도록 합니다. 그러나 항상 성능을 개선한다는 보장은 없기 때문에 하나씩 바꾸는 것이 좋습니다.
innodb_buffer_pool_size:
테이블 및 인덱스 데이터의 캐시가 저장되는 버퍼의 크기를 지정합니다. 이 값이 클수록 캐시 적중률이 높아져 디스크의 부하를 줄일 수 있습니다. 하지만 너무 큰 값은 메모리 swap 을 발생시켜 큰 성능 저하가 발생하므로 주의해야 합니다.
가장 적절한 값을 찾기 위해서는 시스템 메모리의 80%를
innodb_buffer_pool_size
사이즈로 지정한 후iostat
이나vmstat
과 같은 툴로 메모리 swap 이 발생하는지 모니터링하는 것입니다. 만약 DB 서버에 다른 프로세스가 실행 중이거나 앞서 설명한max_connections
값이 충분히 높은 상태라면 80%보다 낮은 값으로 시작하는 것이 좋습니다.[mysqld] ... innodb_buffer_pool_size = 8GB # MySQL 서버가 10GB을 사용할 경우
innodb_log_file_size:
redo 로그 파일 크기를 지정합니다. 높은 값을 지정할 경우 디스크 쓰기 빈도가 낮아져 부하를 줄여주지만 MySQL에 장애가 발생했을 때 복구에 걸리는 시간이 길어질 수 있습니다.
상용 환경에서는 서버의 규모에 따라 64MB에서 512MB 사이의 값을 지정하는 것이 좋습니다. 특히 1초 이내의 순간동안 많은 양의 UPDATE가 발생할 수 있는 상황에서는
innodb_log_file_size
값을 높게 지정함으로써 디스크 부하를 크게 줄일 수 있습니다.Note
이 값을 변경하기 위해서는 MySQL을 종료한 후
redo 파일(
/var/lib/mysql/lib_logfile*
)을 삭제하거나 다른 곳으로 옮겨야 합니다.[mysqld] ... innodb_log_file_size = 256MB
innodb_flush_method:
데이터를 디스크에 플러시 하는 방법을 변경합니다. 기본값은
O_DSYNC
로 OS에서 제공하는 페이지 캐시를 이용합니다.innodb_buffer_pool_size
값이 충분히 크다면 이 값을O_DIRECT
로 변경함으로써 운영체제에서 제공하는 페이지 캐시를 무시할 수 있습니다. 이는 MySQL과 OS가 캐시를 중복 저장하지 않도록 하므로 약간의 성능 개선을 기대할 수 있습니다.innodb_buffer_pool_size
값이 충분하지 않은 상태에서O_DIRECT
를 사용하는 경우에는 오히려 캐시 적중률이 낮아짐으로써 디스크 부하가 커질 수 있으니 값을 변경하기 전에는 충분한 테스트가 필요합니다.[mysqld] ... innodb_flush_method=O_DIRECT
innodb_flush_log_at_trx_commit:
redo 로그 파일 기록 방식을 변경합니다. 지정할 수 있는 값은 0,1,2 로 3가지가 있으며 기본값은 1입니다.
innodb_flush_log_at_trx_commit=0
: 트랜잭션과 관계없이 1초마다 로그 버퍼에 있는 내용을 로그 파일에 저장한 후 디스크로 플러시 합니다. MySQL이 멈출 경우 최대 1초 이내의 로그 버퍼 데이터가 유실될 수 있습니다. 디스크 부하가 가장 적지만 가장 불안전한 파일 기록 방식입니다.innodb_flush_log_at_trx_commit=1
: 하나의 트랜잭션이 완료될 때마다 로그 버퍼에 있는 내용을 로그 파일에 저장한 후 디스크로 플러시 합니다. 기록 방식 중 가장 느린 속도를 가지고 있으며 디스크에 많은 부하를 주지만 데이터의 안정성을 보장받을 수 있습니다.innodb_flush_log_at_trx_commit=2
: 하나의 트랜잭션이 완료될 때마다 로그 버퍼에 있는 내용이 로그 파일로 쓰여집니다. 이와 동시에 1초마다 로그 파일에 있는 내용이 디스크로 플러시 됩니다. MySQL이 멈추더라도 데이터의 안정성을 보장받지만 OS가 멈출 경우 최대 1초 동안 플러시 되지 않은 로그 버퍼 데이터가 유실될 수 있습니다.기본 값(1)을 제외한 두 옵션 모두 트랜잭션이 유실될 수 있으므로 이 값을 변경하는 것 보다는 다른 값을 우선적으로 보는 것이 좋습니다. 1의 경우에도 하드웨어 레벨의 wrtie-back 캐시가 켜져 있다면 데이터 손실의 위험이 있으므로 캐시 옵션을 체크해보는 것이 좋습니다. 손실을 감안할 수 있는 데이터를 저장하는 경우라면 2를 지정하는 것이 좋습니다.
[mysqld] ... innodb_flush_log_at_trx_commit=2