首页 / 资讯中心 / 文章详情

Lovefield 事务机制全解析:从执行计划、隐式/显式事务到表级并发控制

Lovefield 事务机制全解析:从执行计划、隐式/显式事务到表级并发控制 ★ FEATURED ARTICLE
关系型数据库数据库前端【免费下载链接】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/lov/lovefield点击查看免费下载本篇技术指南以 Lovefield 官方规范文档 docs/spec/05_transaction.md 为骨架系统讲解 Lovefield基于 JavaScript 的浏览器端关系型数据库中查询是如何被翻译为执行计划、如何通过隐式事务与显式事务保证原子性以及表级锁Shared / Reserved / Exclusive并发控制的完整原理。读完本文你将掌握exec()、begin()、attach()、commit()、rollback()等核心 API 的正确用法、快照与死锁语义并能在源码层面理解 Lovefield 查询引擎与锁管理器的真实工作方式。1. 执行计划Execution Plan查询如何进入引擎在 Lovefield 中无论查询由哪种方式触发最终都会交给查询引擎Query Engine执行。触发执行的入口有三个lf.query.Builder#exec单个查询构建器直接执行lf.Transaction#exec显式事务批量执行lf.Transaction#attach显式事务中逐步附加执行。上述任一入口被调用时查询都会被转换成查询引擎可以理解的执行计划execution plan。执行计划是一棵物理计划树树上的每个节点对应一个具体的执行步骤如table_access_full_step、join_step、project_step等可参考 lib/proc/ 目录下的 step 实现。1.1explain()调试慢查询的关键工具Lovefield 提供了explain()方法它生成执行计划但不会真正执行查询非常适合用来诊断慢查询、检查优化器logical_plan_rewriter、physical_plan_rewriter到底生成了什么样的计划。规范文档给出了如下示例var p db.getSchema().table(Photo); var a db.getSchema().table(Album); var query db.select(). from(p). innerJoin(a, p.albumId.eq(a.id)). where(a.id.eq(1)); console.log(query.explain()); // DEBUG only从源码看explain()的实现位于 lib/query/base_builder.js它取出物理计划树的根节点对每个节点调用toContextString(this.query)再通过lf.tree.toString将整棵树序列化为易读的文本树从而把「全表扫描还是走索引」「连接顺序如何」等关键信息直观呈现给开发者。与之对比同文件中的exec()lib/query/base_builder.js会先执行assertExecPreconditions()校验查询前置条件然后将查询打包成lf.proc.UserQueryTask交给 Runner 调度执行。注意explain()是 DEBUG 专用方法不应出现在生产代码中。2. 隐式事务Implicit Transaction最简洁的默认执行方式绝大多数场景下单个查询都在隐式事务中执行因为其语法极其简洁ds.connect().then(function(db) { var item db.getSchema().table(Item); // SELECT * FROM Item; return db.select().from(item).exec(); });这段代码的完整流程是db.select()返回一个查询构建器query builder查询构建器通过一系列增强方法from、where、orderBy、limit等逐步完善查询上下文由于参数经过结构化处理而非字符串拼接天然避免了 SQL 注入风险调用exec()后查询被派发给查询引擎查询引擎为该查询创建隐式事务、执行并返回结果。从源码视角印证lf.query.BaseBuilder#exec内部直接new lf.proc.UserQueryTask(this.global, [this.getTaskItem()])并this.runner_.scheduleTask(queryTask)Runner会根据任务类型READ_ONLY或READ_WRITE自动为其请求两阶段锁并执行见 lib/proc/runner.js。也就是说隐式事务是 Lovefield 为单条查询自动包上的原子执行外壳用户无需也无权干预其提交/回滚时机。3. 显式事务Explicit Transaction原子执行多条查询显式事务在 Lovefield 中是读-写事务READ_WRITE其设计目标是以原子方式一次性执行多条查询。事务接口lf.Transaction定义于 lib/transaction.js包含exec、begin、attach、commit、rollback、stats六个成员方法事务类型枚举lf.TransactionType同时定义了READ_ONLY 0与READ_WRITE 1。3.1exec([queries])一次性执行并提交最简单的显式事务用法是db.createTransaction()拿到事务对象后用tx.exec([query1, query2, ...])顺序执行多条查询// Get a transaction object first. var tx1 db.createTransaction(); // exec in order: query 1 first then query2, guaranteed snapshot tx1.exec([query1, query2]); // exec in order: query 3 first then query4 query3.exec().then(function() { query4.exec(); }); // exec in parallel (syntactically): tx1, query3, query5, query6 query5.exec(); query6.exec(); var tx2 db.createTransaction(); tx2.exec([query7, query8]);要点解读tx.exec()返回 Promise与普通查询的exec()一致原子性保证事务内所有查询一次性刷新flush到持久化存储要么全部生效、要么全部不生效快照语义如果事务由select()查询构成这些查询将基于同一快照执行保证读一致性。源码层面的对应实现见 lib/proc/transaction.jsexec()会先做非法状态迁移检查对每个 queryBuilder 调用assertExecPreconditions()并收集getTaskItem()然后封装为lf.proc.UserQueryTask交给 Runner 调度全部成功后再统一返回各查询的 payload。3.2 执行顺序与快照exec()相当于「BEGIN COMMIT」事务总是按库接收到exec()调用的顺序执行因此可能出现与 SQL 世界完全对应的竞争场景t0 ---------- t1 ---------- t2 ---------- t3 ----------- S0 S1 |create tx1 |create tx2 |tx2 exec and committed |tx1 exec and committed虽然tx1创建于tx2之前但tx1的exec()调用发生在tx2已提交之后所以tx1实际作用于快照S1而非其创建时刻的快照S0。规范文档给出的关键结论请把事务对象的exec()同时视为BEGIN TRANSACTION和COMMIT。这也是为什么begin()/exec()会触发锁创建见第 4 节锁表。3.3 终止状态事务一旦结束不可复用当事务无论是隐式还是显式结束后该事务内不能再发起任何查询。试图复用事务对象会直接报错。这意味着任何条件分支逻辑必须横跨多个事务或者由事务内部可用的原语primitive组合实现。exec()、commit()、rollback()都会使事务进入终止状态termination state此后调用事务对象的任何成员函数都会抛出错误。源码证据非常清晰lib/proc/transaction.js 将事务生命周期建模为有限状态机lf.proc.TransactionState_共有 8 个互斥状态CREATED → ACQUIRING_SCOPE → ACQUIRED_SCOPE → EXECUTING_QUERY → ... ↘ COMMITTING / ROLLING_BACK 各终止路径最终汇聚到 FINALIZED任何不在StateTransitions_表中的迁移都会抛出lf.Exception(107)Invalid transaction state transition。对应测试 tests/proc/end_to_end_transaction_test.js 中大量使用lf.testing.util.assertThrowsError(107, rollbackFn)来验证「已提交/已回滚/正在执行中」等场景下再次调用rollback()等操作会被拒绝。3.4begin()attach()事务内顺序查询并引用前序结果很多业务场景要求同一事务内的查询顺序执行且后面的查询要引用前面查询的结果例如先查出符合条件的员工 id再批量更新其休假天数。Lovefield 通过**事务附加transaction attachment**实现这一点var schema db.getSchema(); var e schema.table(Employee); var v schema.table(Vacations); // Get a transaction object as usual. var tx db.createTransaction(); // Secure the scope of queries so that there will be no surprise. // All tables will be exclusively locked. See Concurrency Control section. tx.begin([e, v]).then(function() { var q1 db.select(e.id).from(e).where(e.hireDate.gt(someDate)); // Attach will actually run the query in memory and get back the results. return tx.attach(q1); }).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() { // Commit the transaction, which writes everything into database. // Remember, commit() is an asynchronous call that returns a Promise. return tx.commit(); }).then(function() { // ... });对这段代码的深度解读tx.begin([e, v])声明事务作用域scope传入事务允许访问的所有表Lovefield 会为这些表全部加排他锁见第 4 节并发控制从而杜绝并发写者带来「惊喜」返回的 Promise 在所有锁获取完成后 resolvetx.attach(q1)实际在内存中执行查询并返回结果——注意attach()只是执行查询、读取结果此时并不把变更刷盘tx.commit()把事务内所有变更一次性写入数据库它是异步调用返回 Promise必须return以串起 Promise 链。3.5 更多实践exec()提交与rollback()回滚规范文档还给出了两种变体用法var tx2 db.createTransaction(); tx2.begin([e]).then(function() { return tx.attach( db.update(e).set(e.location, MTV).where(e.id.lt(1000))); }).then(function() { // exec() can be used to commit the transaction, too. return tx.exec( [db.update(e).set(e.location, LAX).where(e.id.gte(1000))]); }); var tx3 db.createTransaction(); tx3.begin([v]).then(function() { return tx.attach(db.update(v).set(v.days, 0); }).then(function() { // Canceling everyones vacation is not really a good idea. // Remember, rollback() is an asynchronous call that returns a Promise. return tx.rollback(); });tx.exec([...])可以兼作提交手段begin()之后用exec()执行剩余查询事务同样会提交tx.rollback()回滚事务内全部变更同样是异步调用、返回 Promise且只允许在事务尚未提交时调用。注tx3示例在begin([v])后误用了tx.attach规范原文如此读者可理解为「对事务作用域内查询的附加执行」实际编码时请保持事务变量一致。3.6 源码级生命周期TransactionTask 与状态机显式事务的底层是lf.proc.TransactionTasklib/proc/transaction_task.jsacquireScope()将 TransactionTask 投递给 Runner 调度确保作用域内所有表的锁先被获取attachQuery()直接执行物理计划根节点taskItem.plan.getRoot().exec(this.journal_, taskItem.context)失败时调用journal_.rollback()并通过execResolver_拒绝以释放已持有的锁commit()通过this.backStore_.createTx(...)创建存储层事务并提交成功后调度 ObserverTask 通知订阅了相关表的观察者可参考 lib/proc/query_task.js 中隐式事务同样经由backStore_.createTx落盘rollback()回滚 journal 并 resolve不触碰存储层。因此隐式事务与显式事务在提交阶段殊途同归都由各自的任务UserQueryTask / TransactionTask在合适的时机调用后端存储IndexedDB、WebSQL、LocalStorage、Memory 或 Firebase见 lib/back_store.js 与 lib/backstore/ 下各实现的createTx完成持久化。4. 并发控制Concurrency Control表级三型锁Lovefield 采用表级锁定table-level locking锁管理器lf.proc.LockManagerlib/proc/lock_manager.js为每个数据库表维护一个锁表项LockTableEntry_。规范文档对外概括为三种锁锁类型语义Shared共享锁读锁可同时授予多个读者Reserved预留锁尝试写锁。阻止目标表再被授予新的 Shared 或 Reserved 锁但此时表尚未被真正修改Exclusive排他锁写锁。阻止对目标表授予任何新锁表即将被修改只有持有 Exclusive 锁才能修改表补充源码细节实际实现中 Reserved 被细分为RESERVED_READ_ONLY与RESERVED_READ_WRITE两种lf.proc.LockType枚举见 lib/proc/lock_manager.js规范文档将它们统一概括为「Reserved」以简化对外描述。四类锁的组合实现了经典的两阶段锁定two-phase locking算法先请求第一把锁成功后再请求第二把升级具体见Runner#requestTwoPhaseLock_lib/proc/runner.js。4.1 哪些操作会创建哪种锁规范文档给出了完整的锁创建对照表引起锁创建的函数创建的锁exec()of aselect()querySharedexec()of aninsert()queryReservedexec()of aninsertOrReplace()queryReservedexec()of anupdate()queryReservedexec()of andelete()queryReservedbegin()orexec()of a transactionReserved从 Runner 的调度逻辑lib/proc/runner.js可以印证READ_ONLY任务先请求RESERVED_READ_ONLY再升级为SHAREDREAD_WRITE任务先请求RESERVED_READ_WRITE再升级为EXCLUSIVE。4.2 Reserved 的自动升级与锁的释放规则所有 Reserved 锁都会由查询 Runner 自动升级为 Exclusive 锁——写查询真正落盘前必须拿到排他权由exec()产生的锁升级为 Exclusive 后一旦 Lovefield 完成必要的数据写入即自动释放由事务begin()产生的锁既不会被升级也不会被释放直到该事务调用rollback()或commit()。正是第三条规则隐含了死锁的可能当多个闭包closure同时试图通过事务写数据库时例如两个事务分别以相反顺序begin([A, B])与begin([B, A])互相等待对方释放表锁就可能出现用户代码制造的死锁。Lovefield 的立场是检测与规避这类死锁是使用者自己的责任。实践中应让所有事务按一致的顺序声明作用域表并避免在持锁期间执行耗时过长的异步逻辑。4.3 Runner任务队列与锁的调度中枢lf.proc.Runnerlib/proc/runner.js是并发控制的调度核心任务队列按「优先级优先、同优先级按任务 id 升序」的规则二分插入TaskQueue_.insert见 lib/proc/runner.jsconsumePending_()反复扫描队列只要某任务的锁可被获取requestTwoPhaseLock_成功就从队列移除并执行任务成功onTaskSuccess_或失败onTaskError_后都会releaseLock并再次尝试消费队列中剩余任务高优先级任务优先级高于USER_QUERY_TASK或TRANSACTION_TASK投递时会先clearReservedLocks清空相关表的 Reserved 锁为其让路。4.4 锁兼容性矩阵源码级根据LockTableEntry_.prototype.canAcquireLocklib/proc/lock_manager.js可以归纳出各锁之间的兼容关系EXCLUSIVE仅当目标表无 Shared 锁、无 Reserved 锁、且当前任务已持有该表的RESERVED_READ_WRITE锁时才可获取即必须由本任务升级而来SHARED要求表上无 Exclusive、无RESERVED_READ_WRITE且当前任务已持有RESERVED_READ_ONLY锁RESERVED_READ_ONLY只要表上无RESERVED_READ_WRITE即可获取可多个读者同时持有RESERVED_READ_WRITE要求无其他RESERVED_READ_ONLY锁且表上无其他写者或同任务重复持有。这一矩阵保证了「多读单写」的并发模型读读并行、读写互斥、写写串行与规范文档中三型锁的描述完全一致。5. 测试印证事务语义的可验证性Lovefield 的端到端测试 tests/proc/end_to_end_transaction_test.js 直接验证了规范文档中的多数语义setUp中通过db.createTransaction().exec([insert...])批量灌入 50 个 Job、300 个 Employee、10 个 Department 等样本数据tests/proc/end_to_end_transaction_test.js印证exec([...])的批量原子写入用法测试中多次构造「事务已提交/已回滚后再调用rollback()」的场景并断言抛出lf.Exception(107)如 tests/proc/end_to_end_transaction_test.js印证「终止状态后所有成员函数抛错」的规范约定。若需在本地运行这些测试可按仓库 docs/running_tests.md 的指引搭建测试环境后执行tests/proc目录下的事务用例。6. 小结事务最佳实践清单单条查询直接用查询构建器exec()让隐式事务为你兜底多条查询需原子提交db.createTransaction()tx.exec([...])顺序执行、统一落盘、共享快照查询间有数据依赖tx.begin([tables])声明作用域 →tx.attach(q)取中间结果 → 最终tx.commit()需要放弃改动在提交前调用tx.rollback()牢记三条铁律exec()即BEGIN COMMIT事务结束即终止禁止复用begin()的排他锁要持续到commit()/rollback()死锁风险由使用者自行规避。更深入的设计背景可继续阅读仓库中的设计文档 docs/dd/05_transaction.md以及与事务相关的全部源码与测试接口定义lib/transaction.js事务实现与状态机lib/proc/transaction.js事务任务lib/proc/transaction_task.js锁管理器lib/proc/lock_manager.jsRunner 调度lib/proc/runner.js端到端测试tests/proc/end_to_end_transaction_test.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/lov/lovefield点击查看免费下载相关推荐MikroORM 5.9 事务与并发控制完全指南从隐式事务到乐观/悲观锁MikroORM 5.9 事务与并发控制完全指南从隐式事务到乐观/悲观锁 事务与并发控制是 MikroORM 这类基于 Unit of Work工作单元与后端TypeORM 迁移假执行Faking Migrations与事务模式控制全解TypeORM 迁移假执行Faking Migrations与事务模式控制全解 数据库表结构已被手工或外部工具改过、迁移文件里的 up 逻辑其实早已生效却后端数据库ORMMikroORM 事务与并发控制实战事务边界、传播行为与锁定机制MikroORM 事务与并发控制实战事务边界、传播行为与锁定机制 本文基于 MikroORM v6.6 官方文档《Transactions and Concu网络安全网络IDS上一篇Cycle.js状态管理面试指南响应式状态设计的常见问题与答案下一篇RootHide越狱终极指南如何在iOS 15上实现完全隐藏的越狱体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站