Node.js+better-sqlite3如何避免长耗时更新与查询操作冲突
实现方案
你这个场景不需要引入复杂的第三方库,用状态标记+等待队列+SQLite事务的组合就能完全满足需求,比单独用标记位或者全局暂停数据库活动的方案都更稳妥,性能也更好。
首先先修正你现有代码的异步逻辑bug:你写的hypervisors.forEach(async function(hypervisor) { ... })不会等待内部的HTTP请求返回,会直接执行后续逻辑,导致关联表数据还没插完,后续查询就已经跑了,这个问题要先处理。
第一步:实现极简更新锁
总共就3个变量,实现更新中状态标记、等待队列逻辑,支持重复更新任务去重、查询等待/跳过两种策略:
// 锁状态:标记当前是否有更新任务在执行 let isUpdating = false; // 等待队列:存放所有等待更新完成的回调 const waitQueue = []; /** * 执行更新任务的包装方法 * 同一时间只会跑一个更新任务,重复触发会等待当前任务执行完成 */ async function withUpdateLock(updateTask) { if (isUpdating) { return new Promise(resolve => waitQueue.push(resolve)); } isUpdating = true; try { await updateTask(); } finally { isUpdating = false; // 唤醒所有等待中的任务 while (waitQueue.length) { waitQueue.shift()(); } } } /** * 执行查询的包装方法 * @param {Function} queryFn 查询逻辑(better-sqlite3同步逻辑直接写在这里) * @param {boolean} skipIfUpdating 更新中是否直接跳过,默认false=等更新完再查 * @returns 查询结果,跳过时返回null */ async function runQuery(queryFn, skipIfUpdating = false) { if (isUpdating) { if (skipIfUpdating) return null; await new Promise(resolve => waitQueue.push(resolve)); } return queryFn(); }
第二步:优化更新逻辑,拆分拉数和写入阶段
不要把HTTP请求和数据库写操作混在一起,先把所有需要的远程数据全部拉到内存里,再开一个SQLite事务一次性写入所有数据:
- 事务的特性是要么所有写入全部成功,要么全部失败,不会出现半写的脏数据
- 内存数据写入SQLite的速度极快,通常是毫秒级,几乎不会阻塞操作
修正后的buildDatabase代码如下:
async function buildDatabase() { // 建表操作,执行很快 db.exec('CREATE TABLE IF NOT EXISTS VMS (name TEXT, id TEXT, environment TEXT, ait TEXT)'); db.exec('CREATE TABLE IF NOT EXISTS HYPERVISORS (name TEXT, id TEXT, environment TEXT)'); db.exec('CREATE TABLE IF NOT EXISTS HOSTING (vm TEXT, hypervisor TEXT)'); // ========== 第一阶段:拉取所有远程数据,这个阶段不做任何数据库写操作 ========== const hypervisors = await api.getEntities(test_config, 'type("HYPERVISOR")'); // 替换原来的forEach异步写法,用for...of等待所有VM数据拉取完成 const hostingRelations = []; for (const hypervisor of hypervisors) { const runningVms = await api.getCurrentVMs(test_config, hypervisor.entityId); runningVms.forEach(vmId => { hostingRelations.push({ vmId, hypervisorId: hypervisor.entityId }); }); } const vms = await api.getEntities(test_config, 'type("HOST"),hypervisorType("VMWARE")'); const vmRecords = vms.map(vm => { let aitTagValue = "missing"; for (const tag of vm.tags) { if (tag.key === AIT_TAG_KEY) { aitTagValue = tag.value; break; } } return { name: vm.displayName, id: vm.entityId, env: test_config.host, ait: aitTagValue }; }); // ========== 第二阶段:开事务一次性写入所有数据,毫秒级完成 ========== const writeAllData = db.transaction(() => { // 清空旧数据,避免重复插入,如果需要增量更新可以调整这部分逻辑 db.exec('DELETE FROM HYPERVISORS'); db.exec('DELETE FROM HOSTING'); db.exec('DELETE FROM VMS'); // 批量写入hypervisor数据 const insertHv = db.prepare("INSERT INTO HYPERVISORS VALUES (?, ?, ?)"); for (const hv of hypervisors) { insertHv.run(hv.displayName, hv.entityId, test_config.host); } // 批量写入关联关系 const insertHosting = db.prepare("INSERT INTO HOSTING VALUES (?, ?)"); for (const rel of hostingRelations) { insertHosting.run(rel.vmId, rel.hypervisorId); } // 批量写入VM数据 const insertVm = db.prepare("INSERT INTO VMS VALUES (?, ?, ?, ?)"); for (const vm of vmRecords) { insertVm.run(vm.name, vm.id, vm.env, vm.ait); } }); writeAllData(); }
第三步:调度和查询调用方式
- 定时任务触发更新时,直接用锁包装调用即可,哪怕多个更新任务同时触发,也只会执行一个,不会重复写入:
// node-schedule的任务里直接这么写 schedule.scheduleJob('你的cron表达式', () => { withUpdateLock(buildDatabase); });
- 查询时按需选择策略,不需要每次查询都触发更新:
// 策略1:等更新完成再查询 const allVms = await runQuery(() => { return db.prepare('SELECT * FROM VMS').all(); }); // 策略2:如果正在更新就直接跳过本次查询 const allHvs = await runQuery(() => { return db.prepare('SELECT * FROM HYPERVISORS').all(); }, true);
方案优势
- 逻辑简单,总代码量不到100行,没有额外依赖,出问题很容易排查
- 完全避免脏读:事务保证写入原子性,锁机制保证查询不会在写入过程中执行
- 性能好:数据库写入阶段是批量同步操作,耗时极短,不会长时间阻塞
- 支持你要的两种查询策略,也自动处理了重复触发更新任务的去重问题
内容的提问来源于stack exchange,提问作者funkmeister
相关产品推荐
相关产品推荐

