精通 ClientDB 事务:乐观更新、runTransaction 与回滚 rebase 机制完整指南

【免费下载链接】clientdb ClientDB is an open source in-memory database for enabling real-time web apps. 【免费下载链接】clientdb 项目地址: https://gitcode.com/gh_mirrors/cl/clientdb

ClientDB 是一款开源的内存数据库(in-memory database),专为实时 Web 应用打造。而事务(Transaction)是它最核心、也最容易被新手忽略的能力:借助 runTransaction,你可以把多次数据变更打包成一个原子操作,配合乐观更新先让 UI 即时生效,再在失败时通过回滚(rollback)+ rebase 机制把数据精确恢复到正确状态。本文带你彻底搞懂这套机制。

ClientDB 内存数据库事务机制教程配图

一、ClientDB 是什么?为什么需要事务?

ClientDB 的客户端核心完全用 TypeScript 编写,基于 MobX 实现响应式数据流。它的定位是:数据变更在内存中即时生效,UI 零延迟响应,之后再把变更同步到服务端。

这种"先改后同步"的模式带来一个经典问题:

  • 如果同步到服务端失败了,内存里已经改掉的数据怎么办?
  • 如果多个事务交叉操作同一条数据,回滚时会不会把别人的改动也冲掉?

ClientDB 用三件套回答了这两个问题:乐观更新 + 事务分组 + rebase 回滚。

💡 安装方式:yarn add @clientdb/core,更多安装细节见 core/README.md。

二、三个核心概念:乐观更新、事务、rebase

1️⃣ 乐观更新(Optimistic)

所有事务的变更立即写入内存,实体和 UI 马上就能读到新值,无需等待服务端确认。这就是"乐观"的含义——先假设成功,失败了再补救。

2️⃣ 事务(Transaction)

runTransaction 把回调中的所有变更"记账"到一起。每条变更除了应用到内存,还会被注册到对应实体身上,直到整个事务被 commit 才注销。事务未提交前,服务端是"可反悔"的。

3️⃣ 回滚与 rebase(重放)

这是最精妙的部分。当某个事务被拒绝(reject)时:

  1. 先撤销:把该事务的每条变更从内存中回滚(rollback 直接走 store,不会重新触发同步事件);
  2. 再重放:如果这期间还有其他未提交事务改过同一条数据,ClientDB 会把那些后续变更重新应用一遍(rebase),让实体状态"仿佛被拒绝的那次变更从未发生过"。

这正是官方源码注释中描述的架构设计,详见 core/transaction.ts:

// undo it in memory so we're sure entity has original data it had before change
// rebase all changes made in the meantime so entity will have all other changes
// applied as if rejected change never happened

三、runTransaction 实战:快速上手第一个事务

runTransaction 的返回值是一个二元组 [结果, 事务对象],事务对象提供 commit / reject / undo 三个操作。

import { runTransaction } from "@clientdb/core";

// 手动控制:拿到事务对象,稍后再决定是否保留
const [, transaction] = runTransaction(() => {
  db.entity(owner).create({ name: "A" });
  db.entity(owner).create({ name: "B" });
});

// 同步到服务端失败时,一行代码撤销全部变更
transaction.reject();

如果回调中主动抛出异常,事务会自动回滚,无需手动处理:

try {
  runTransaction(() => {
    db.entity(owner).create({ name: "A" });
    throw new Error("校验失败");
  });
} catch (error) {
  // 此时内存数据已恢复原样
}

这一行为有完整的测试覆盖,可以阅读 core/tests/transactions.spec.ts 中 "is optimistic" 与 "will not commit data if failed" 两个用例。

自动回滚的边界情况:rebase 生效场景

设想连续三个事务都修改同一个人物的名字:Adam → A → B → C。如果此时拒绝最老的 A,当前值应仍是 C(A 的撤销不能影响 B、C);如果拒绝最新的 C,名字则回落到 B。测试用例完整验证了这个行为:

const [, changeToA] = runTransaction(() => adam.update({ name: "A" }));
const [, changeToB] = runTransaction(() => adam.update({ name: "B" }));
const [, changeToC] = runTransaction(() => adam.update({ name: "C" }));

expect(adam.name).toBe("C");
changeToA.reject();   // 撤销中间某条 → 仍是 "C"
expect(adam.name).toBe("C");
changeToC.reject();   // 撤销最新一条 → 回到 "B"
expect(adam.name).toBe("B");

对应源码是 core/tests/transactions.spec.ts 中的两个 "rebase" 相关用例,这也是理解 rebase 机制最好的参照。

四、commit / reject / undo:一张表看懂三种结局

操作含义是否重新触发事件/同步典型场景
commit事务确认,变更正式生效✅ 提交时统一发出服务端同步成功
reject事务失败,静默撤销❌ 直接改内存,不重发事件服务端拒绝、网络失败
undo手动"反做",即使事务已成功✅ 会重新发出变更事件用户点击"撤销"按钮

注意一个关键区别(源码注释原话):rollback 用于事务失败,undo 用于成功后反悔——后者因为要同步给其他端,必须走正常的变更事件链路。该区别定义在 core/transaction.ts 顶部的架构说明中。

五、监听 transaction 事件:打通前后端同步

事务最终会通过 transaction 事件暴露给 db 层。这是把 ClientDB 对接服务端同步(如 WebSocket 推送)的天然挂点:

db.events.on("transaction", ({ transaction }) => {
  // 拿到事务包含的所有变更,发送给服务端
  const changes = transaction.getChanges();
  // sendToServer(changes)
});

两个细节值得注意:

  • 若变更不在 runTransaction 中,每条变更会被包装成"单步事务"逐个发出(见 core/client.ts);
  • 事务不支持嵌套,在事务内再调用 runTransaction 会直接抛错(core/transaction.ts);
  • 事件类型定义在 core/events.ts。

六、rebase 内部原理:三步走(进阶)

想深入源码的话,reject 的执行链路非常清晰:

  1. 注册期:每次变更通过 pushChange 被记入事务,同时用 WeakMap 按实体注册(registerEntityChange);
  2. 撤销期:reject 逐条回滚变更,并调用 removeChangeAndUpdateNextTransaction —— 当撤销的变更"夹在"队列中间时,把它记录的原值(before 值)注入到紧随其后的变更中,保证后续变更重放时能恢复到正确基线;
  3. 重放期:对受影响实体调用 rebaseEntityChanges,按顺序重新应用所有仍挂起的变更,最终内存状态 = 原始数据 + 其余有效变更。

七、新手常见疑问 FAQ

Q1:不加 runTransaction 直接改数据行不行? 可以。单条变更会被自动包装成单步事务发出,只是失去了"多变更原子性"和统一回滚能力。

Q2:为什么事务不能跨多个 ClientDB 实例? pushChange 会校验所有变更来自同一个 db 实例,跨实例直接抛错(core/transaction.ts)。一个页面建议只维护一个数据库实例。

Q3:reject 和 undo 到底该选哪个? 服务端说"不" → reject(静默回滚);用户说"撤销" → undo(需要让其他端知道)。

八、关键文件速查

模块路径作用
事务核心实现core/transaction.tsrunTransaction、commit/reject/undo、rebase
实体客户端core/client.ts变更如何被捕获进当前事务
事件类型定义core/events.tstransaction 事件结构
数据库实例core/db.tscreateClientDb、事件总线
事务行为测试core/tests/transactions.spec.ts乐观更新与 rebase 的全量用例

小结:ClientDB 的事务模型可以浓缩为一句话——乐观更新让 UI 快如闪电,事务让变更可打包,reject + rebase 让失败无感恢复。掌握 runTransaction 的三个返回值操作和 transaction 事件,你就能把它平滑接入任何实时同步架构 🚀

【免费下载链接】clientdb ClientDB is an open source in-memory database for enabling real-time web apps. 【免费下载链接】clientdb 项目地址: https://gitcode.com/gh_mirrors/cl/clientdb

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐