先记住这个答案
IndexedDB 的 readwrite 或 readonly 事务由 IDBTransaction 管理。当某个请求发出错误且未阻止其默认行为(即未调用 event.preventDefault())时,会触发事务中断(abort),浏览器自动回滚该事务内所有已执行的写入。实现方式是每个事务有活跃期,若请求失败并冒泡到事务层,则中止并撤销修改。为确保原子性,开发者不应在 onerror 中误调 preventDefault() 阻止默认中止行为,除非有意识地部分回滚。
- 事务内操作失败默认终止并回滚
- 未阻止错误默认行为才会触发自动回滚
- 回滚不包含事务提交后的崩溃恢复
事务原子性的内部保障机制
当调用 db.transaction(storeNames, 'readwrite') 创建事务时,浏览器为其分配一个作用域。事务进入活跃状态,期间发出的 add、put、delete 等请求按顺序排队。只要事务仍活跃,请求结果会不断触发相应事件。若某个请求抛出错误(例如主键冲突或约束失败),该错误会作为 IDBRequest 的 error 事件派发。如果该请求的 onerror 处理函数没有调用 event.preventDefault(),错误会冒泡到 IDBTransaction 对象上,导致事务被标记为失败。
事务被标记失败后,浏览器立即中止事务并触发 abort 事件。此时所有已经写入对象存储的数据都会被回滚,仿佛这个事务从未执行过。这也意味着事务内后续尚未发出的请求不会执行,队列被清空。整个机制确保了资源不会处于中间状态。需要明确的是,这里回滚的对象是“数据变更”,包括新增、修改、删除的内容;而对象存储的结构变更(如建表)不在普通事务范围内,只有 versionchange 事务才负责 schema 调整。
批量导入时部分失败的整体回滚
假设一个管理系统需要导入用户列表,业务要求要么全部成功,要么全部不导入。代码在一个 readwrite 事务中循环删除旧用户名索引,再插入新记录。例如先 store.clear(),然后逐个 store.put。若第 10 条数据违反了唯一索引约束,该 put 请求的错误被默认处理,整个事务立刻中止,之前的 clear 和第 1–9 条 put 全部撤销。最终对象存储仍然保留原来的旧数据,没有出现“清空一半但新数据没有写入”的脏状态。开发者只需监听 transaction.onabort,在 UI 中提示“导入失败,数据已回滚”。
这种原子性特别适合需要一致性的批处理。如果不希望全部回滚,则应在每个请求的 onerror 中调用 event.preventDefault() 并自己记录错误,错误冒泡被阻止后事务不会中止。但那样就需要自己维护部分成功的数据,原本的原子性就被放弃了。在实际工程中,优先依赖默认回滚,可编写一个简单的事务包装器,把多次请求封装在 Promise 里,任何失败都 reject 并让事务自动中止,节省大量手动补偿代码。
原子性的失效边界与应对
原子性不是持久性。虽然事务提交时数据已写入,但如果操作系统崩溃或浏览器在事务提交后、数据落盘前崩溃,仍可能丢失整个事务。Firefox 从 40 起放宽了持久性保证以提升性能,complete 事件在数据尚未真正刷盘时也会触发。关键在于“提交成功”不等同于“永久保存”,需要强持久性时应考虑 readwriteflush 非标准模式或在事务外额外验证。这不是原子性失败,而是硬件故障导致的丢数据。
另外,错误处理是边界中的关键。请求的 error 事件如果被 preventDefault() 阻止,事务不会被中止,失败的操作只是该请求作废。例如一个 get 找不到记录并不会触发错误,但某些约束冲突可能因业务需求被吞掉。吞掉后事务继续,后续写入可能依赖失败前的状态,造成逻辑不一致。开发者需明确:事务原子性默认开启,但可通过 preventDefault() 自愿关闭某次失败的中止效果。在敏感场景,最好始终让意外错误冒泡,只对预期错误做针对性处理。
容易答错的地方
- 把请求错误都视为事务失败
- 一个请求出错并不必然导致事务回滚。只有错误冒泡到事务且未被
preventDefault()拦截时才会中止。若在onerror里调用event.preventDefault(),事务继续运行,该请求失败不会触发回滚。 - 认为‘任一操作失败’包括读取失败
- 只读事务中的读取操作(如
get)找不到目标记录会正常完成,不视为失败。只有真正发生的错误(如约束冲突、数据损坏)才可能触发中止。
面试官还会怎么问?
如何区分单个请求失败和事务全部回滚?
监听事务的 onabort,若触发则说明整个事务回滚。单个请求的 onerror 是其自身错误,事务可能因该请求失败而中止,也可能在 preventDefault 后继续。
`versionchange` 事务有原子性吗?
有,versionchange 事务同样具备原子性,且其作用域覆盖所有对象存储。但它与普通读写事务触发时机不同,只能在数据库升级时创建。如果升级过程中出错,数据库结构和数据均保持一致。
如果事务中调用 `abort()` 会发生什么?
调用 IDBTransaction.abort() 会主动中止事务并回滚所有修改,触发 onabort 事件。这可以用于显式撤销基于业务判断的不当操作。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。