Skip to content

IndexedDB - 事务

事务是 IndexedDB 的核心。它们是读取或写入数据的唯一方式,并提供关键的安全性和一致性保证。可以将事务想象成数据库操作的保护气泡。气泡内的一切要么一起成功,要么如果其中一项失败,整个气泡就会破裂,数据库会恢复到什么都没发生过的状态。这个特性称为原子性。

  • 范围(Scope): 事务可以访问的对象存储区集合。你在创建事务时定义此项(例如,db.transaction(['store1', 'store2']))。只请求你需要的存储区是性能最佳实践。
  • 模式(Mode): 决定事务允许执行的操作。
  • readonly:(默认)只能读取数据。这是最快的模式,因为多个 readonly 事务可以并发运行而不会相互阻塞。
  • readwrite:可以读取、创建、更新和删除数据。这些事务是独占的;为了防止冲突,一个对象存储区一次只能有一个 readwrite 事务处于活动状态。
  • versionchange:一种特殊的独占模式,仅在 onupgradeneeded 事件中运行。它是唯一可以修改数据库结构(其架构)的地方,例如创建或删除对象存储区和索引。
  • 生命周期(自动提交): 只要你对事务发出新请求,事务就处于活动状态。一旦你的代码块完成且没有更多待处理的请求,事务将自动尝试提交其更改。你无法对已提交或已中止的事务发出新请求。

想象一个场景,你需要将一个机器人从一个项目转移到另一个项目。这需要两次写入操作:更新旧的项目记录和新的项目记录。事务确保这些操作原子性地发生。

首先,一个成功的事务:

async function transferBot(db, botId, fromProjectId, toProjectId) {
// 在 'projects' 存储区上启动 'readwrite' 事务
const tx = db.transaction('projects', 'readwrite');
try {
console.log(`正在将机器人 ${botId} 从项目 ${fromProjectId} 转移到 ${toProjectId}...`);
// 获取两个项目记录
const [fromProject, toProject] = await Promise.all([
tx.store.get(fromProjectId),
tx.store.get(toProjectId)
]);
// 执行逻辑
fromProject.bots = fromProject.bots.filter(id => id !== botId);
toProject.bots.push(botId);
// 将两项更改写回数据库
await Promise.all([
tx.store.put(fromProject),
tx.store.put(toProject)
]);
await tx.done; // 显式等待事务完成
console.log('转移成功!');
} catch (error) {
console.error('转移失败!正在回滚更改。', error);
// 事务在出错时会自动中止,无需 tx.abort()
}
}

现在,让我们看看如果事务的一部分失败会发生什么。我们将通过尝试添加一个已存在键的记录来模拟错误。这将导致整个事务中止,并且第一次成功的 put 操作将被回滚。

async function failedTransactionDemo(db) {
const tx = db.transaction('employees', 'readwrite');
console.log('\n--- 正在演示一个失败的事务 ---');
try {
// 第一次操作将暂时成功
await tx.store.put({ id: 5, name: 'Frank', department: 'Security' });
console.log('已临时添加 Frank。');
// 第二次操作将失败,因为 ID 为 1 的员工已存在
await tx.store.add({ id: 1, name: 'Duplicate Alice', department: 'Error' });
await tx.done;
} catch (error) {
console.error(`事务按预期中止: ${error.name}`);
}
// 让我们检查数据库。Frank 不应该在那里。
const frank = await db.get('employees', 5);
console.log('中止事务后 Frank 是否在数据库中?', frank); // 应该为 undefined
}
  • 快速操作: 不要长时间保持事务开放。如果事务保持空闲状态,浏览器可能会使其超时并中止。
  • 不要 await 非 IDB 操作: 永远不要在活动事务内部 await 网络请求(如 fetch)或长时间的 setTimeout。事务很可能在你的其他操作完成之前关闭,导致后续的数据库请求失败。
  • 具体明确: 除非你绝对需要写入,否则始终请求 readonly 模式。它的性能更高。
  • 处理错误: 始终将你的事务逻辑包装在 try...catch 块中,以优雅地处理潜在的错误,例如 ConstraintError 或写入失败。