ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Lovefield 数据存储层设计全解:BackStore 插件架构与五大存储后端深度剖析

Lovefield 数据存储层设计全解:BackStore 插件架构与五大存储后端深度剖析 关系型数据库数据库前端【免费下载链接】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/dd/02_data_store.md 为骨架深入剖析这个浏览器端关系型数据库的存储层设计从lf.BackStore插件化接口的公共契约到 Memory、IndexedDB、Firebase、WebSQL、LocalStorage 五种存储后端的内部结构与取舍再到数据库升级与外部变更处理机制。读完本文你将理解 Lovefield 如何通过存储抽象实现查询引擎与存储技术解耦掌握 Bundled Mode 的加速原理与切换步骤并能在源码层面定位每个设计决策的具体实现。1. 为什么需要数据存储抽象持久化、易失与云同步在 Lovefield 设计之初见 docs/dd/00_intro.md团队就识别出两类存储需求持久化数据存储persistent data store服务于需要跨会话保存数据的应用易失数据存储volatile data store服务于其余对持久性无要求的应用以及测试场景。随着时间推移集成不同持久化存储技术的需求不断出现。最典型的例子是 Firebase 集成它让开发者鱼与熊掌兼得——浏览器端拥有关系型查询引擎同时所有客户端通过 Firebase 服务器获得完全同步的数据库。正是基于这样的演进Lovefield 采用了数据存储插件化架构plug-in architecture。所有数据存储都实现lf.BackStore接口从而让查询引擎与具体的存储技术彻底解耦。对于支持升级模式upgrade mode的数据库Lovefield 还提供了lf.raw.BackStore接口来专门处理这种特殊场景。这一抽象的价值在于每种存储技术都有不同的限制与边界条件IndexedDB 的自动提交、Firebase 的云端约束、WebSQL 的容量上限等Lovefield 的贡献者负责在实现中消化这些坑而使用者的查询代码完全感知不到底层差异。2. 公共契约所有数据存储共享的三个方法设计文档的第 2.1 节Common Nominator指出所有数据存储都拥有三个公共方法。这一契约在源码 lib/back_store.js 的lf.BackStore接口中有精确对应// 摘自 lib/back_store.js已简化接口签名 lf.BackStore.prototype.init; // 初始化数据存储 lf.BackStore.prototype.createTx; // 创建底层事务对象 lf.BackStore.prototype.close; // 关闭连接2.1init初始化与行号续接init表示数据存储的初始化通常意味着与持久化实例建立连接。数据库升级过程也在此时发起详见第 8 节。除此之外初始化还有一个容易被忽略的关键职责识别到目前为止的最大行 id并从此处继续生成行 id。这个设计源于 Lovefield 的行 id 模型参见 docs/dd/01_schema.md每一行在创建时由系统分配一个全库唯一的 row id取值范围为 0 ~ 2^53-1JavaScript number 的安全整数上限。唯一行 id 是查询引擎正确工作的前提。以 IndexedDB 实现为例lib/backstore/indexed_db.js 在onsuccess中调用scanRowId_()扫描全表最大行号随后通过lf.Row.setNextIdIfGreater(rowId 1)将全局行号生成器推进到该位置之后lib/backstore/web_sql.js 则对每个表执行SELECT MAX(id)汇总后同样调用setNextIdIfGreater。这保证了重启后新插入的行绝不会与历史行冲突。2.2createTx事务与 Journal 的关联createTx从数据存储返回一个事务对象同时暗示数据存储一次原子写操作即将发生。该事务会关联一个唯一的Journal日志由它保证所有变更一次性刷盘flush。Lovefield 只要求数据存储支持原子写入Lovefield 自行管理事务并将一个事务内的所有写入合并为单次 flush。这一机制在 lib/backstore/base_tx.js 中落地commitReadWrite_先执行journal_.checkDeferredConstraints()检查延迟约束然后mergeIntoBackstore_将 Journal 中的表变更mergeTableChanges_与索引变更mergeIndexChanges_全部合并写入最后journal_.commit()一次性提交。2.3close尽力而为的关闭close关闭与持久化实例的连接。由于技术限制这个调用是best-effort尽力而为的——例如 IndexedDB 和 WebSQL 都没有可靠的close语义IndexedDB 只要有连接存在就无法保证数据库完全关闭见第 4.1 节而 WebSQL 根本不支持关闭数据库连接lib/backstore/web_sql.js 中该方法为空实现。3. Memory Store最简单的易失存储设计文档第 2.2 节描述Memory store 内部是一个lf.structs.Map将表名映射到表而表本身也是一个lf.structs.Map将行 id 映射到该表的lf.Row对象。Memory store 的事务则是一个朴素的 Promise——commit 时 resolveabort 时 reject。源码验证了这一点。lib/backstore/memory.js 中lf.backstore.Memory function(schema) { this.schema_ schema; this.tables_ lf.structs.map.create(); // MaptableName, MemoryTable };initlib/backstore/memory.js遍历schema.tables()为每个表创建空的MemoryTable若表启用了persistentIndex还会额外创建索引表和 RowId 索引表createTx返回lf.backstore.MemoryTx。而 lib/backstore/memory_tx.js 中事务的 commit/abort 正是两个 Promise 调用lf.backstore.MemoryTx.prototype.abort function() { this.resolver.reject(undefined); }; lf.backstore.MemoryTx.prototype.commitInternal function() { this.resolver.resolve(); return this.resolver.promise; };由于 Memory store 不持久化任何数据close为空操作且不支持外部变更订阅subscribe/unsubscribe/notify均为空实现。它是最适合测试与原型验证的存储对应的测试见 tests/backstore/memory_back_store_test.js。4. IndexedDB Store持久化主力与五个设计约束IndexedDB 规范带来了一些有趣的坑caveats它们反过来深刻影响了 Lovefield 的整体规范设计。设计文档第 2.3 节列出了五个影响设计的关键特性。4.1 五个影响设计的关键特性自动提交Auto commit如果事务离开其消息循环message loopIndexedDB 会自动提交该事务。例如在 IndexedDB 事件回调中发起一个 XHR 请求实际上就会导致当前事务被提交。这要求 Lovefield 在事务生命周期内严格控制异步操作的范围。升级UpgradeIndexedDB 的 schema 只能在onupgradeneeded事件的处理器中修改而该事件只会在数据库被打开时触发。因此Lovefield 数据库的 schema 变更只能在初始化阶段完成。此外IndexedDB 不提供表重命名能力重命名表需要重建一张内容完全相同的表再删除旧表——这无法在升级事务内安全完成用户需要在onUpgrade函数之外手动操作。尽力关闭Best effort closing如果数据库仍存在连接IndexedDB 不保证数据库能完全关闭。因此 Lovefield 无法可靠地保证完全关闭一个数据库实例后再重新打开以修改 schema。先写者胜First writer winsIndexedDB 支持多连接意味着多个进程/标签页可以同时连接同一个数据库实例。IndexedDB 目前不提供任何跨进程锁因此最先到达数据库的写入者获胜其他存在冲突范围的写入者将被回滚。同时 IndexedDB 也不为其他会话/进程/标签页造成的变更提供 observer/事件通知。事件密集Event happyIndexedDB 提供了丰富的事件供开发者监听利用但这也意味着批量加载时浏览器会触发大量未使用的事件。如果单表数据超过 10K 行用户需要考虑使用实验性的Bundled Mode见 4.4 节来改善应用加载时间。4.2 包装器架构WrapperLovefield 用不同类包装 IndexedDB 对象设计文档第 2.3 节表格IndexedDB 对象Lovefield 包装器IDBDatabaself.backstore.IndexedDB和lf.backstore.IndexedDBRawBackstore提供onupgrade辅助IDBTransactionlf.backstore.IndexedDBTxIDBObjectStorelf.backstore.ObjectStore和lf.backstore.BundledObjectStore用于 Bundled Mode这些包装器的目标提供跨浏览器 shim最小化浏览器相关代码为用户处理事务自动提交问题采用最佳实践以获得最优性能以更优雅的方式处理升级流程。每个 IndexedDB 事务都关联一个Journal对象。Journal 承载一个逻辑lf.Transaction对象中的所有变更并在逻辑事务提交时通过物理 IndexedDB 事务对象一次性全部刷盘。因此在整个代码库中IndexedDB 事务对象被命名为Tx以区别于逻辑事务对象。这一区分在 lib/backstore/indexed_db_tx.js 中可见IndexedDBTx将tx_.oncomplete绑定到 resolver 的 resolve、tx_.onabort绑定到 reject。4.3 存储格式数据在 IndexedDB 中按以下层级存放设计文档第 2.3.1 节整个数据库以数据库名存入一个 IndexedDB 实例每个表对应一个 object store每行对应 object store 中的一个对象包含两个字段id全库唯一的行 idvalue该行的实际载荷payload其中的每个字段对应 schema 中的一个列。从 lib/backstore/indexed_db.js 的实现看createObjectStoresForTable_使用db.createObjectStore(tableSchema.getName(), {keyPath: id})创建以id为 keyPath 的 object store若表启用了persistentIndex还会为每个索引创建以规范化索引名命名的 object store 以及 RowId 索引表createIndexTable_。持久化索引表在升级时会先被清理removeIndexTables_通过判断名称中是否含.来识别 Lovefield 创建的索引表。4.4 Bundled Mode 实验以写换读的启动加速背景按照当时的 IndexedDB 规范从表中加载全部行的唯一方式是openCursor逐行游标遍历var req objectStore.openCursor(); req.onsuccess function() { if (cursor) { // 通过 cursor.value 获取一行 cursor.continue(); } else { // 遍历结束 } };这段代码涉及 N 次cursor.continue调用和 N 次onsuccess事件触发。当 N 很大时开销非常昂贵设计文档给出实测数据——WebKit 在 HP Z620 上触发一个事件需要 57us仅触发 N 个onsuccess事件加载 100K 行的墙钟时间就要5.7 秒还没算上回调处理时间。方案Bundled Mode 将多个逻辑行捆绑进 IndexedDB 中的一个物理行。设计文档说明上限为 512 个逻辑行源码 lib/backstore/page.js 中常量定义与此吻合lf.backstore.Page.BUNDLE_EXPONENT 9; // 2^9 512Page类即捆绑页——每个物理行是一个 PagetoPageId通过rowId 9计算行所属页lib/backstore/page.jsgetPageRange反推页的 id 区间serialize/deserialize负责页面与存储格式的转换lib/backstore/page.js。数据读写通过BundledObjectStorelib/backstore/bundled_object_store.js完成getAll_用openCursor逐页读取后解包所有逻辑行put则按pageId将行分组写入对应页面。由于该方法可能带来其他与性能相关的影响该特性被标记为实验性。启用 Bundled Mode 的用户需要牢记以下事实写入/更新更慢捆绑模式需要额外分组开销。基准测试显示更新 10K 行大约慢 300-500 ms作为交换批量加载快了好几秒。适用场景主要面向 10K 行以上的数据表。较小的数据库开启捆绑模式可能反而更慢用户应当自行基准测试判断是否可行。模式转换步骤非捆绑 ↔ 捆绑db.export()导出整个数据库的 JavaScript 表示用window.deleteDatabase()彻底删除原数据库重新用connect()创建数据库用db.import()导入先前导出的数据。调试困难捆绑数据库更难通过开发者工具检查。写入前页面会把载荷序列化为字符串这是为了在 Chrome测试于 v39.0.2171.36中获得大 JSON 对象上明显更好的性能。此外从 lib/backstore/indexed_db.js 可以看到在捆绑模式下扫描最大行 id 时需要先反序列化 Page、遍历其 payload 中的全部行号取最大值而非直接读游标 key。5. Firebase Store云端同步后端Lovefield 可以构建在 Firebase 之上把 Firebase 作为云后端存储。使用 Firebase 后端时必须遵守三条规则设计文档第 2.4 节所有访问该数据库的客户端必须使用 Lovefield只能有一个客户端负责创建数据库数据库升级需要以不同方式进行升级期间除升级方外的其他客户端必须停止使用该数据库这通常借助 Firebase 安全控制实现。5.1 存储格式出于性能考虑Lovefield 在 Firebase 中的数据结构与本地存储差异很大设计文档第 2.4.1 节schema_name: { rev: { R: N, }, db: { version: schema version }, table: { table_name: table_id }, row id 1: { R: N1, T: T1, P: object1 }, row id 2: { R: N2, T: T2, P: object2 }, ... }其中R 代表修订号revision、T 代表表 idtable id、P 代表载荷payload缩写是为了保证最优的线上传输效率。源码 lib/backstore/firebase.js 的createNewDb_完整复现了这一结构且初始rev/R为 1每个表按索引顺序分配table中的数字 id。源码注释中还给出了配套的 Firebase 服务器端规则建议对R、T建立索引.indexOn: [R, T]以提升查询性能。5.2 初始化与增量同步Firebase 后端的初始化流程lib/backstore/firebase.js很有特色读取db/version若为 null 则是全新数据库先创建初始结构并回调onUpgrade若版本一致则读取rev/R与table映射逐表reloadTable拉取数据若版本不一致则走升级分支后重试init()。initRowId_汇总所有内存表的getMaxRowId()后推进全局行号。外部变更监听lib/backstore/firebase.js通过db_.on(child_removed, ...)记录被删除的行并利用orderByChild(R).startAt(this.revision_ 1)只监听修订号大于当前值的增量变更onChange_lib/backstore/firebase.js计算表差异generateDiff_、同步到内存表并notify给订阅者然后重新建立监听以续接修订号。这也解释了为何数据库升级在 Firebase 场景下含义不同版本不匹配通常意味着用户浏览器上运行的是缓存的旧版 JS真正要做的是让用户刷新会话、加载更新后的二进制。6. WebSQL Store已弃用的过渡补丁WebSQL 存储已被标记为DEPRECATED弃用设计文档建议用户升级到 Safari 10 并停止使用该存储。源码注释lib/backstore/web_sql.js将其定位为填补空缺的过渡补丁一旦苹果修复 Safari 的 IndexedDB 问题就会被移除。6.1 存储格式WebSQL 的数据结构与 IndexedDB 类似设计文档第 2.5.1 节整个数据库以数据库名存入一个 WebSQL 实例一张名为__lf_ver的特殊表存放整个数据库的元数据每个表对应一张 WebSQL 表每行对应 WebSQL 表中的一行包含两个字段id全库唯一的行 idvalue行载荷的序列化 JSON 字符串。__lf_ver表的存在有一个现实原因Chrome 的changeVersion功能不可用因此 Lovefield 用这张表手动保存数据库版本lib/backstore/web_sql.js。checkVersion_创建__lf_ver(id INTEGER PRIMARY KEY, v INTEGER)并读取版本低于当前 schema 版本则触发升级onUpgrade_高于当前版本则抛出异常 108试图用旧代码打开新数据库相等则直接通过。升级时preUpgrade_lib/backstore/web_sql.js会删除所有持久化索引表名称含索引标记INDEX_MARK的表并为每个表执行CREATE TABLE ... (id INTEGER PRIMARY KEY, value TEXT)。WebSQL 后端也不支持变更通知subscribe/unsubscribe/notify抛异常 355。7. LocalStorage Store实验性的外部变更研究载体设计文档 docs/dd/00_intro.md 将 LocalStorage 列为实验性数据存储其价值在于作为处理外部变更的 proof of concept——这正是第 9 节外部变更机制中提到的两条支持通知的存储之一。源码 lib/backstore/local_storage.js 展示了其实现特点存储格式键为namespace.tableName版本以namespace.version#特殊键保存数据以序列化对象形式存于每个键下源码注释及loadTables_lib/backstore/local_storage.js。同步初始化initSync校验版本不一致时抛异常 360升级逻辑当时尚未实现。容量限制受浏览器限制最多可保存约 10MB 数据。外部变更subscribe注册window上的storage事件监听器lib/backstore/local_storage.jsonStorageEvent_过滤本库前缀的 key 变更对受影响表计算差异table.diff(newData)并回调变更处理器lib/backstore/local_storage.js。8. 数据库升级流程升级流程由提升 schema 版本号触发设计文档第 2.6 节。Lovefield 用更新后的版本号打开数据库IndexedDB 随即触发onupgradeneeded事件。在该事件中Lovefield 依次执行创建 schema 中存在但 IndexedDB 中不存在的表object store将数据库连接包装进lf.backstore.IndexedDBRawBackstore以包装后的实例调用用户提供的onUpgrade函数。源码 lib/backstore/indexed_db.js 的onUpgradeNeeded_正是这条流程先removeIndexTables_清理持久化索引表再createTables_创建缺失的 object store最后onUpgrade(rawDb)交给用户逻辑。IndexedDBRawBackstorelib/backstore/indexed_db_raw_back_store.js作为lf.raw.BackStore的实现为用户提供onupgrade场景下的辅助 API如创建/删除表。设计文档也坦承该设计仍让用户暴露在意外自动提交的风险之下例如在onUpgrade中发起异步调用可能导致事务提前提交但当时不存在已知的、可在跨浏览器场景下工作的更好替代方案。9. 外部变更处理数据存储中的内容可能被其他会话甚至其他客户端修改设计文档第 2.7 节。大多数数据存储缺乏外部变更通知能力。因此对于跨标签页实现需要借助 Web Worker 或 Service Worker 来托管数据库以隔离多个连接。Lovefield 团队当时已知晓该问题并与 IndexedDB 团队密切合作推动 IndexedDB 的变更通知标准。在已有实现中LocalStorage 和 Firebase提供变更通知来告知 Lovefield 数据已被外部来源修改。默认情况下 Lovefield 会监听这些变更并更新三部分内容数据库缓存Database cache索引Indices被观察的查询Observed queries这套机制的协调者是 lib/backstore/external_change_observer.js 中的ExternalChangeObserverstartObserving向当前 backstore 注册回调lib/backstore/external_change_observer.js当 backstore 报告变更时onChange_构造一个lf.proc.ExternalChangeTask并交给 Runner 调度lib/backstore/external_change_observer.js任务实现在 lib/proc/external_change_task.js。该任务负责按表差异更新内存中的缓存与索引层从而使观察中的查询得到最新结果。对应的测试见 tests/backstore/external_change_observer_test.js。注意外部变更处理假定单写者、多读者模型——当前逻辑不检查外部变更与进行中的读写查询之间的冲突。10. 与其他设计文档的衔接数据存储层并非孤立存在它与 Lovefield 的其余设计章节环环相扣docs/dd/01_schema.md行 id 的分配规则、动态/静态 schema 定义是存储层数据格式的基础docs/dd/03_life_of_db.md描述connect()流程——创建并注册行缓存与数据存储对象、初始化数据存储必要时执行升级、服务初始化、预取数据prefetch。数据存储初始化时扫描最大行 id 的行为在本文第 2.1 节已详述docs/dd/04_query_engine.md 与 docs/dd/05_transaction.md查询执行运行在事务上下文中而事务正是经由createTx从数据存储获得的docs/spec/02_data_store.md对应的规范文档Specification定义各存储后端的对外行为契约各存储的测试用例可作为深入验证的入口tests/backstore/indexeddb_test.js、tests/backstore/memory_back_store_test.js、tests/backstore/firebase_test.js、tests/backstore/web_sql_test.js、tests/backstore/local_storage_test.js。结语Lovefield 的数据存储层通过lf.BackStore插件接口成功地将关系型查询引擎与底层存储技术解耦Memory 提供零持久化开销的测试底座IndexedDB 承担持久化主力职责并在 Bundled Mode 下以写性能换读取启动速度Firebase 带来多客户端云同步WebSQL 作为历史过渡方案已弃用LocalStorage 则充当外部变更通知的实验载体。理解这五个后端的内部结构与取舍无论是为 Lovefield 编写新存储适配器还是调试性能问题都能做到有的放矢。赞分享关系型数据库数据库前端【免费下载链接】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点击查看免费下载相关推荐Node.js v12.22.5LTS安全维护版本深度解析DNS 域名校验、HTTP/2 释放后使用与 TLS 参数校验修复Node.js v12.22.5LTS安全维护版本深度解析DNS 域名校验、HTTP/2 释放后使用与 TLS 参数校验修复 本篇技术指南以 nodejs关系型数据库数据库前端为什么选择Shirdel-Coder-9B-Claude-Fable-53大优势对比其他代码生成模型为什么选择Shirdel Coder 9B Claude Fable 53大优势对比其他代码生成模型 Shirdel Coder 9B Claude Fabl高性能内存数据库 Rudis网络架构与数据存储设计深度剖析高性能内存数据库 Rudis网络架构与数据存储设计深度剖析 引言内存数据库的性能挑战与 Rudis 的解决方案 在当今高并发、低延迟的应用场景中内存数据库数据库KV存储上一篇为什么选择nli-mpnet-base-v2句子相似性任务的性能对比与优势下一篇BullMQ 任务查询指南用 Getters 掌握任务生命周期、状态计数与分页检索创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表