如何可靠检测IndexedDB事务是否处于活跃状态?
我正在实现一个简易IndexedDB管理器,目标是在必要时自动创建新事务,简化操作接口。目前我把事务的oncomplete回调作为事务不可再用的标识,在get、add、put等方法开始时创建新事务。多数情况下这个方法可行,但偶尔会抛出事务不活跃的异常。
我做了一个复现场景:给输入框绑定input事件监听器,将输入框当前值保存到IndexedDB。快速输入时,控制台有时会抛出异常,且最后输入的值无法保存。比如同时按下1和2后,控制台输出如下:
dbManager.js:56 put 1 dbManager.js:47 Ensuring the store is valid dbManager.js:18 Generating new store dbManager.js:56 put 12 dbManager.js:47 Ensuring the store is valid dbManager.js:67 put - catch 12 dbManager.js:60 Uncaught (in promise) DOMException: Failed to execute 'put' on 'IDBObjectStore': The transaction is not active. at (...)/lib/dbManager.js:60:44 at new Promise (<anonymous>) at MyStore.put ((...)/lib/dbManager.js:58:16) at HTMLInputElement.putData ((...)/lib/script.js:5:11) dbManager.js:62 put - onsuccess 1 dbManager.js:27 Transaction completed
可以看到,transaction is not active异常在事务的oncomplete回调触发前就抛出了,这个问题在Chrome、Opera、Edge和Firefox中都会出现。
我该如何解决这个问题?有没有其他方法检测事务是否活跃?我想到一个临时方案是把请求推入FIFO队列,按顺序执行并等待每个请求完成,但这可能导致每个请求都要创建单独的事务。
lib/dbManager.js
class MyIndexedDb { async connect(name, upgrade, version) { return new Promise((resolve, reject) => { const request = indexedDB.open(name, version); request.onupgradeneeded = (e) => upgrade(request.result, e); request.onsuccess = () => { this.database = request.result; this.database.onerror = (e) => console.error(e); resolve(); }; request.onerror = () => reject(request.error); }); } getStore = (storeName, mode) => { const idbStore = new MyStore(() => { console.log("Generating new store"); if (!this.database) { throw ("Database is not defined."); } const transaction = this.database.transaction(storeName, mode); const store = transaction.objectStore(storeName); transaction.oncomplete = () => { console.log("Transaction completed"); idbStore.invalidateTransaction(); } return store; }); return idbStore; } } class MyStore { constructor(storeGenerator) { this.storeGenerator = storeGenerator; this.shouldRegenerateStore = true; } invalidateTransaction = () => { this.shouldRegenerateStore = true; } ensureValidStore = () => { console.log("Ensuring the store is valid"); if (!this.shouldRegenerateStore) { return; } this.shouldRegenerateStore = false; this.store = this.storeGenerator(); } put = async (key, value) => { console.log("put", value); this.ensureValidStore(); return new Promise((resolve, reject) => { try { const request = this.store.put(value, key); request.onsuccess = () => { console.log("put - onsuccess", value); return resolve(request.result); } } catch (e) { console.log("put - catch", value); reject(e); return; } }); } }
lib/script.js
let store, input; function putData() { try { store.put("key", input.value); } catch (e) { console.error(e); } } function upgradeDb(db) { db.createObjectStore("store"); } async function load() { const idb = new MyIndexedDb(); try { await idb.connect("db", upgradeDb); } catch (e) { console.error(e); return; } store = idb.getStore("store", "readwrite"); input = document.getElementById("input"); input.addEventListener("input", putData); } load();
index.html
<!doctype html> <html> <head> <script defer src="lib/dbManager.js"></script> <script defer src="lib/script.js"></script> </head> <body> <input id="input"/> </body> </html>
核心问题分析
问题出在事务的生命周期判断时机:IndexedDB的事务可能在oncomplete回调触发前就进入非活跃状态。当你快速发起第二个put请求时,第一个事务已经因为没有新请求而被浏览器自动关闭,但oncomplete回调还没来得及执行,此时shouldRegenerateStore仍然是false,导致复用了已经失效的事务。
解决方法
1. 捕获异常并自动重试
既然无法提前精准判断事务是否活跃,最简单的方案是在执行操作时捕获TransactionInactiveError,捕获到异常后重新生成事务并重试请求:
修改MyStore的put方法:
put = async (key, value) => { console.log("put", value); let attempt = 0; const maxAttempts = 2; // 最多重试一次 while (attempt < maxAttempts) { try { this.ensureValidStore(); return await new Promise((resolve, reject) => { const request = this.store.put(value, key); request.onsuccess = () => { console.log("put - onsuccess", value); resolve(request.result); }; request.onerror = () => reject(request.error); }); } catch (e) { if (e.name === "TransactionInactiveError" && attempt < maxAttempts - 1) { console.log("Transaction inactive, retrying put", value); this.shouldRegenerateStore = true; // 标记需要重新生成事务 attempt++; } else { console.log("put - catch", value); throw e; } } } };
2. 监听所有事务结束事件
除了oncomplete,还要监听事务的onerror和onabort事件,确保任何导致事务结束的情况都能标记shouldRegenerateStore为true:
修改getStore中的事务事件绑定:
const transaction = this.database.transaction(storeName, mode); const store = transaction.objectStore(storeName); transaction.oncomplete = () => { console.log("Transaction completed"); idbStore.invalidateTransaction(); }; transaction.onerror = () => { console.log("Transaction errored"); idbStore.invalidateTransaction(); }; transaction.onabort = () => { console.log("Transaction aborted"); idbStore.invalidateTransaction(); };
3. 使用请求队列合并操作(推荐)
你的临时方案思路可优化:不是每个请求单独创建事务,而是将短时间内的多个请求合并到同一个事务中,既保证顺序执行,又减少事务创建开销。
修改MyStore类,添加请求队列:
class MyStore { constructor(storeGenerator) { this.storeGenerator = storeGenerator; this.shouldRegenerateStore = true; this.requestQueue = []; this.isProcessingQueue = false; } invalidateTransaction = () => { this.shouldRegenerateStore = true; } ensureValidStore = () => { if (!this.shouldRegenerateStore) { return; } this.shouldRegenerateStore = false; this.store = this.storeGenerator(); } // 处理队列的核心方法 processQueue = async () => { if (this.isProcessingQueue) return; this.isProcessingQueue = true; while (this.requestQueue.length > 0) { const { operation, resolve, reject } = this.requestQueue.shift(); try { this.ensureValidStore(); const result = await new Promise((res, rej) => { const request = operation(this.store); request.onsuccess = () => res(request.result); request.onerror = () => rej(request.error); }); resolve(result); } catch (e) { if (e.name === "TransactionInactiveError") { this.shouldRegenerateStore = true; // 重新加入队列头部重试 this.requestQueue.unshift({ operation, resolve, reject }); } else { reject(e); } } } this.isProcessingQueue = false; } put = async (key, value) => { console.log("put", value); return new Promise((resolve, reject) => { this.requestQueue.push({ operation: (store) => store.put(value, key), resolve, reject }); this.processQueue(); }); } }
这个方案的优势:
- 自动合并短时间内的多个请求到同一个事务,提升性能
- 确保请求按顺序执行,避免并发冲突
- 遇到事务失效时自动重试,无需手动处理
最终效果
使用队列方案后,快速输入时所有请求都会被正确处理,不会再出现事务不活跃的异常,同时也能复用事务减少开销。
内容的提问来源于stack exchange,提问作者Jakub Janowski

