- 数据库
- 关系型数据库
- 嵌入式数据库
【免费下载链接】lovefield
Lovefield is a relational database for web apps. Written in JavaScript, works cross-browser. Provides SQL-like APIs that are fast, safe, and easy to use.
Lovefield 是一款运行于浏览器端的 JavaScript 关系型数据库,其事务模型参考传统数据库系统的 ACID 语义设计,但针对 Web 环境做了精简与适配。本文以设计文档 docs/dd/05_transaction.md 为核心骨架,结合规范文档 docs/spec/05_transaction.md 与 lib/proc 目录下的源码实现,完整讲解 Lovefield 的事务创建、生命周期状态机、Runner 调度、锁管理以及 Journal 原子性保证机制。读完本文,你将掌握如何编写隐式与显式事务、理解表级锁与优先级队列的工作原理,并能依据状态转移图诊断事务异常。
图 1:Lovefield 事务状态转换图(出自 docs/dd/images/state.png)
事务的本质:原子执行的逻辑单元
A transaction is an atomic unit of execution that guarantees all queries inside this transaction to be committed as a whole, or none get committed.
事务是原子执行单元:事务内所有查询要么全部提交,要么全部不提交。Lovefield 从两个维度对事务分类:
- 功能维度:只读事务(read-only)与读写事务(read-write);
- 语法维度:隐式事务(implicit)与显式事务(explicit)。
并发控制模型在规范 docs/spec/05_transaction.md 中定义(§5.4 Concurrency Control),采用表级锁(table-level locking),共有 Shared、Reserved、Exclusive 三种锁。
5.1 事务创建:隐式与显式两条路径
隐式事务
调用查询构建器的exec()即创建隐式事务:
db.select().from(job).exec(); // 隐式只读事务 db.insert().into(job).values(rows).exec(); // 隐式读写事务- 所有只读事务都是隐式事务,由
SELECT查询的exec()创建,对其作用域内的表加Shared 锁(允许多个读者并发); - 读写查询(insert/update/delete/insertOrReplace)的
exec()创建隐式读写事务,加Reserved 锁,由 Runner 自动提升为 Exclusive 锁。
性能提示:多个隐式事务的性能劣于单个显式事务。官方建议尽量将写操作合并到尽可能少的显式事务中,以减少锁竞争与快照切换开销。
显式事务
var tx = db.createTransaction();显式事务全部是读写事务。事务的**作用域(scope)**必须显式声明,有两种方式:
- 通过
exec()传入查询数组,Lovefield 解析查询自动推导表作用域; - 通过
begin()显式指定表作用域。
var tx = db.createTransaction(); // 方式一:exec() 自动推导作用域(自动提交,等价于 BEGIN + COMMIT) tx.exec([query1, query2]); // 方式二:begin() 显式声明作用域,随后 attach + commit 细粒度控制 var schema = db.getSchema(); var e = schema.table('Employee'); var v = schema.table('Vacations'); tx.begin([e, v]).then(function() { var q1 = db.select(e.id).from(e).where(e.hireDate.gt(someDate)); return tx.attach(q1); // attach 在内存中执行并返回结果 }).then(function(results) { var ids = results.map(function(row) { return row['id']; }); var q2 = db.update(v).set(v.days, 15).where(v.empId.in(ids)); return tx.attach(q2); }).then(function() { return tx.commit(); // 异步,返回 Promise });注意:exec()、commit()、rollback()都会使事务进入终止状态(FINALIZED),此后任何对事务对象成员函数的调用都会抛出错误(对应源码 lib/proc/transaction.js 中的lf.Exception105/107)。事务不可复用,条件化逻辑必须拆分为多个事务或用事务内原语组合。
事务执行顺序与快照语义
事务严格按照exec()被调用(即提交)的先后顺序执行。规范 docs/spec/05_transaction.md 给出时序示例:
t0 ---------- t1 ---------- t2 ---------- t3 -----------> S0 S1 |create tx1 |create tx2 |tx2 exec and committed |tx1 exec and committed虽然tx1先创建,但tx2先执行并提交,因此tx1实际基于快照S1(提交时刻的快照)而非创建时刻的快照S0。请将事务对象的exec()视为BEGIN TRANSACTION与COMMIT的合体。若为select()创建显式事务,事务内所有 SELECT 均从同一快照读取,保证一致性。
5.2 事务执行:八个状态的生命周期
设计文档定义了事务生命周期的 8 个状态:
| # | 状态 | 含义 |
|---|---|---|
| 1 | CREATED | 初始状态,事务已创建 |
| 2 | ACQUIRING_SCOPE | 从锁管理器为涉及的表获取锁 |
| 3 | ACQUIRED_SCOPE | 所需锁已授予 |
| 4 | EXECUTING_QUERY | 执行单个查询 |
| 5 | EXECUTING_AND_COMMITTING | 立即执行并提交整个事务 |
| 6 | COMMITTING | 提交中 |
| 7 | ROLLING_BACK | 回滚中 |
| 8 | FINALIZED | 执行终结 |
状态转移见图 1。对应源码为 lib/proc/transaction.js 中的lf.proc.TransactionState_枚举与lf.proc.StateTransitions_映射:
CREATED→ACQUIRING_SCOPE或EXECUTING_AND_COMMITTING;ACQUIRING_SCOPE→ACQUIRED_SCOPE;ACQUIRED_SCOPE→EXECUTING_QUERY/COMMITTING/ROLLING_BACK(也可经 attach 回到ACQUIRED_SCOPE);EXECUTING_QUERY→ACQUIRED_SCOPE或FINALIZED;EXECUTING_AND_COMMITTING/COMMITTING/ROLLING_BACK→FINALIZED。
任何不在该映射中的转移都会抛出错误 107Invalid transaction state transition。方法映射为:exec()走EXECUTING_AND_COMMITTING路由,begin()走ACQUIRING_SCOPE路由,attach()走EXECUTING_QUERY,commit()/rollback()分别走COMMITTING/ROLLING_BACK。
事务创建后,会生成关联的TransactionTask或UserQueryTask。Task是 Runner 中的执行单元(逻辑事务),被调度进 Runner 的执行队列等待执行。两种执行模式对应两套状态转移路径:
tx.exec([builder1, builder2, ...]):语法上隐含自动提交,选择EXECUTING_AND_COMMITTING路由,由UserQueryTask承载多个 task item(见 lib/proc/user_query_task.js);tx.begin(); tx.attach(...); tx.commit();模式:用户希望细粒度控制提交/回滚,选择ACQUIRING_SCOPE路由,由TransactionTask承载(见 lib/proc/transaction_task.js)。
5.3 Runner:调度与并发控制核心
Runner 的核心逻辑是按正确顺序调度并执行任务。每个 Task 是原子执行单元,带有作用域。Lovefield 允许同一张表多个并发读者,直到有写者请求锁。Runner 基于作用域安排执行顺序,并尽量并发执行尽可能多的事务。
源码 lib/proc/runner.js 中的scheduleTask()会检查任务优先级:若高于USER_QUERY_TASK/TRANSACTION_TASK(即高优先级任务),先调用lockManager_.clearReservedLocks()清空持有的 Reserved 锁,再插入队列并触发consumePending_()。consumePending_()遍历队列快照,对只读任务执行两阶段加锁RESERVED_READ_ONLY → SHARED,对读写任务执行RESERVED_READ_WRITE → EXCLUSIVE(见requestTwoPhaseLock_()),成功后从队列移除并执行。任务完成或出错时释放锁、resolve/reject 并再次consumePending_()处理后续任务。
5.3.1 六类 Runner 任务与优先级
优先级数值越低优先级越高(定义于 lib/proc/task.js 的lf.proc.TaskPriority):
| 任务类型 | 优先级 | 说明 |
|---|---|---|
| Export | 0 | 将当前数据库全部数据导出为 JavaScript 对象 |
| Import | 0 | 向空数据库导入数据 |
| Observer Query | 0 | 重新执行被观察的 SELECT 查询 |
| External Change | 1 | 外部(如跨标签页)数据变更 |
| User Query | 2 | 用户查询 |
| Transaction | 2 | 显式事务 |
Runner 实现优先级队列(Runner.TaskQueue_,用binaryInsert按优先级插入),高优先级任务抢占低优先级任务先执行。ImportTask与ExportTask是特殊任务,优先级最高,且会锁住全部表。
观察者查询(Observer)与即时任务
Task 对象还用于实现被观察的 SELECT 查询:当查询被 observe 时,若其作用域发生变化、或查询绑定了不同的参数集,Task 会检查是否需要重新执行。
所有观察者存放在全局ObserverRegistry(查询 → 观察者的映射,相关服务见 lib/observer_registry.js)。观察者在以下两种情形被触发:
- 被观察的作用域发生变化;
- 属于被观察作用域的表被修改。
Runner 每完成一个任务,都会检查该任务是否改动了被观察查询的作用域;若是,则将那些查询调度为即时任务(immediate task)。同时 Runner 检查绑定值相比上次是否有变化,若有则运行快照差异(snapshot diff)逻辑并触发观察者。即时观察者任务会像传统 DBMS 的TRIGGER一样延迟所有待处理任务——用户需自行评估可能的性能损失并提前规划。相关实现可参考 lib/proc/observer_query_task.js 与 lib/proc/user_query_task.js 中的scheduleObserverTask_()/notifyObserversDirectly_()。
5.4 Journal:事务原子性的内存快照
事务的原子性由Journal保证,它是事务执行状态的内存快照(源码位于 lib/cache/journal.js,由lf.cache.Journal实现)。
执行流程:
- 事务执行时,其中查询的物理计划逐一执行;
- 需要改动数据库内容的物理计划,其变更被暂存在 Journal 中(
insert/insertOrReplace/update/remove方法记录到tableDiffs_,每张表一个lf.cache.TableDiff); - 全部计划执行完毕后,生成事务前后状态的 diff,刷写(flush)到数据存储;
- Lovefield 假定数据存储提供原子写入——写入要么全部成功、要么全部失败——由此事务被整体提交或回滚。
Journal 同时是作用域守卫:构造时接收表作用域集合,任何对作用域外表的访问都会抛出错误 106Attempt to access {0} outside of specified scope(checkScope_())。约束(非空、外键、级联更新/删除)也在 Journal 内即时或延迟检查:外键级联逻辑见updateByCascade_()/removeByCascade_(),延迟约束检查见checkDeferredConstraints()。若约束违规,Journal 进入pendingRollback_状态,只允许回滚;回滚时对tableDiffs_取逆(getReverse())并由InMemoryUpdater还原内存缓存与索引。此外,持久化索引的变更通过getIndexDiff()提取后在提交时一并刷盘。
锁类型与两阶段锁(Concurrency Control)
规范 docs/spec/05_transaction.md §5.4 定义了三种表级锁:
- Shared:读者锁,可授予多个读者;
- Reserved:try-writer 锁,阻止对该表授予新的 Shared/Reserved 锁,但表尚未被修改;
- Exclusive:写者锁,阻止授予任何新锁,表即将被修改;只有持有 Exclusive 锁才能修改表。
源码 lib/proc/lock_manager.js 中lf.proc.LockType实际细分为四类:RESERVED_READ_ONLY、SHARED、RESERVED_READ_WRITE、EXCLUSIVE,配合LockTableEntry_实现两阶段锁定(先 Reserved 再提升)。canAcquireLock_()明确各类型互斥关系:Exclusive 要求无 Shared/Reserved 且已持有本任务的 Reserved_READ_WRITE;Shared 要求无 Exclusive/Reserved_READ_WRITE 且已持有本任务的 Reserved_READ_ONLY。
锁创建场景如下表:
| 引发锁创建的函数 | 创建的锁 |
|---|---|
exec()of aselect()查询 | Shared |
exec()of aninsert()查询 | Reserved |
exec()of aninsertOrReplace()查询 | Reserved |
exec()of anupdate()查询 | Reserved |
exec()of andelete()查询 | Reserved |
begin()或exec()of a transaction | Reserved |
锁的升级与释放:所有 Reserved 锁由查询 Runner 自动升级为 Exclusive 锁。由exec()创建的锁在 Lovefield 完成必要数据写入后自动释放;而由事务begin()创建的锁在rollback()/commit()之前既不会升级也不会释放。这意味着多个闭包并发通过事务写库时可能产生死锁(用户代码生成的死锁),需要用户自行预防与检测。
事务使用的完整模式总结
| 场景 | 推荐写法 | 特点 |
|---|---|---|
| 单条读查询 | db.select().from(t).exec() | 隐式只读,Shared 锁 |
| 单条写查询 | db.insert().into(t).values(r).exec() | 隐式读写,Reserved→Exclusive 自动释放 |
| 多条查询原子提交 | db.createTransaction().exec([q1, q2]) | 自动提交,UserQueryTask |
| 依赖前序结果的序列化 | begin([t1, t2])+attach(q)+commit() | 细粒度控制,TransactionTask |
| 放弃变更 | begin(...)+attach(...)+rollback() | 回滚,释放锁 |
| 调试执行计划 | query.explain() | 只生成计划不执行,见 docs/spec/05_transaction.md §5.1 |
规范明确指出:exec()之后不能再在事务内发起查询;事务对象在终止后所有成员函数调用都会抛错。若需条件化行为,要么拆分多个事务,要么用事务内可用原语组合。
相关测试与进一步阅读
- 事务端到端测试:tests/proc/end_to_end_transaction_test.js、tests/proc/end_to_end_test.js
- 锁管理器测试:tests/proc/lock_manager_test.js
- Runner 测试:tests/proc/runner_test.js
- 观察者任务测试:tests/proc/observer_query_task_test.js
- 配套设计文档:docs/dd/03_life_of_db.md、docs/dd/04_query_engine.md
- 事务规范原文:docs/spec/05_transaction.md
本文所有结论均可在上述源码与测试中得到验证;若要深入阅读 Runner 的任务调度与锁管理细节,建议从 lib/proc/runner.js 与 lib/proc/lock_manager.js 入手。
- 数据库
- 关系型数据库
- 嵌入式数据库
【免费下载链接】lovefield
Lovefield is a relational database for web apps. Written in JavaScript, works cross-browser. Provides SQL-like APIs that are fast, safe, and easy to use.
相关推荐
Lovefield 事务机制深度解析:原子执行、Runner 调度与 Journal 提交模型
Lovefield 事务机制深度解析:原子执行、Runner 调度与 Journal 提交模型 Lovefield 是一个运行在浏览器端的 JavaScript
关系型数据库数据库前端WinUtil 上手指南:30 分钟搞定装软件、系统调优、修故障、管更新
WinUtil 上手指南:30 分钟搞定装软件、系统调优、修故障、管更新 你盯着安装向导里第四个勾选框——“安装 XX 工具包(推荐)”——犹豫:勾了,明天桌面
数据库关系型数据库嵌入式数据库toyDB MVCC 事务机制深度解析:基于快照隔离的多版本并发控制实现
toyDB MVCC 事务机制深度解析:基于快照隔离的多版本并发控制实现 toyDB 是一个用 Rust 编写的分布式 SQL 数据库教学项目。本文以其存储层的
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考