Zeze 如何工作
Zeze 如何工作
Section titled “Zeze 如何工作”读完这篇,你脑子里会有一张清晰的图:数据在 Zeze 里是怎么流动的、事务是怎么保护你的、分布式又是怎么拼上去的。理解了这套心智模型,后面的 API 文档就好读了。
Zeze 的运转可以拆成三层,从内到外分别是:存储过程(事务的执行单位)→ 乐观锁(并发的处理方式)→ 一致性缓存(内存与数据库的关系)。我们一层层拆开看。
第一层:存储过程 —— 事务的执行单位
Section titled “第一层:存储过程 —— 事务的执行单位”存储过程(Procedure) 是 Zeze 里执行业务代码的基本单元。你在协议处理函数里写的逻辑,默认就跑在一个独立的存储过程里。框架会自动创建 Procedure 对象,你通常不用手动建。
一个存储过程的生命周期很简单:
执行业务代码 → 提交时加锁并检查冲突 → 成功则提交,冲突则重做关键在于执行阶段:你写的代码正常读写数据,但写操作产生的变更不会直接改原始数据,而是以日志形式暂存。原始数据在执行阶段始终保持不变。
- 执行阶段:代码读写数据,写操作记成日志,原始数据不动;
- 提交:存储过程返回成功后,日志应用到原始数据,变更生效;
- 回滚:存储过程返回错误或抛异常时,日志被丢弃,数据恢复原状。
嵌套存储过程
Section titled “嵌套存储过程”存储过程可以嵌套——一个过程内部调用另一个过程。内层有独立的回滚范围:内层失败只回滚内层的修改,外层不受影响,可以根据返回值决定是否继续。内层成功时,它的变更合并到外层,最终由最外层统一提交。
底层用**保存点(Savepoint)**实现:每进入一层就建一个保存点,记录这层的日志和回滚范围。对开发者而言嵌套是自然的代码组织方式,通常不用直接碰保存点。
第二层:乐观锁 —— 为什么不死锁
Section titled “第二层:乐观锁 —— 为什么不死锁”传统悲观锁在读写前先加锁,直到事务结束才释放,并发高时容易等待和死锁。Zeze 的乐观锁反其道而行:执行过程完全不加锁,只在提交时才检查冲突。
- 执行阶段:读数据时记录访问的记录及其时间戳(Timestamp);写操作记成日志,不改原始数据。全程不持任何锁。
- 提交阶段:对访问过的所有记录按固定顺序加锁,逐一比对记录的当前时间戳与读取时的时间戳。
- 成功提交:所有时间戳没变 → 无冲突 → 应用日志、递增版本号、释放锁。
- 冲突重做:发现某条记录时间戳已变 → 丢弃日志和访问记录 → 从头重新执行整个存储过程。
为什么不可能死锁
Section titled “为什么不可能死锁”死锁的本质是循环等待。Zeze 从两个层面消除它:
- 执行阶段无锁:多个事务能同时读写相同数据,互不阻塞。冲突不在这阶段解决,推到提交阶段处理。
- 提交阶段按固定全局顺序加锁:所有记录按
TableKey的固定顺序(TreeMap 自然排序)逐个锁定。所有事务都用同一套顺序请求锁,不可能形成环形等待。
这是经典的资源有序分配策略。再加两条实现上的保证(初次加锁不中途返回、重做加锁不”回头”),乐观锁在任何阶段都不会形成环。
「乐观锁不会死锁」不是巧合,是它设计的本质决定的。
重做是乐观锁的代价
Section titled “重做是乐观锁的代价”当并发冲突频繁时,事务可能被反复重做(最多 256 次)。但在大多数业务场景里(游戏、社交),不同用户操作的数据交集很小,冲突概率低,乐观锁的性能优势显著。
框架对重做做了优化:冲突重做时保持已得到的锁,这样第二次执行一般就能成功,不会陷入”一直冲突、永远完不成”的困境。
第三层:一致性缓存 —— 内存与数据库的关系
Section titled “第三层:一致性缓存 —— 内存与数据库的关系”这一层是 Zeze 最精妙的设计,也是它高性能的来源。
缓存的是什么
Section titled “缓存的是什么”Zeze 把数据库里的数据以**记录(Record)**为单位缓存到内存。每条记录维护一个版本号(Timestamp),用于乐观锁的冲突检测。
脏记录与 Checkpoint
Section titled “脏记录与 Checkpoint”事务提交后,被修改的记录标记为脏记录,版本号递增。「脏」意味着内存里的数据已更新,但还没写回数据库。Checkpoint 线程定期收集所有脏记录,把变更批量写入后端数据库,写完清除脏标记。
Checkpoint 的触发策略由 CheckpointMode 配置决定,目前实际可用的值是 Table(默认,按表维度批量刷写);Immediately(提交后立即写)在源码中标注为不可用。
记录的生命周期
Section titled “记录的生命周期”记录由框架自动管理,对应用完全透明:
- 首次访问 → 按需从数据库加载到内存;
- 事务修改 → 提交后变成脏记录;
- Checkpoint → 写回数据库,清除脏标记;
- 不再使用 → 从缓存中移除;
- 提交时发现版本号不匹配 → 触发重做,从数据库重新加载最新数据。
应用代码不需要关心数据来自内存还是数据库——这就是透明的内存-数据库自动同步。
一个改变认知的类比:CPU 缓存
Section titled “一个改变认知的类比:CPU 缓存”理解了上面三层,你会发现 Zeze 的工作方式跟 CPU 访问内存惊人地相似:
| CPU 体系结构 | Zeze 服务器 | |
|---|---|---|
| 高速层 | CPU cache | 服务器内存缓存 |
| 慢速层 | 主存(memory) | 数据库 |
| 速度差距 | 1~2 个数量级(L1 vs 主存) | 数量级(内存 vs 数据库) |
| miss 时 | stall,等内存返回 | 阻塞,等数据库返回 |
| 多核协调 | 缓存一致性协议(Directory-based 等) | GlobalCacheManager |
这个类比非常有解释力:
- 为什么能忍受偶尔的 cache miss——CPU 也忍,只要命中率够高(一般 >99%)。游戏里高频的移动、放技能不在 Zeze 框架下处理,Zeze 事务基本对应”玩家作为人的操作”,吞吐要求不会太高。
- 为什么同步写 cache 也能高性能——内存 map 的读写是纳秒级、无网络往返、无磁盘 I/O,而 SQL 查询要经过网络、解析、执行、回表,两者差好几个数量级。Zeze 把热数据留在内存,只在 Checkpoint 时批量落库,摊薄了写库成本。
- 现代 Java 的加持——Java 21 的虚拟线程(virtual thread)让锁等待和同步 I/O 不再占用操作系统载波线程,带来了跟异步方案同样的性能,又能用同步方式写代码。
第四层:走向分布式
Section titled “第四层:走向分布式”前三层描述的是单实例的缓存机制。分布式部署时,同一个模块可以部署多个 Server 实例,每台都有自己的本地缓存。这时要协调多实例间的数据一致性。
缓存状态与全局协调
Section titled “缓存状态与全局协调”每条记录除了版本号,还维护一个缓存状态(State):
- Modify:当前实例有读写权限,能改数据;
- Share:只读权限,能读不能改;
- Invalid:不持有该记录的有效缓存。
核心规则:同一条记录在全局范围内,最多只有一个实例拥有 Modify 权限。这个权限由 GlobalCacheManager(GCM)统一分配。
权限降级与数据同步
Section titled “权限降级与数据同步”实例 A 持有记录 R 的 Modify 权限时,如果实例 B 也要改 R:
- GCM 要求 A 把 R 降级——A 先把 R 的最新数据 Checkpoint 写库,再释放 Modify 权限;
- B 获得 Modify 权限,从数据库加载 R 的最新数据到本地缓存;
- 此后 B 可以安全地修改 R。
这个过程跟 CPU 的 Directory-based cache coherence 是一回事——GCM 就是那个 Directory。GCM 是单点?不,Zeze 用 Raft 让它高可用。
对开发者透明
Section titled “对开发者透明”这一切你完全不用管。用 Zeze Table 读写数据时,不需要任何额外操作——事务会自动处理多实例间的数据共享和一致性保证。你只管写存储过程逻辑,框架负责:自动管理缓存状态和权限、提交时检测冲突并重做、协调多实例间的数据同步。
把整张图拼起来
Section titled “把整张图拼起来” ┌─────────────────────────────────┐ 客户端请求 ───────────▶ │ 存储过程 (Procedure) │ │ ┌───────────────────────────┐ │ │ │ 执行业务:读写数据 │ │ ← 执行阶段不加锁 │ │ 读 → 记录访问+时间戳 │ │ │ │ 写 → 记日志,不动原始数据 │ │ │ └────────────┬──────────────┘ │ │ ▼ │ │ 提交:按固定顺序加锁,比对时间戳 │ │ │ │ │ 无冲突 │ │ ├──▶ 应用日志 → 脏记录 │ │ │ │ │ │ │ ▼ │ │ │ Checkpoint ──▶ 数据库 │ │ 有冲突 │ │ └──▶ 丢弃日志,重做 │ └─────────────────────────────────┘ 分布式时: GlobalCacheManager 协调多实例权限 (Modify/Share/Invalid) + Raft 高可用记住三句话:
- 写数据先记日志,提交才生效,失败就回滚——这就是事务。
- 执行不加锁,提交按序加锁,冲突就重做——这就是乐观锁,所以不死锁。
- 数据在内存里跑,Checkpoint 自动落库——这就是一致性缓存,所以快。
事务隔离级别:精确 vs 性能
Section titled “事务隔离级别:精确 vs 性能”Zeze 支持两种事务级别,用在不同的业务场景:
- Serializable(默认):访问的所有记录(读或写)提交时都检查是否被改过。保证可串行化,适合需要精确一致性的业务(如余额校验)。
- AllowDirtyWhenAllRead:当事务只有读、没有写时,跳过冲突检查直接返回成功。读到的是执行过程中的快照,不保证可串行化。适合统计、排行榜查询等允许近似值的场景,能显著提高吞吐。
细节见 事务参考。
心智模型建立好了,接下来动手: