Table 存储接口
Table 是 Zeze 中的核心数据存储单元,语义上等价于一个类型安全的 Map<K, V>。每张表由一个 key(实现 Comparable)和一个 value(继承自 Bean)组成,通过 solution.xml 中的 <table> 声明并由代码生成器自动产生 TableX<K, V> 子类(→ bean, solution-xml)。
核心 CRUD API
Section titled “核心 CRUD API”以下方法均须在事务内调用(→ transaction),否则会抛出异常。
// 返回 value,记录不存在时返回 null@Nullable V get(@NotNull K key)get 是最基础的读取操作。首次访问某条记录时,框架会从 Storage 层加载到内存缓存,并提升全局缓存状态至 StateShare。
getOrAdd
Section titled “getOrAdd”// 记录不存在则创建并返回新值@NotNull V getOrAdd(@NotNull K key)
// 通过 OutObject<Boolean> 判断是否新建@NotNull V getOrAdd(@NotNull K key, @Nullable OutObject<Boolean> isAdd)当 isAdd.value == true 时表示本次创建了新记录。典型用法:
var isAdd = new OutObject<Boolean>();BPlayer player = tablePlayer.getOrAdd(roleId, isAdd);if (isAdd.value) { // 首次创建,初始化默认值 player.setLevel(1); player.setName("newbie");}put / insert / tryAdd
Section titled “put / insert / tryAdd”// 直接写入(覆盖或新增)void put(@NotNull K key, @NotNull V value)
// 仅当 key 不存在时写入,已存在则抛 IllegalArgumentExceptionvoid insert(@NotNull K key, @NotNull V value)
// 仅当 key 不存在时写入,返回是否成功boolean tryAdd(@NotNull K key, @NotNull V value)put 不管记录是否已存在,直接覆盖。insert 和 tryAdd 提供了”仅新增”语义。
remove
Section titled “remove”void remove(@NotNull K key)将指定 key 对应的记录标记为删除。记录在事务提交后才会真正生效,后续由 Checkpoint 刷写到数据库。
contains
Section titled “contains”boolean contains(@NotNull K key)等价于 get(key) != null,用于判断记录是否存在。
部分场景需要在事务外读取数据,框架提供了两种方式:
selectCopy
Section titled “selectCopy”// 返回记录的深拷贝,事务内外均可使用@Nullable V selectCopy(@NotNull K key)- 事务内调用:若该事务已访问过此 key,返回最新值的拷贝;否则从后台加载并拷贝,但不加入事务的 RecordAccessed。
- 事务外调用:从缓存或数据库加载后返回拷贝。
- 得到的对象不应用于修改,建议搭配
ReadOnly接口使用。
selectDirty
Section titled “selectDirty”// 从本地缓存快速读取,默认 3 秒有效期@Nullable V selectDirty(@NotNull K key)// 自定义缓存有效期(毫秒),0 表示总是从数据库取最新值@Nullable V selectDirty(@NotNull K key, int cacheTTL)selectDirty 不经过 GlobalCacheManager 权限协商,速度快但一致性较弱,适合用于 whileCommit 回调、日志统计等可容忍短暂不一致的场景。
内存缓存与 LRU 机制
Section titled “内存缓存与 LRU 机制”TableCache
Section titled “TableCache”TableCache 是每张表的内存缓存层,内部维护一个 ConcurrentHashMap<K, Record1<K, V>> 作为主数据存储,并使用分段式 LRU 策略管理缓存淘汰。
核心参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
MAX_NODE_COUNT | 8640 | LRU 节点队列最大长度,超过时触发 shrink |
SHRINK_NODE_COUNT | 8000 | shrink 后的目标节点数 |
CacheInitialCapacity | 31 | 缓存初始容量 |
RealCacheCapacity | -1(不限) | 缓存容量上限,-1 表示不限制 |
LRU 淘汰策略:框架定期创建新的 热点段(ConcurrentHashMap),新记录总是插入当前热点段。后台定时任务检查总节点数,超过 MAX_NODE_COUNT 时,将最老的段合并到当前头部段并丢弃多余段。当缓存记录总数超过 RealCacheCapacity 时,从最老的段开始逐条尝试回收非脏、非新鲜的记录。
本地 RocksDB 缓存
Section titled “本地 RocksDB 缓存”每张表在本地维护一个 RocksDB 缓存表(localRocksCacheTable),用于在内存紧张时为 LRU 缓存中的 value 提供本地恢复。
TableCache 的 ConcurrentHashMap 保存了所有缓存记录的 key(Record1 条目),但其中的 value 是 SoftReference——当 JVM 内存紧张时,GC 会回收这些软引用。本地 RocksDB 的作用就是保存这些被 GC 回收的 value,下次访问时可直接从本地恢复,避免回源远程数据库。
在没有本地 RocksDB 的设计中(如早期的 xdb),缓存容量配置需要考虑 value Bean 的大小:大 value 的记录只能配置很小的容量,否则内存会溢出。不同表的 value 大小差异很大,需要逐表手动调参,运维负担重。
引入本地 RocksDB 后,value 由 SoftReference 管理,内存紧张时 GC 自动回收,需要时从本地 RocksDB 恢复。因此缓存容量(cache.capacity)只影响 Record1 条目数量,与 value 大小无关。默认值 20000 × 5.0(即 100,000 条)对绝大多数应用都合适,基本不再需要手动配置。
第一原则:与后端数据库一致
Section titled “第一原则:与后端数据库一致”本地 RocksDB 的内容始终与后端数据库保持一致——后端数据库有什么,RocksDB 里就有什么。这一原则贯穿所有操作路径:
- 写入路径:Checkpoint 刷写脏记录时,在同一个
flush调用中同时写入远程数据库和本地 RocksDB。远程写什么,本地就写什么。 - 加载路径:记录从远程数据库加载后,立即写入本地 RocksDB。如果远程没有该记录,本地 RocksDB 中对应的条目也被删除。
- 删除路径:记录从远程数据库删除时,本地 RocksDB 中同步删除。
由于本地 RocksDB 始终是后端数据库的本地镜像,value 被 GC 回收后可安全地从本地 RocksDB 恢复,无需访问远程数据库。
分布式场景下的失效
Section titled “分布式场景下的失效”在分布式场景下,其他实例修改某条记录后,当前实例的记录状态被 GlobalCacheManager 降级为 Invalid。下次访问时,框架从远程数据库重新加载最新数据,并覆盖本地 RocksDB,恢复”与后端数据库一致”的不变式。
Storage 与数据库同步
Section titled “Storage 与数据库同步”Storage
Section titled “Storage”Storage 是 Table 与底层数据库之间的桥梁,在 open() 时创建。对于内存表(isMemory() == true),Storage 为 null。
public final class Storage<K extends Comparable<K>, V extends Bean> { Storage(TableX<K, V> table, Database database, String tableName) Table getTable() Database.Table getDatabaseTable()}脏数据标记与刷写流程
Section titled “脏数据标记与刷写流程”每条记录(Record / Record1)内部维护一个 dirty 标志。数据修改并提交后,记录被标记为脏。刷写流程由 Checkpoint 驱动,支持两种模式:
- CheckpointMode.Immediately — 事务提交后立即将变更写入数据库,不使用脏标记。
- CheckpointMode.Table — 按表批量刷写。Checkpoint 时遍历脏记录,依次执行
encode0()(序列化快照)和flush()(写入数据库),最后cleanup()清理快照状态。
Record 生命周期
Section titled “Record 生命周期”一条记录从访问到持久化,经历以下阶段:
- Load — 通过
TableCache.getOrAdd()创建Record1,首次访问时从 Storage 或本地 RocksDB 加载 value。 - Access — 事务通过
get/getOrAdd获取记录,框架通过全局缓存管理器协商权限(StateInvalid→StateShare→StateModify)。 - Modify — 事务修改 Bean 字段,
Procedure提交时调用Record.commit()设置dirty = true并更新timestamp。 - Flush — Checkpoint 调用
Record.encode0()序列化快照,再调用Record.flush()写入数据库。 - Cleanup — 写入完成后调用
Record.cleanup()清除快照引用和脏标记。
遍历 API
Section titled “遍历 API”Table 提供三类遍历方式,均须在事务外调用:
walk / walkDesc — 遍历数据库(含缓存合并)
Section titled “walk / walkDesc — 遍历数据库(含缓存合并)”// 遍历全表,返回处理的记录数long walk(@NotNull TableWalkHandle<K, V> callback) throws Exceptionlong walkDesc(@NotNull TableWalkHandle<K, V> callback) throws Exception
// 仅遍历 keylong walkKey(@NotNull TableWalkKey<K> callback) throws Exception
// 分页遍历,exclusiveStartKey 为起始 key(不含),返回下一个 key@Nullable K walk(@Nullable K exclusiveStartKey, int proposeLimit, @NotNull TableWalkHandle<K, V> callback) throws Exceptionwalk 从后台数据库遍历,同时与内存缓存合并,能看到最新的已提交数据。注意:新增但未 Checkpoint 的记录可能看不到。每个记录回调时加读锁,回调完成立即释放。
回调接口定义:
@FunctionalInterfacepublic interface TableWalkHandle<K, V> { boolean handle(@NotNull K key, @NotNull V value) throws Exception; // 返回 false 可中断遍历}
@FunctionalInterfacepublic interface TableWalkKey<K> { boolean handle(@NotNull K key) throws Exception;}walkDatabase / walkDatabaseRaw — 直接遍历数据库
Section titled “walkDatabase / walkDatabaseRaw — 直接遍历数据库”// 类型化遍历,看不到本地缓存数据long walkDatabase(@NotNull TableWalkHandle<K, V> callback) throws Exception
// 原始字节遍历(仅 KV 表支持)long walkDatabaseRaw(@NotNull TableWalkHandleRaw callback) throws ExceptionwalkDatabase 直接从后台数据库读取,不经过本地缓存,适合批量导出或后台分析。walkDatabaseRaw 以原始 byte[] 形式返回,效率更高但不做反序列化。
walkMemory — 遍历内存缓存
Section titled “walkMemory — 遍历内存缓存”// 遍历当前缓存中的记录,返回处理的记录数long walkMemory(@NotNull TableWalkHandle<K, V> callback) throws Exception
// 仅遍历缓存中的 keylong walkCacheKey(@NotNull TableWalkKey<K> callback) throws ExceptionwalkMemory 只遍历内存缓存中状态为 StateShare 或 StateModify 的记录。对于内存表,如果配置了容量限制,会从本地 RocksDB 缓存遍历。
TableReadOnly 与 TableDynamic
Section titled “TableReadOnly 与 TableDynamic”TableReadOnly
Section titled “TableReadOnly”TableReadOnly 是一个只读接口,模块可将其暴露给其他模块用于只读访问:
public interface TableReadOnly<K, V, VReadOnly> { @Nullable VReadOnly getReadOnly(@NotNull K key); boolean contains(@NotNull K key); @Nullable V selectCopy(@NotNull K key); // ... 以及所有 walk 方法的只读版本}生成代码会为每张表同时生成 TableReadOnly 实现,value 的只读视图通过 VReadOnly 泛型参数提供。
TableDynamic
Section titled “TableDynamic”TableDynamic 允许运行时动态创建表,复用已有表的 key/value 编解码逻辑:
// 基于母表创建动态表var dynamicTable = new TableDynamic<>(zeze, "dynamic_table", templateTable);动态表通过 zeze.openDynamicTable() 注册,使用与母表相同的序列化方案,但拥有独立的存储空间。可通过 dropTable() 删除。
在 solution.xml 中定义 Table
Section titled “在 solution.xml 中定义 Table”在 solution.xml 的 <module> 内通过 <table> 标签声明:
<module name="demo" id="1"> <bean name="BPlayer"> <variable id="1" name="Level" type="int"/> <variable id="2" name="Name" type="string"/> </bean> <table name="tPlayer" key="long" value="BPlayer"/></module>key 类型约束:key 必须是简单类型(int、long、string 等)或 <beankey> 定义的复合键类型,且必须实现 Comparable。
value 类型约束:value 必须是 <bean> 定义的类型,继承自 Zeze.Transaction.Bean。
可选属性:
| 属性 | 说明 |
|---|---|
suffix | 表名后缀,支持 @ServerId、@AppMainVersion 等变量替换,用于数据隔离 |
autoIncrement | 是否使用自增 key |
代码生成器会根据定义自动生成 TableX<K, V> 的具体子类,开发者通过模块中自动注入的表实例进行操作。