持久化与 DB 线程模型
BigWorld 的持久化不是业务代码直接写 SQL。BaseApp 负责协调 Base/Cell 数据、备份和归档;DBApp 通过 IDatabase 抽象屏蔽 MySQL/XML 等后端;MySQL 后端把阻塞数据库操作下沉到后台任务。
先给结论
持久化链路可以概括为:
关键源码:
- BaseApp 每个 tick 调用
backup()和archive(),见 baseapp.cpp。 Base::archive()调用onPreArchive,然后writeToDB(WRITE_BASE_CELL_DATA),见 base.cpp。Base::writeToDB()如果存在 Cell entity 且没有 Cell 数据,会先requestCellDBData(),见 base.cpp。requestCellDBData()向 CellApp 发CellAppInterface::writeToDBRequest,见 base.cpp。Archiver::tick()按 archive period 分摊实体归档,并根据 DBApp channel 窗口决定是否跳过,见 archiver.cpp。DBApp实现IDatabasepass-through,见 dbapp.hpp。- MySQL 后端
putEntity()等操作会加入后台任务,见 mysql_database.cpp。
BaseApp 的持久化职责
BaseApp 不是只把消息转发到 DBApp。它还负责:
- 决定什么时候 archive。
- 决定是否 backup。
- 调用脚本
onPreArchive/onWriteToDB。 - 需要 Cell 数据时向 Cell entity 请求。
- 处理 secondary DB。
- 控制 DBApp channel 背压。
- BaseApp retiring 时处理备份和 offload。
这说明持久化和运行时状态治理强耦合。
自动归档
Base::autoArchive() 根据 shouldAutoArchive_ 决定是否执行归档,NEXT_ONLY 成功后关闭自动归档,见 base.cpp。
Base::archive() 会:
- 如果 Cell entity 正在 destroy pending,通常跳过。
- 调用
onPreArchive,脚本可返回 false 取消。 - 如果 Base 已销毁,跳过。
- 调用
writeToDB(WRITE_BASE_CELL_DATA)。
源码见 base.cpp。
Cell 数据请求
概述: 如果 Base 有 Cell entity,但调用 writeToDB() 时没有 Cell data,就会先发请求。这体现了 Base/Cell 分层持久化:Base 持有数据库协调权,Cell 持有空间和位置数据。
源码入口: base.cpp:1970
// base.cpp:1970 - Cell 数据请求
bool Base::requestCellDBData( WriteDBFlags flags,
WriteToDBReplyStructPtr pReplyStruct )
{
// 1. 检查是否有 Cell entity
if (!this->hasCellEntity())
{
return false;
}
// 2. 创建请求 bundle
Mercury::Bundle & bundle = this->cellBundle();
// 3. 发送 writeToDBRequest 到 CellApp
bundle.startRequest( CellAppInterface::writeToDBRequest,
new WriteToDBReplyHandler( this, flags, pReplyStruct ) );
bundle << id_;
// 4. 发送到 Cell
this->sendToCell();
return true;
}流程图:
sequenceDiagram participant Base as Base Entity participant Bundle as Mercury Bundle participant CellApp as CellApp participant Cell as Cell EntityBase->>Base: writeToDB() 检查 Base->>Base: 没有 Cell data Base->>Bundle: startRequest(writeToDBRequest) Bundle->>Bundle: 写入 EntityID Base->>CellApp: 发送请求 CellApp->>Cell: 查找 Cell Entity Cell->>Cell: 收集持久化数据 Cell->>Bundle: 写入 Cell data CellApp->>Base: 回复 Cell data Base->>Base: 合并 Base + Cell data Base->>DBApp: 发送完整数据
详细讲解:
分层持久化:Base 持有数据库协调权,Cell 持有空间和位置数据。两者需要合并才能完整持久化。
异步请求:
startRequest()是异步操作,Base 不会阻塞等待 Cell 回复。EntityID 传递:CellApp 有多个 Cell Entity,需要 EntityID 路由到正确的 Entity。
ReplyHandler:
WriteToDBReplyHandler处理 Cell 回复,合并 Base 和 Cell 数据后发送到 DBApp。为什么这样设计:
- Base 和 Cell 可能在不同进程
- Cell 可能正在迁移或 offload
- Base 需要协调多个 Cell 的数据(如果实体有多个 Cell)
持久化完整流程
源码入口: base.cpp:1947
// base.cpp:1947 - Base 持久化
bool Base::writeToDB( WriteDBFlags flags, WriteToDBReplyHandler * pHandler,
PyObjectPtr pCellData, DatabaseID explicitDatabaseID )
{
// 1. 检查是否需要 Cell 数据
if (this->hasCellEntity() && pCellData == NULL)
{
// 先请求 Cell 数据
return this->requestCellDBData( flags,
new WriteToDBReplyStruct( pHandler ) );
}
// 2. 准备 Base 数据
PyObjectPtr pBaseData = this->getDict();
// 3. 发送到 DBApp
Mercury::Bundle & bundle = dbApp_.bundle();
bundle.startRequest( DBAppInterface::writeEntity,
new WriteToDBReplyHandler( this, flags, pHandler ) );
// 4. 写入 EntityID 和 DatabaseID
bundle << id_;
bundle << explicitDatabaseID;
// 5. 写入 Base 数据
bundle << pBaseData;
// 6. 写入 Cell 数据(如果有)
if (pCellData)
{
bundle << pCellData;
}
return true;
}关键细节:
- 如果有 Cell Entity 但没有 Cell 数据,先请求 Cell 数据
- Base 数据和 Cell 数据分别序列化
- DatabaseID 可能是 0(首次写入)或已有值(更新)
- 写入是异步的,通过 ReplyHandler 处理结果
- Cell 持有空间和 cell-only 属性权威。
- 写 DB 时必须把两边数据拼起来。
writeToDB 防御逻辑
Base::writeToDB() 会处理多种边界:
- flags 为 0 时直接失败。
- delete from DB 时自动加 log off,并清掉 base/cell data 写入。
- 未写入 DB 的实体不需要 log off。
- 类型不能在 Cell 上时清掉
WRITE_CELL_DATA。 - 如果需要 Cell data 但
pCellData_不存在,报错并失败。 - 写 base data 前调用
onWriteToDB(cellData)。 - 如果不是销毁中,会先
backupBaseNow()。
这说明写 DB 不只是序列化,还要考虑备份、一致性和生命周期状态。
持久化流到底写了什么
BigWorld 写数据库时,写入单元不是“某个属性生成一条 SQL”,而是“把当前实体的持久化视图重新序列化成一段实体流,再交给 DB 后端解释”。
Base 侧的证据很直接:
Base::addToStream()写 Base 属性时,只传EntityDescription::BASE_DATA | EntityDescription::ONLY_PERSISTENT_DATA,见 base.cpp。- 写 Cell 数据时,
PyCellData::addToStream()也传addPersistentOnly=true,见 base.cpp。
Cell 侧不是单独维护另一套 DB 序列化格式,而是复用备份格式:
RealEntity::writeBackupProperties()的注释明确说,备份和writeToDB使用相同格式,见 real_entity.cpp。Base::getDBCellData()会先吃掉 Cell backup 的外层包,再把其中的 Cell 属性 dict 解出来,见 base.cpp。
所以完整链路是:
- Base 决定归档或显式
writeToDB()。 - 如果需要 Cell 数据,Base 向 Cell 请求备份格式的数据。
- Base 从备份流里提取 Cell 属性 dict。
- Base 重新拼出“Base persistent + Cell persistent”的实体流。
- DBApp/IDatabase/MySQL 再把这段流写入数据库。
这里还要注意一个细节:落库流里除了实体属性,还可能伴随 Base mailbox、logoff、autoload 等元数据,它们不是属性列的一部分,而是和实体流并行处理。证据见 IDatabase::putEntity(...) 的参数和 PutEntityTask 对 baseMailbox_ / updateAutoLoad_ 的处理,分别在 idatabase.hpp 和 put_entity_task.cpp。
有没有通用的“脏数据标记”
这部分最容易被误解成 ORM。
当前源码里,确实存在“属性变化对象”,但它主要服务于 Cell 到 Client / Ghost 的增量同步:
PropertyOwnerBase::changeOwnedProperty()在属性变化时构造SinglePropertyChange,见 property_owner.cpp。Entity::onOwnedPropertyChanged()会把变化编码成客户端属性消息或 ghost 更新,见 entity.cpp。PropertyChange::addToExternalStream()/addToInternalStream()是网络增量同步编码,不是数据库更新编码,见 property_change.cpp。
但在数据库持久化路径上,我没有看到一套通用的“字段级 dirty set -> UPDATE 某几列”机制。源码证据更接近下面这个模型:
- 数据库写入由
archive()、显式writeToDB()、销毁/登出等事件触发。 - 触发后重新从当前实体状态生成完整 persistent stream。
- MySQL 更新阶段再把这段流按映射拆回列或子表。
也就是说:
- 对网络同步来说,BigWorld 有精细的属性变化对象。
- 对数据库持久化来说,BigWorld 更像“按实体 persistent 视图整体刷新”,而不是典型 ORM 的 session dirty flush。
这也是为什么修改 Identifier 属性时,源码只是发出“以后写库可能失败”的提示,而不是立刻维护某个统一 dirty 标志,见 entity.cpp。
属性是怎么进数据库的
如果把问题具体化成“脚本里一个属性,最后怎么落到 MySQL 表里”,链路如下:
Base::writeToDB()把当前 Base/Cell 的 persistent 属性写成二进制流,见 base.cpp 和 base.cpp。DBApp::writeEntity()读取flags、typeID、dbID、entityID,把剩余流交给WriteEntityHandler,见 dbapp.cpp。WriteEntityHandler::writeEntity()决定这是首次插入、普通更新还是 logoff,仅在WRITE_BASE_CELL_DATA时把流传给数据库层,见 write_entity_handler.cpp。MySqlDatabase::putEntity()创建PutEntityTask后台任务,见 mysql_database.cpp。PutEntityTask::performBackgroundTask()调entityTypeMapping_.update(...)或insertNew(...)/insertExplicit(...),见 put_entity_task.cpp。EntityTypeMapping::visit()再用BASE_DATA | CELL_DATA | ONLY_PERSISTENT_DATA遍历 persistent 属性,把流拆成 SQL 参数,见 entity_type_mapping.cpp。
这条链路说明,数据库层从来没有直接读 Python 属性对象。它读到的是一段已经按 EntityDef 排好顺序的持久化流。
MySQL 映射结构
MySQL 映射层不是单表万能映射,而是一组类型专用 PropertyMapping:
PropertyMapping是“流 <-> 数据库”的抽象基类,定义了fromStreamToDatabase()和fromDatabaseToStream(),见 property_mapping.hpp。PropertyMapping::create()按DataType选择具体实现,见 property_mapping.cpp。
从源码能确认几类常见映射:
- 基础数值、字符串、向量等,通常映射成列。
CLASS、FIXED_DICT走复合映射,不是简单一列。ARRAY/TUPLE在非索引场景下可能走SequenceMapping子表,或者在dbLen() > 0时走BlobbedSequenceMapping,见 property_mapping.cpp。USER_TYPE走用户类型映射,如果绑定失败会直接阻止 DBApp 继续,避免误改表结构,见 property_mapping.cpp。
因此,一个 .def 属性并不必然等于“一列”。复杂类型可能展开成多列、子表,或者退化成 blob 存储。
除了实体属性表,MySQL 侧还维护运行时元数据:
EntityTypeMapping::addLogOnRecord()维护bigworldLogOns,记录databaseID对应的 Base mailbox,见 entity_type_mapping.cpp。updateAutoLoad()更新 autoload 标志,见 entity_type_mapping.cpp。
所以“数据库里的实体”实际上由两部分构成:
- persistent 属性映射出的业务数据。
- logon/autoload/mailbox 这类引擎运行时元数据。
这和 ORM 的区别
BigWorld 的数据库层和 ORM 有相似处,但核心模型并不一样:
| 维度 | BigWorld | 典型 ORM |
|---|---|---|
| 持久化单元 | EntityDef 驱动的 persistent stream | 内存对象/Session |
| 写入触发 | archive() / writeToDB() / logoff | transaction flush / save |
| 更新粒度 | 重新生成实体 persistent 视图 | 常见是脏字段或脏对象 |
| 映射来源 | .def + PropertyMapping | 类注解/模型定义 |
| 复杂类型 | 可拆子表、复合列或 blob | 视 ORM 能力而定 |
| 运行时语义 | 和 AOI、Proxy、Base/Cell、登录元数据耦合 | 多数不关心实时网络语义 |
更准确的判断是:BigWorld 的 DB 层借用了“对象映射”的思路,但它首先是分布式实体引擎的持久化后端,不是通用业务 ORM。
Secondary DB
BaseApp 可以写 secondary SQLite DB:
- 条件包括存在
pSecondaryDB、已有完整 DBID、不是 autoload/delete/primary/explicit dbid 等。 - 写入时先
addToStream(),再带GameTime写 secondary DB。 - explicit write 时 commit secondary DB。
源码见 base.cpp。
Archiver::tickSecondaryDB() 会 tick secondary DB,并按 maxCommitPeriodInTicks commit,见 archiver.cpp。
这属于 BigWorld 的容灾/恢复设计,不是普通缓存。
Archiver 分摊策略
Archiver::tick() 根据 archivePeriodInTicks() 把 basesToArchive_ 分摊到多个 tick 中:
- archive disabled 时不做。
- 如果没有 secondary DB 且 DBApp channel send window 使用超过阈值,跳过归档。
- 周期结束后重新洗牌实体列表。
- 每 tick 只归档当前范围内已写过 DB 的 Base。
源码见 archiver.cpp。
这体现了游戏服务器持久化的关键取舍:不能为了落库阻塞 Tick,也不能把 DBApp 网络窗口打满。
DBApp 与 IDatabase
DBApp 注册 DBAppInterface,见 dbapp.cpp。
DBApp 暴露:
loadEntitywriteEntitydeleteEntitylookupEntitylookupEntitieswriteSpaceswriteGameTimesecondaryDBRegistrationupdateSecondaryDBs
见 dbapp.hpp。
同时它提供 IDatabase pass-through,注释明确说调用这些方法而不是直接调用 IDatabase,这样 DBApp 可以拦截和处理,见 dbapp.hpp。
MySQL 后台任务
MySQL 后端把阻塞数据库操作变成后台任务:
getEntityputEntitydelEntityexecuteRawCommandgetIDssetGameTimewriteSpaceData- secondary DB operations
搜索证据显示 MySqlDatabase::putEntity() 创建 PutEntityTask 并加入 BufferedEntityTasks,见 mysql_database.cpp。
PutEntityTask::performBackgroundTask() 执行数据库写入,performEntityMainThreadTask() 回主线程调用 handler,见 put_entity_task.cpp。
这与 线程架构与后台任务 中的后台任务模型一致。
一致性边界
BigWorld 的持久化不是强事务分布式数据库模型。需要注意:
- Base 和 Cell 数据要跨进程请求后拼接。
- 写 DB 前可能先备份 Base。
- Secondary DB 和 primary DB 有不同路径。
- DBApp channel 背压会导致归档跳过。
- BaseApp retiring/offload 会影响备份和写 DB。
- 如果 DBApp 不可用,源码会报数据丢失风险。
例如 Base::writeToDB() 找不到 DBApp 时会报错并返回 false,见 base.cpp。
源码取舍
持久化链路的核心取舍可以直接从代码看出来:
- DB 阻塞操作通过
IDatabase后端任务下沉,不在 BaseApp 主 Reactor 里直接执行 SQL。 - Base 统一协调 Base/Cell persistent 数据,DBApp 只接收已经按 EntityDef 顺序编码好的实体流。
- Archive 按 tick 分摊,并受 DBApp channel send window 约束,避免写档把实时循环压垮。
- Secondary DB 复用 BaseApp 侧序列化结果,提供本地恢复路径,但不替代 primary DB。
- MySQL 映射层按
PropertyMapping拆流到列、复合列、子表或 blob,不是 Python 对象到表的直接反射。
源码代价也很明确:
- Base/Cell 数据拼接依赖跨进程请求,消息时序和实体生命周期会影响写库结果。
- 数据库 schema 与 EntityDef persistent 视图强绑定,
.def变更会穿透到 digest、表同步和历史数据解释。 - 没有看到通用字段级 dirty set 驱动的 DB flush;属性变化对象主要服务网络增量同步。
- DBApp、后台任务、channel 背压和登录/恢复路径共用资源,积压会影响写档、查档和上线。
源码验证重点
持久化测试应覆盖实际链路,而不是只测 MySQL 后端能写入:
Base::archive()调用onPreArchive返回 false 时必须跳过写 DB。- Base 有 Cell entity 且缺少 Cell 数据时,应先走
writeToDBRequest请求 Cell 数据。 Base::addToStream()和 Cell 数据写入必须只包含ONLY_PERSISTENT_DATA。- 属性变化产生的
PropertyChange不应被误当成数据库 dirty flush 机制。 EntityTypeMapping::visit()建表和写库时应使用同一份 persistent 视图。- 复杂属性应按
PropertyMapping::create()选择列、子表、复合映射或 blob。 - DBApp channel send window 超阈值时,Archiver 应跳过本轮 primary archive。
bigworldLogOns、autoload、Base mailbox 和实体属性应分别验证,不能混成同一种属性列。Base::writeToDB()找不到 DBApp 时应失败并记录数据丢失风险。
本章边界
本章解释持久化和 DB 线程模型。.def 如何决定 persistent 属性、Identifier、索引和数据库 digest,在 EntityDef 契约与协议生成 中继续展开。
相关文档
- EntityDef 契约生成,详见 EntityDef 契约与协议生成。
- 序列化实现,详见 序列化与 EntityDef。
- 实体模型,详见 实体模型 Base/Cell/Client。
- 实体生命周期,详见 实体生命周期状态机。
- 登录与会话,详见 登录、会话与 Proxy 接管。
