Skip to content

选配数据库

读完这篇,你将能根据项目所处的阶段(开发、单机部署、生产运维、超大规模)为 Zeze 选出最合适的数据库后端,并知道怎么通过配置在多个数据库之间灵活分配表。

为什么数据库可以是”透明”的

Section titled “为什么数据库可以是”透明”的”

很多框架把业务逻辑和某种特定的数据库死死绑在一起——比如一上来就让你写 SQL,代码里到处是 SELECTINSERT。一旦哪天想换个存储,几乎要重写一遍。

Zeze 不这么干。它的做法是:在底层存储和上层事务之间,放一层统一的 KV(键值)抽象。你写业务代码时操作的是 Bean 和事务,并不关心数据最终落到了内存里、MySQL 里还是 TiKV 里。需要换数据库时,只改 XML 配置,不动一行业务代码,同一套逻辑就能跑在另一种存储上。

这就是”数据库透明”的核心:一套代码,多种后端,按需切换

Database 抽象类:统一接口的入口

Section titled “Database 抽象类:统一接口的入口”

这一层抽象的根基是 Database 抽象类,它对外暴露的核心能力很简洁:

  • openTable(name, id):打开一张表。
  • beginTransaction():获取一个数据库事务。
  • Operates 接口:直接操作后台数据库,包括实例的 setInUse / clearInUse 管理、以及版本化数据的读写。

Database.Table 又有两类实现,对应两种数据组织方式:

实现类型说明适用范围
AbstractKVTableKV 模式,key 和 value 都是 ByteBuffer所有数据库都支持
关系表映射把 Bean 的字段映射为表的列MySQL / PostgreSQL 支持

也就是说,KV 模式是”最大公约数”,无论你选哪种后端都能用;而关系映射是给 MySQL/PostgreSQL 的”加强包”,能让你享受索引、关系查询的好处。

在这两者之间起桥梁作用的是 Storage 层:它读着表的配置,决定某张表该走 KV 模式还是关系映射模式,把 TableX 和底层的 Database.Table 衔接起来。

DatabaseType 枚举列出了 Zeze 支持的所有后端:

DbType说明
Memory纯内存,不持久化。适合单元测试和开发调试
MySql基于 JDBC,同时支持 KV 和关系映射
PostgreSQL基于 JDBC,同时支持 KV 和关系映射
SqlServer基于 JDBC,需自备 mssql 驱动
RocksDB嵌入式 KV,性能极高,不支持 GlobalCacheManager(仅单机模式)
MongoDB文档存储
RedisKV 存储
Tikv分布式事务 KV,支持 distTxn
Dbh2Zeze 自研的分布式数据库

直接对着场景选,是最省心的方式:

场景推荐理由
单元测试 / 开发调试Memory零配置,启动最快,跑完即弃
单机高性能RocksDB嵌入式、无网络开销,读写极快
生产环境需要 SQL 运维MySQL / PostgreSQL成熟稳定,运维生态完善,支持关系映射
需要跨表关系查询MySQL / PostgreSQL 关系映射Bean 字段映射为表列,可建索引
高吞吐热点数据Redis内存级读写延迟
超大规模分布式TiKV分布式事务,能水平扩展
多数据中心同步Dbh2内置分片与复制

一个务实的成长路径是:开发期用 Memory,单机上线换 RocksDB,规模扩大或需要 SQL 运维时迁到 MySQL/PostgreSQL,走向真正的大规模分布式时再上 TiKV。因为切换只改配置,这条路走得平滑。

数据库的选择最终落在 XML 配置上。一个 DatabaseConf 大致长这样:

<DatabaseConf
Name=""
DatabaseType="MySql"
DatabaseUrl="jdbc:mysql://127.0.0.1:3306/zeze"
DatabaseName="zeze"
DisableOperates="false"
distTxn="false" />

几个要点:

  • Name 为空字符串代表默认数据库,没有特别指定的表都会落到它上面。
  • 可以同时配置多个 DatabaseConf,给每个起个不同的 Name
  • 然后用 <TableConf> 把具体的表分配到指定的数据库实例:
<!-- 把角色表放到主库 -->
<TableConf Name="Role" DatabaseName="zeze" />
<!-- 把日志表单独放到另一个库,分摊压力 -->
<TableConf Name="GameLog" DatabaseName="zeze_log" />

这就是多库混用:你可以把热点表和高频写入的表拆到不同的数据库实例上,各取所长。比如把核心业务放 MySQL、把排行榜这种高吞吐热点放 Redis,组合使用。

一个绕不开的限制:RocksDB 只能单机

Section titled “一个绕不开的限制:RocksDB 只能单机”

RocksDB 是嵌入式数据库,数据直接写在进程所在机器的磁盘上,没有网络开销,所以单机性能极高。但这也带来了一个硬限制:

⚠️ RocksDB 不支持多实例。它无法配合 GlobalCacheManager 做多服务器之间的缓存同步——如果你强行配置,框架会直接抛异常

换句话说,一旦你的服务要走向多 Provider 的分布式部署(参见上一篇 走向分布式 里讲到的 GCM 缓存一致性),就必须换掉 RocksDB,改用支持分布式的后端(如 MySQL、TiKV 等)。RocksDB 的定位很明确:单机时代的最佳选择

另外有几个 key 长度限制值得留意,避免踩坑:Database.eMaxKeyLength = 2712(受 PostgreSQL 限制),MySQL 是 3072,MongoDB 是 1024。关系映射模式则只有 MySQL / PostgreSQL 支持。

如果说 DatabaseConf 决定”数据存哪儿”,那 TableConf 决定的是”这张表怎么被缓存和使用”。它有几个关键属性:

属性作用
CacheCapacity缓存容量(记录数)。最重要的配置。建议设成预期在线人数
CacheFactor放大因子,默认 5.0。借助 SoftReference,实际可缓存的记录数能远超 CacheCapacity
CheckpointWhenCommit事务提交时是否立即把该表持久化。默认 false,对充值货币等关键数据建议设为 true
DatabaseName指定这张表归属哪个数据库实例
DatabaseOldName / DatabaseOldMode旧库配置,用于数据迁移:新库查不到时自动从旧库读取并导入

CacheCapacity 是你最能”感受到性能”的一个旋钮。它的实际效果是:

实际缓存记录数 ≈ CacheCapacity × CacheFactor

原理是 Zeze 用 SoftReference(软引用)来持有缓存对象。JVM 内存充裕时,缓存可以远超 CacheCapacity 的设定值(最多到乘以 CacheFactor 的量级);只有当内存紧张时,才会按需回收软引用,把缓存压回到 CacheCapacity 附近。

所以一条简单的经验法则是:CacheCapacity 设成你预期的在线人数。在线的人数据基本都能命中缓存,离线的人数据在内存紧张时被自然淘汰,下次访问再从数据库加载。配合默认的 CacheFactor=5.0,缓冲空间相当宽裕。

大多数表采用延迟写(提交事务时不立刻刷盘,由检查点机制统一处理),性能更好。但对于绝对不能丢的数据——比如玩家的充值货币——可以把 CheckpointWhenCommit 设为 true,让每次事务提交都立刻持久化这张表。代价是多一点 I/O,换来的是断电也不丢数据的安心。

当你需要换库或重构表结构时,DatabaseOldName / DatabaseOldMode 能让迁移变得平滑:框架在读取时,如果新库查不到,会自动回退到旧库读取并导入到新库。这样你就能边跑边迁,而不用停服做一次性大搬家。

把这篇收个尾,其实就三句话:

  • 数据库透明:统一 KV 抽象 + XML 配置切换,让你在 Memory、关系型、NoSQL、分布式 KV 之间自由选择,业务代码纹丝不动。
  • 按场景选型:开发用 Memory、单机上 RocksDB、生产运维上 MySQL/PostgreSQL、超大规模上 TiKV,热点上 Redis,路径清晰。
  • 精调缓存:用 CacheCapacity 配合 CacheFactor 把缓存调到刚好的水位,用 CheckpointWhenCommit 守护关键数据,用新旧库配置做平滑迁移。

选好了数据库,下一步就该理解在它之上、Zeze 是如何让你不用操心多线程也能写出正确的并发程序的——请继续阅读 不懂多线程也能写并发程序。需要查阅配置项与数据库抽象的完整细节时,参考 数据库总览配置参考