☰
Lovefield 事务机制深度解析:状态机、并发控制与 Journal 快照
2026/10/9 1:26:12 网站建设 项目流程
  • 数据库
  • 关系型数据库
  • 嵌入式数据库

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/lo/lovefield
点击查看免费下载

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 个状态:

#状态含义
1CREATED初始状态,事务已创建
2ACQUIRING_SCOPE从锁管理器为涉及的表获取锁
3ACQUIRED_SCOPE所需锁已授予
4EXECUTING_QUERY执行单个查询
5EXECUTING_AND_COMMITTING立即执行并提交整个事务
6COMMITTING提交中
7ROLLING_BACK回滚中
8FINALIZED执行终结

状态转移见图 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):

任务类型优先级说明
Export0将当前数据库全部数据导出为 JavaScript 对象
Import0向空数据库导入数据
Observer Query0重新执行被观察的 SELECT 查询
External Change1外部(如跨标签页)数据变更
User Query2用户查询
Transaction2显式事务

Runner 实现优先级队列(Runner.TaskQueue_,用binaryInsert按优先级插入),高优先级任务抢占低优先级任务先执行。ImportTask与ExportTask是特殊任务,优先级最高,且会锁住全部表。

观察者查询(Observer)与即时任务

Task 对象还用于实现被观察的 SELECT 查询:当查询被 observe 时,若其作用域发生变化、或查询绑定了不同的参数集,Task 会检查是否需要重新执行。

所有观察者存放在全局ObserverRegistry(查询 → 观察者的映射,相关服务见 lib/observer_registry.js)。观察者在以下两种情形被触发:

  1. 被观察的作用域发生变化;
  2. 属于被观察作用域的表被修改。

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实现)。

执行流程:

  1. 事务执行时,其中查询的物理计划逐一执行;
  2. 需要改动数据库内容的物理计划,其变更被暂存在 Journal 中(insert/insertOrReplace/update/remove方法记录到tableDiffs_,每张表一个lf.cache.TableDiff);
  3. 全部计划执行完毕后,生成事务前后状态的 diff,刷写(flush)到数据存储;
  4. 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 transactionReserved

锁的升级与释放:所有 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.

项目地址:https://gitcode.com/gh_mirrors/lo/lovefield
点击查看免费下载

相关推荐

上一篇:终极指南:使用applera1n高效绕过iOS 15-16激活锁
下一篇:如何用applera1n免费绕过iOS激活锁:完整指南与操作教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询