上篇提到,任务进度频繁变化时,原本可以合并的多次更新,被逐次写进了数据库。这是写入次数上的开销。
还有另一种开销,上篇只简单带了一句:业务只改了少量状态,保存时却写入了一大块数据。
要分析这个问题,不妨先看看这一大块数据里究竟装了什么。
一个业务对象里,可能既有玩家独有的状态,也有从配置读取的数据,还有为了计算和访问方便而保留的临时结果。如果直接序列化整个对象,这些内容就可能被一股脑写进数据库。
但对象里有这些数据,不代表 DB 里也需要完整保留一份。
有些内容可以通过配置或已保存的状态可靠重建;另一些则记录了玩家实际获得、改变或消耗的结果,必须保留下来。
这篇先把这个问题说清楚:哪些数据需要存,哪些可以在加载后恢复。

DB 保留恢复业务状态所需的信息,其余内容需能可靠重建。图形为概念示意,不代表容量比例。
从内存对象回到业务状态
开发时,直接保存内存对象是一条很顺手的路径:保存时序列化,加载时还原,业务代码也容易理解。
但内存中的数据组织,首先要方便业务运行。
为了减少查询或重复计算,对象里可能会保留一些能够重新获取的数据。持久化则需要回答另一个问题:服务重启、内存数据丢失之后,我们需要恢复出什么状态?
以装备为例。假设一件装备在内存中关联了配置中的名称、基础属性,以及玩家获得的随机词条、强化等级和当前耐久。保存时,这些数据需要分别考虑。
配置中的名称和基础属性,如果能够通过配置 ID 可靠恢复,就可以考虑只保存标识,避免每件装备重复保存相同的配置内容。
随机词条、强化等级和当前耐久,则承载了这件装备自己的状态,需要有可靠的持久化依据。
特别是随机词条——程序虽然能够再生成一组,但新的随机结果无法代替玩家当时获得的结果。
能重新生成某种数据,并不意味着能够恢复原来的业务状态。

重新随机可能得到另一组词条;读取保存的原始结果,才能还原已有词条。数值仅作示意。
计算结果是否需要保留
还有一些内容是计算结果。例如,装备的最终属性可以由基础属性、随机词条和强化状态计算得到。
如果输入完整、计算过程能够重复,就可以考虑在加载后重新计算。

基础属性、随机词条和强化状态构成计算输入;输入完整且规则可重复时,最终属性可以在加载后重建。
同时保存输入和计算结果,则需要处理两者的同步问题:强化等级已经更新,最终属性却还是旧值时,加载后以哪一份为准?
当然,重建成本较高时,保留计算结果也可能有价值,但需要明确它如何更新,以及与原始状态不一致时如何处理。
配置变化之后,还能正确恢复吗
这里还有一个容易忽略的前提:配置会变化。
如果业务希望旧装备跟随新配置调整,根据当前配置恢复属性,可能正是预期行为。
但如果要求保留装备生成时的属性,只保存配置 ID 就未必足够。这时,可以保存配置版本或必要的属性快照。
选择保存版本时,还需要确保对应版本的配置能够重新取得。
列表和历史数据也一样。如果业务只关心某件事是否完成,保存完成状态可能就足够;如果还需要完成时间、完成次数和每次结果,就要保留对应的信息。
保存之前,先问三个问题
整理这些数据时,我通常会问三个问题:
- 这份数据是玩家独有的状态,还是能够从其他来源获得的信息?
- 如果不保存,能否可靠恢复,重建成本能否接受?
- 配置或规则变化后,恢复结果是否仍符合业务预期?
这些问题不一定会得出“少存一些”的结论。有些字段可以省略,有些场景则需要补充版本或快照。
内存对象新增了展示字段或临时计算结果,并不意味着 DB 也要增加一份。
优化持久化,首先要把数据保存的理由说清楚。
明确哪些状态必须保留、哪些内容可以可靠重建,才能在保持业务正确的前提下,减少不必要的保存开销。
这篇先把“哪些数据需要存”说清楚。但即使存下来的都是必要数据,也不代表每次保存都需要把它们重新写一遍。
下一篇聊 DB “写太多”的问题:哪些更新可以合并,怎样 减少重复落库,让多次业务变化不必对应多次 DB 写入。