第4章:IndexedDB 事务模型 — 对象存储、索引、游标、异步API与版本管理

4.1 边界与核心概念

IndexedDB 是浏览器内置的 NoSQL 事务型数据库,其事务模型决定了数据读写的一致性、并发控制与版本演化能力。在本课程中,IndexedDB 事务模型的边界被界定为“单域、异步、结构化对象持久化层”,与 localStorage(同步键值对)、Cache API(请求-响应存储)、Web Storage(同步)形成明确职责划分。

边界提示:IndexedDB 不适用于极小数据(< 100KB)或极简单键值场景,这些场景首选 localStorage 或 SessionStorage;但需要复杂查询、索引、事务回滚时,IndexedDB 是唯一内置方案。

4.2 分层架构

事务模型内部划分为四个逻辑层:

  • 连接管理层 — 负责打开/关闭数据库、监听版本变化、管理连接池。
  • 事务执行层 — 统一创建读写事务、控制提交/回滚、维护事务状态(active/inactive/finished)。
  • 对象存储与索引层 — 管理 objectStore 定义、索引结构、键路径与唯一性约束。
  • 游标与结果集层 — 提供游标遍历、范围查询、排序与迭代器接口。
层名称核心 API职责
连接管理层indexedDB.open(), IDBOpenDBRequest版本协商、onupgradeneeded 触发、定义存储结构
事务执行层IDBTransaction, transaction()创建读写或只读事务、控制自动提交、监听 error/abort/complete
对象存储层IDBObjectStore, add/put/delete/clear/get管理存储记录、自动生成键、支持批量操作
索引层IDBIndex, openCursor / getAll / count建立非主键索引、支持复合索引、快速范围查询
游标层IDBCursor, IDBCursorWithValue逐一遍历结果、更新或删除当前记录

4.3 组件职责详解

4.3.1 对象存储(Object Store)

一个对象存储对应一张表,每个存储可定义 keyPath(主键字段)或 autoIncrement(自增键)。支持任意 JavaScript 对象(含 Date、Array、Blob),但不可存储 Function 或 Symbol。

  • 写操作:add(自动生成键,禁止重复)、put(更新或插入)、delete、clear。
  • 读操作:get(通过主键)、getAll、count。
  • 约束:所有操作必须在一个活动事务内执行;事务结束后对象存储自动关闭。

4.3.2 索引(Index)

索引是对对象存储中非主键字段的查找优化。每个索引有 unique 属性和 multiEntry 属性(数组字段展开索引)。索引不可单独修改字段值,只能通过主数据更新同步。

性能要点:索引本质是一个 B-tree 结构,每次更新主记录时同步更新索引,写放大指数约为索引数量。建议对高频查询字段建立索引,避免在单次事务中操作过多索引。

4.3.3 游标(Cursor)

游标用于在结果集上逐行迭代,避免一次性加载大量数据到内存。游标的 direction 属性可设为 "next"、"prev"、"nextunique"、"prevunique"。游标叠加索引时可实现分组聚合或按索引排序遍历。

// 游标典型使用模式(伪逻辑,非完整代码)
const range = IDBKeyRange.bound(100, 200);
const cursorRequest = objectStore.openCursor(range, 'prev');
cursorRequest.onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // 处理 cursor.value
    cursor.continue();  // 移动到下一条
  }
};

4.3.4 异步 API

IndexedDB 所有操作均返回 IDBRequest 对象,通过 onsuccess 回调或现代 Promise 封装(如 idb 库)获取结果。异步模型避免阻塞主线程,但要求开发者管理请求生命周期。

  • 每个请求对应一个异步结果,事务上下文在请求创建时绑定。
  • 错误处理:通过 request.onerror 或 transaction.onerror 捕获;未捕获的错误会导致事务自动回滚。

4.3.5 版本管理

数据库版本号是一个 无符号 64 位整数,每次升级结构(增加/删除对象存储或索引、修改键路径)必须提高版本号并在 onupgradeneeded 事件中执行变更。版本管理是 IndexedDB 唯一支持模式演化的入口。

风险点:版本号只能递增,无法降级。若多个标签页同时打开,浏览器会阻塞后续连接请求,直到当前升级事务完成或失败。不当的版本升级可能导致全量数据丢失。

4.4 数据流与控制流

一次典型读事务的数据流如下:

  1. 通过 indexedDB.open() 获取数据库连接,若版本变化则触发 onupgradeneeded。
  2. 调用 db.transaction(['storeName'], 'readonly') 创建只读事务。
  3. 从事务中获取 objectStore('storeName'),再通过主键 get() 或索引 openCursor() 发起请求。
  4. 请求成功回调中获得结果(或游标对象)。
  5. 事务结束时(所有请求完成 + 自动提交),数据写入磁盘、索引同步更新。

控制流依赖于 事件循环:每个请求完成时递交微任务,事务在最后一个请求完成后自动提交。若在事务中抛出未处理异常,整个事务回滚。

信任链路:IndexedDB 存储于用户本地磁盘的私有目录,受同源策略控制。即使页面通过 HTTPS 加载,存储内容也对其他来源隔离。传输层安全(TLS)不直接影响存储,但防止中间人篡改页面代码读取本地数据。

4.5 选型权衡

对比项IndexedDB 事务模型localStorageWeb SQL (已废弃)
事务支持完整 ACID,读写/只读事务,自动回滚无事务,同步阻塞支持事务但规范已弃用
数据类型结构化对象、Blob、ArrayBuffer仅字符串仅关系型行
查询能力索引、游标、范围、复合查询无SQL 查询
异步全异步,不阻塞主线程同步异步
版本演化显式版本号 + onupgradeneeded无不支持
存储上限通常无限制,但浏览器可能限制每个源 10~50MB通常 5MB50MB

4.6 风险点与应对策略

  • 事务并发死锁:同一数据库连接中不建议同时打开多个读写事务。不同标签页共享一个数据库时,版本升级事务会阻塞所有其他连接。建议使用短期事务(毫秒级)并避免在事务内做异步等待。
  • 版本冲突导致数据丢失:升级脚本中如果删除对象存储,数据不可恢复。生产环境应采用“创建新存储 + 数据迁移”策略,而非删除旧存储。
  • 异步错误传递路径长:未绑定的 onerror 处理会导致事务静默回滚。建议在全局注册 transaction.onerror 并打印详细日志。
  • 游标性能陷阱:在大型数据集上使用游标循环逐条更新会使写放大严重,应考虑批量 put 或使用索引条件过滤。

4.7 落地步骤

  1. 定义数据模型:确定对象存储名称、主键字段、需要索引的字段。
  2. 版本规划:初始版本从 1 开始,每次变更结构加 1。
  3. 编写升级逻辑:在 onupgradeneeded 中执行 db.createObjectStore() 和 objectStore.createIndex()。
  4. 封装事务处理层:提供 get、set、delete、query 函数,内部使用 Promise 包装 IDBRequest。
  5. 实现事务重试:对因版本冲突或数据库关闭导致的 TransactionInactiveError 进行指数退避重试。
  6. 增加监控:记录事务开始、提交、回滚事件,用于调试性能瓶颈。

4.8 验收标准

验收项预期结果检查方法
基本 CRUD能正常增删改查对象写入后重新打开数据库读取确认
事务回滚写入事务中抛出异常后,已写入数据不可见在 put 后制造错误,检查数据库状态
索引查询通过索引字段范围查询返回正确子集创建 100 条记录,查询索引范围,点数
游标分页游标按 direction 顺序遍历,支持 continue用游标遍历 50 条记录,与 get 结果一致
版本升级打开高版本号触发 onupgradeneeded,旧数据保留先写入数据,再升级版本增加索引,确认数据存在
并发安全两个标签页同时写入同一对象存储不丢失数据用 Web Worker 模拟两个连接写入不同键,检查最终结果

4.9 事务模型架构总览

下图展示了 IndexedDB 事务模型中的核心组件与数据流路径。深色节点为浏览器内置实现,浅色框为开发者交互层。
IndexedDB 事务模型架构 用户代码 (App) 连接管理层 版本管理 事务执行层 创建/提交/回滚 对象存储 add / put / delete 索引 openCursor / getAll 游标 迭代 / continue 异步 API 模型 每个操作返回 IDBRequest → onsuccess/onerror 回调 → 事务自动提交
架构要点:对象存储和索引层是数据持久化的物理载体;游标层提供按需迭代能力;事务执行层保证原子性;版本管理是唯一的结构变更入口。所有组件均通过异步 API 与用户代码交互,形成完整的请求-响应闭环。