第4章:IndexedDB 事务模型 — 对象存储、索引、游标、异步API与版本管理
4.1 边界与核心概念
IndexedDB 是浏览器内置的 NoSQL 事务型数据库,其事务模型决定了数据读写的一致性、并发控制与版本演化能力。在本课程中,IndexedDB 事务模型的边界被界定为“单域、异步、结构化对象持久化层”,与 localStorage(同步键值对)、Cache API(请求-响应存储)、Web Storage(同步)形成明确职责划分。
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 属性(数组字段展开索引)。索引不可单独修改字段值,只能通过主数据更新同步。
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 数据流与控制流
一次典型读事务的数据流如下:
- 通过
indexedDB.open()获取数据库连接,若版本变化则触发onupgradeneeded。 - 调用
db.transaction(['storeName'], 'readonly')创建只读事务。 - 从事务中获取
objectStore('storeName'),再通过主键get()或索引openCursor()发起请求。 - 请求成功回调中获得结果(或游标对象)。
- 事务结束时(所有请求完成 + 自动提交),数据写入磁盘、索引同步更新。
控制流依赖于 事件循环:每个请求完成时递交微任务,事务在最后一个请求完成后自动提交。若在事务中抛出未处理异常,整个事务回滚。
4.5 选型权衡
| 对比项 | IndexedDB 事务模型 | localStorage | Web SQL (已废弃) |
|---|---|---|---|
| 事务支持 | 完整 ACID,读写/只读事务,自动回滚 | 无事务,同步阻塞 | 支持事务但规范已弃用 |
| 数据类型 | 结构化对象、Blob、ArrayBuffer | 仅字符串 | 仅关系型行 |
| 查询能力 | 索引、游标、范围、复合查询 | 无 | SQL 查询 |
| 异步 | 全异步,不阻塞主线程 | 同步 | 异步 |
| 版本演化 | 显式版本号 + onupgradeneeded | 无 | 不支持 |
| 存储上限 | 通常无限制,但浏览器可能限制每个源 10~50MB | 通常 5MB | 50MB |
4.6 风险点与应对策略
- 事务并发死锁:同一数据库连接中不建议同时打开多个读写事务。不同标签页共享一个数据库时,版本升级事务会阻塞所有其他连接。建议使用短期事务(毫秒级)并避免在事务内做异步等待。
- 版本冲突导致数据丢失:升级脚本中如果删除对象存储,数据不可恢复。生产环境应采用“创建新存储 + 数据迁移”策略,而非删除旧存储。
- 异步错误传递路径长:未绑定的 onerror 处理会导致事务静默回滚。建议在全局注册 transaction.onerror 并打印详细日志。
- 游标性能陷阱:在大型数据集上使用游标循环逐条更新会使写放大严重,应考虑批量 put 或使用索引条件过滤。
4.7 落地步骤
- 定义数据模型:确定对象存储名称、主键字段、需要索引的字段。
- 版本规划:初始版本从 1 开始,每次变更结构加 1。
- 编写升级逻辑:在 onupgradeneeded 中执行
db.createObjectStore()和objectStore.createIndex()。 - 封装事务处理层:提供
get、set、delete、query函数,内部使用 Promise 包装 IDBRequest。 - 实现事务重试:对因版本冲突或数据库关闭导致的 TransactionInactiveError 进行指数退避重试。
- 增加监控:记录事务开始、提交、回滚事件,用于调试性能瓶颈。
4.8 验收标准
| 验收项 | 预期结果 | 检查方法 |
|---|---|---|
| 基本 CRUD | 能正常增删改查对象 | 写入后重新打开数据库读取确认 |
| 事务回滚 | 写入事务中抛出异常后,已写入数据不可见 | 在 put 后制造错误,检查数据库状态 |
| 索引查询 | 通过索引字段范围查询返回正确子集 | 创建 100 条记录,查询索引范围,点数 |
| 游标分页 | 游标按 direction 顺序遍历,支持 continue | 用游标遍历 50 条记录,与 get 结果一致 |
| 版本升级 | 打开高版本号触发 onupgradeneeded,旧数据保留 | 先写入数据,再升级版本增加索引,确认数据存在 |
| 并发安全 | 两个标签页同时写入同一对象存储不丢失数据 | 用 Web Worker 模拟两个连接写入不同键,检查最终结果 |