Tips:不熟悉宏任务、微任务、事件循环(Event-Loop)等概念的童鞋,可以先看下这篇文章:https://segmentfault.com/a/1190000014940904
测试目标
- transaction 的执行方式
- transaction 的自动 commit 机制
- 手动 commit 和 abort 的行为
- 事务隔离情况
- 高频率开启 transaction 的性能情况
前置代码
1 | // 开启数据库连接 |
1. transaction 的执行方式
1 | const objectStore = db.transaction('test', 'readwrite').objectStore('test') |
输出:
1 | direct 1 |
结论
transaction 操作无论读还是写,都是异步的,且是 宏任务 级别。
2. transaction 的自动 commit 机制
1 | const transaction = db.transaction('test', 'readwrite') |
输出:
1 | 1-1 直接执行 start |
现象描述
- 建立 transaction 后,直接使用或放到一个微任务中使用它都没问题,但放到宏任务里使用则会抛出
transaction is not active
的异常。 - transaction 的一次操作完成后,再次直接执行或在微任务里执行下一次操作也没问题。
- 尝试在“宏任务”里使用 transaction 时,就算 transaction 仍有操作没有执行完成,也会抛出异常。
(所以能不能正常使用 transaction 并不完全是根据 transaction 是否“已结束”来判断的,而是根据“transaction 在当前 event-loop 中是否有效 ”来判断)
结论
浏览器对 transaction 的检查及自动提交的机制如下:
- 建立 transaction 的那个 event-loop,以及每一个操作完成触发回调的 event-loop,都会被标记为“transaction 可用”
- 在拥有标记的 event-loop 里,可以调用 transaction 执行操作,反之则不允许。(会抛出异常,而不是触发
request.onerror
回调) - 当所有有标记的 event-loop 都运行完成,且每一个 event-loop 里都没有再触发新操作,浏览器就认为事务已完成,触发 commit。
(注意!Safari 中略有不同,见后面小节)
用伪代码来表现这个逻辑:
1 | const db = { |
使用和前面完全一样的测试代码,获得了同样的输出:
1 | 1-1 直接执行 start |
注意事项
因为 transaction 是以“宏任务”方式运行的,而一个 transaction 在新开的宏任务中是未激活状态。
所以同时使用多个 transaction 时需要注意:
1 | const t1 = db.transaction('test', 'readwrite') |
Safari 中的注意事项
Mac 和 iOS 上的 Safari 都有一个奇怪的现象:如果 transaction 是在一个宏任务中生成的,那么必须直接使用,不能在新开的微任务中使用(当然更不能在新开的宏任务中使用)。
1 | setTimeout(() => { |
以上代码,在 Chrome 和 Firefix 中正常,在 Safari 中就会报错:Failed to execute 'getAll' on 'IDBObjectStore': The transaction is inactive or finished.
因此,考虑到兼容性,最好在所有地方都保证生成 transaction 后直接使用,不要传递到微任务中。
3.1 commit 的行为
1 | const transaction = db.transaction('test', 'readwrite') |
输出:
1 | query-1 start |
结论:
- 调用
commit()
前执行的 transaction 操作都能正常完成(即使调用commit()
当时尚未完成) - 调用
commit()
后执行 transaction 操作会抛出异常(而不是触发request.onerror
回调)
3.2 abort 的行为
1 | const transaction = db.transaction('test', 'readwrite') |
输出:
1 | put-1 finish |
结论:
- 调用
abort()
时若有尚未完成的操作,这些操作会失败,触发request.onerror
回调。 - 调用
abort()
后尝试执行 transaction 操作会抛出异常。 abort()
会撤销当前 transaction 里已执行的操作。
4. 事务隔离
代码:
1 | setTimeout(() => { |
输出:
1 | case-1: objectStore 范围完全一致的两个 transaction |
现象分析:
- 如果两个 transaction 的 objectStore 列表有交集,会等前一个 transaction 的所有读写操作结束后,才执行后一个 transaction 的操作。
(无论后一个 transaction 实际读写的是不是前一个 transaction 读写的 objectStore,也无论前一个 transaction 是 readwrite 还是 readonly) - 如果两个 transaction 的 objectStore 列表没有交集,那么执行操作不会产生等待。
结论:
- 浏览器通过让后面的 transaction 等待前面的 transaction 来实现事务间的隔离。
这样可避免传统 RDBMS 里事务 commit 时产生冲突的情况。 - 只要 transaction 的 objectStore 有交集,即使某个具体操作不涉及另一个 transaction 的 objectStore,也会发生等待。
因为这个 objectStore 里的内容可能是基于另一个 objectStore 计算出来的。
这样看也就能理解为什么建立 transaction 时必须制定 objectStore 范围了。
5. 高频率开启 transaction 的性能情况
代码:
1 | const cases = [[100, 1], [500, 5], [1000, 10], [5000, 20], [10000, 100]] |
输出:
1 | 读写 100 次,每 1 次插入开一个 transaction |
结论:
- 将多次读写操作合并到一个 transaction 里,可以显著提高运行效率。
- 但不要无意义地扩大一个 transaction 的 objectStore 范围,不然因为 transaction 间的等待机制,反而会影响效率。