SQLite异步批量插入返回重复ID,部分文件未写入数据库求助
这不是SQLite的固有问题,你的问题大概率出在并发场景下的数据库连接管理不当、SQL参数化不彻底,或者事务使用错误上,以下是具体分析和解决办法:
可能的原因
连接复用导致的竞态问题
如果多个上传请求共用同一个SQLite连接,异步执行插入操作时,可能出现请求间的竞态:前一个请求的RETURNING Id结果被后一个请求错误获取,导致返回重复ID,而实际未插入成功的请求是因为SQLite的单写机制阻塞或覆盖了并发写入。SQL拼接的风险与时序问题
你的SQL语句中,uploaderId、extension.Id、fileSize、Created都是直接字符串拼接,只有FileName用了参数。这种写法不仅存在SQL注入风险,还可能在高并发下因为字符串拼接的时序问题,导致生成的SQL语句不符合预期,进而影响插入结果和ID返回。事务使用错误
你提到用事务时出现语法错误,大概率是事务的执行逻辑不对:SQLite的事务必须在同一个数据库连接内完成BEGIN、INSERT、COMMIT/ROLLBACK的完整流程,跨连接的事务操作必然报错。
解决步骤
1. 彻底修复参数化查询
把所有变量都改为参数化传递,杜绝字符串拼接:
const insertSql = `INSERT INTO ${this.TableName} ( UploaderId, ExtensionId, Size, FileName, IsStaging, Created ) VALUES (?, ?, ?, ?, 1, ?) RETURNING Id;`; const insertResult = await Db.GetAsync(insertSql, uploaderId, extension.Id, fileSize, nameDetails.NameSanExtension, SysTrick.NowToUtcString() );
参数化查询不仅能避免SQL注入,还能确保每个请求的SQL语句独立,不会被并发操作干扰。
2. 确保每个请求使用独立的数据库连接
SQLite本质是单写数据库,高并发场景下必须保证每个请求使用独立的连接(或通过连接池管理连接),避免多个异步请求共用同一连接导致的竞态:
- 如果使用
sqlite(Promise版)或sqlite3,建议用连接池分配连接; - 如果用
better-sqlite3,注意它是单线程同步库,需要用异步包装(如worker_threads)处理并发请求,避免阻塞。
3. 正确使用事务包裹插入操作
如果需要保证插入的原子性,事务必须在同一个连接内执行,示例代码如下:
// 假设DbPool是你的数据库连接池实例 const dbConn = await DbPool.acquire(); try { await dbConn.run('BEGIN TRANSACTION'); const insertResult = await dbConn.get(insertSql, [ uploaderId, extension.Id, fileSize, nameDetails.NameSanExtension, SysTrick.NowToUtcString() ]); await dbConn.run('COMMIT'); result.Id = SysTrick.SafeInteger(insertResult.Id); } catch (err) { await dbConn.run('ROLLBACK'); throw err; // 抛出错误让上层处理 } finally { DbPool.release(dbConn); // 释放连接回池 }
4. 检查自增ID的定义
确保你的ID字段是INTEGER PRIMARY KEY AUTOINCREMENT,而不是单纯的INTEGER PRIMARY KEY。后者是SQLite rowid的别名,删除记录后可能会复用ID;前者会严格递增,确保数据库内ID绝对唯一(这一步是补充,不是直接解决返回重复ID的问题,但能强化数据完整性)。
总结
你的问题是并发场景下的代码实现问题,和SQLite本身的特性无关。按照上面的步骤修复后,多文件上传时的ID返回重复、记录丢失问题应该能解决。
内容的提问来源于stack exchange,提问作者abc

