IndexedDB事务需手动控制生命周期,无自动提交,必须显式处理请求回调、声明所有访问表名、监听onabort以应对并发冲突,且事务abort不回滚已成功写入的数据。

事务生命周期必须手动控制,不能依赖自动提交
IndexedDB 没有自动提交机制,transaction 一旦创建就处于活跃状态,直到所有请求完成且没有新请求加入,或显式调用 abort() / commit()(后者不存在)——它只靠“自然结束”或手动中止。这意味着:如果在事务中发起一个 put(),但后续忘了监听它的 onsuccess 或没处理 onerror,事务可能卡住,阻塞其他写操作。
- 务必为每个请求绑定
onsuccess和onerror,哪怕只是空函数,否则未捕获的失败会让事务静默失败 - 不要在事务回调里异步发起新请求(比如
setTimeout或 Promise.then),这会导致请求脱离事务上下文,被拒绝并抛出InvalidStateError: Transaction is inactive - 多个表操作必须在同一个
transaction实例上调用objectStore(),不能分别开两个事务再“拼起来”
多表写入必须声明全部 storeName,否则写失败不报错
创建事务时传入的 store 名称数组,是该事务**唯一允许访问**的 object store 列表。漏写某个要写的表名,调用 transaction.objectStore('missing-store') 会直接抛出 NotFoundError;更隐蔽的是:如果只写了读取用的表,却在事务里尝试写入未声明的表,同样失败,但错误可能被吞掉——尤其在没监听 transaction.onabort 的情况下。
- 写操作涉及哪几个表,就老老实实把它们全列进
db.transaction(['storeA', 'storeB', 'storeC'], 'readwrite') - 避免用
transaction.objectStoreNames动态推导,它返回的是当前事务实际打开的 store 名,不是你“以为”能用的 - 如果某次操作只需读 A、写 B,但代码里先读了 A 再写 B,而事务只声明了
['B'],那读 A 这一步就会失败
并发写冲突靠 abort() + 重试,不是锁机制
IndexedDB 不提供行级锁或乐观锁原语。当两个事务同时写同一 key 时,后启动的事务不会等待,而是继续执行——但若最终 commit 阶段发现底层数据已被修改(如版本变化),浏览器会静默 abort 该事务,并触发 transaction.onabort。这和传统数据库的锁等待完全不同,容易误判为“没生效”。
- 必须监听
transaction.onabort,并在其中判断是否因并发冲突导致(可通过检查event.target.error?.name === 'AbortError'粗略识别) - 简单重试逻辑可封装成带次数限制的递归调用,但注意避免无限循环——建议加
setTimeout延迟后再试,给前一个事务留出完成时间 - 不要依赖
transaction.db.version做手动版本比对,IndexedDB 的版本升级是全局独占操作,与事务内数据一致性无关
事务失败后,已执行的写操作不会回滚一半
这是最容易误解的一点:IndexedDB 事务是原子性的,但“原子性”仅指“全部成功或全部不生效”,而它的“全部不生效”是通过中止整个事务实现的——也就是说,只要事务没 abort,前面的 put() 就已经落盘;一旦 abort,所有未完成的写操作都会被丢弃,但**已成功完成的写操作不会被撤销**。换句话说,它不保证 ACID 中的 A(Atomicity)在 abort 后回退已提交变更。
- 因此,不要在事务中混用“关键写”和“非关键写”,比如先更新用户余额,再记录日志。如果日志写入失败导致事务 abort,余额变更仍会保留
- 真正需要强一致的多步操作(如转账),应把所有相关字段放在同一个 object store 的同一条 record 里,用单 key 更新替代跨表写入
- 如果必须分表,考虑在应用层做补偿逻辑:事务 abort 后,主动查最新状态,再决定是否反向修正已写入的数据

















