Lovefield 事务机制深度解析:状态机、并发控制与 Journal 快照

📅 发布时间:2026/10/9 1:26:17
Lovefield 事务机制深度解析:状态机、并发控制与 Journal 快照
数据库关系型数据库嵌入式数据库【免费下载链接】lovefieldLovefield 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 原子性保证机制。读完本文你将掌握如何编写隐式与显式事务、理解表级锁与优先级队列的工作原理并能依据状态转移图诊断事务异常。图 1Lovefield 事务状态转换图出自 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_COMMITTINGACQUIRING_SCOPE→ACQUIRED_SCOPEACQUIRED_SCOPE→EXECUTING_QUERY/COMMITTING/ROLLING_BACK也可经 attach 回到ACQUIRED_SCOPEEXECUTING_QUERY→ACQUIRED_SCOPE或FINALIZEDEXECUTING_AND_COMMITTING/COMMITTING/ROLLING_BACK→FINALIZED。任何不在该映射中的转移都会抛出错误 107Invalid transaction state transition。方法映射为exec()走EXECUTING_AND_COMMITTING路由begin()走ACQUIRING_SCOPE路由attach()走EXECUTING_QUERYcommit()/rollback()分别走COMMITTING/ROLLING_BACK。事务创建后会生成关联的TransactionTask或UserQueryTask。Task是 Runner 中的执行单元逻辑事务被调度进 Runner 的执行队列等待执行。两种执行模式对应两套状态转移路径tx.exec([builder1, builder2, ...])语法上隐含自动提交选择EXECUTING_AND_COMMITTING路由由UserQueryTask承载多个 task item见 lib/proc/user_query_task.jstx.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。观察者在以下两种情形被触发被观察的作用域发生变化属于被观察作用域的表被修改。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 scopecheckScope_()。约束非空、外键、级联更新/删除也在 Journal 内即时或延迟检查外键级联逻辑见updateByCascade_()/removeByCascade_()延迟约束检查见checkDeferredConstraints()。若约束违规Journal 进入pendingRollback_状态只允许回滚回滚时对tableDiffs_取逆getReverse()并由InMemoryUpdater还原内存缓存与索引。此外持久化索引的变更通过getIndexDiff()提取后在提交时一并刷盘。锁类型与两阶段锁Concurrency Control规范 docs/spec/05_transaction.md §5.4 定义了三种表级锁Shared读者锁可授予多个读者Reservedtry-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_WRITEShared 要求无 Exclusive/Reserved_READ_WRITE 且已持有本任务的 Reserved_READ_ONLY。锁创建场景如下表引发锁创建的函数创建的锁exec()of aselect()查询Sharedexec()of aninsert()查询Reservedexec()of aninsertOrReplace()查询Reservedexec()of anupdate()查询Reservedexec()of andelete()查询Reservedbegin()或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.jsRunner 测试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 入手。赞分享数据库关系型数据库嵌入式数据库【免费下载链接】lovefieldLovefield 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 事务机制深度解析原子执行、Runner 调度与 Journal 提交模型Lovefield 事务机制深度解析原子执行、Runner 调度与 Journal 提交模型 Lovefield 是一个运行在浏览器端的 JavaScript关系型数据库数据库前端WinUtil 上手指南30 分钟搞定装软件、系统调优、修故障、管更新WinUtil 上手指南30 分钟搞定装软件、系统调优、修故障、管更新 你盯着安装向导里第四个勾选框——“安装 XX 工具包推荐”——犹豫勾了明天桌面数据库关系型数据库嵌入式数据库toyDB MVCC 事务机制深度解析基于快照隔离的多版本并发控制实现toyDB MVCC 事务机制深度解析基于快照隔离的多版本并发控制实现 toyDB 是一个用 Rust 编写的分布式 SQL 数据库教学项目。本文以其存储层的上一篇终极指南使用applera1n高效绕过iOS 15-16激活锁下一篇如何用applera1n免费绕过iOS激活锁完整指南与操作教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考