Node.js/MSSQL循环复用预准备语句报错求助
解决TS/NodeJS/MSSQL中预准备语句复用的连接占用问题
嘿,我刚看完你的问题,这个错误其实是新手用Node.js异步操作+数据库预准备语句时很容易踩的坑,我来一步步帮你理清问题和解决方案。
首先,先拆解你遇到的错误Can't acquire connection for the request. There is another request in progress的核心原因:
forEach不支持异步等待:你在products.forEach(async (p) => { ... })里用了async函数,但forEach会直接遍历所有元素并立即触发所有异步操作,不会等待前一个操作完成。这就导致瞬间有大量的execute请求同时向连接池要连接,要么连接池被打满,要么预准备语句绑定的连接还没释放就被下一个请求争抢。- 预准备语句的生命周期管理不严谨:你的代码里
unprepare是在iterateProducts函数外面调用的,但如果forEach里的异步操作还没完成就执行unprepare,也会导致报错。 - 连接池初始化可能没确保成功:你创建了ConnectionPool,但没确保连接已经成功建立就开始使用预准备语句,这也可能导致连接获取失败。
接下来是具体的解决方案,我会给你修正后的完整代码,同时解释每个关键点:
1. 先确保连接池正确初始化
首先,连接池需要先连接成功才能使用,建议用异步方式初始化,避免在未连接的状态下操作:
import mssql from 'mssql'; // 用ES模块导入更符合TS风格 export interface IProduct { name: string; price: number; } // 初始化连接池并确保连接成功 const pool = new mssql.ConnectionPool({ server: '[server address]', database: '[database name]', user: '[username]', password: '[password]', options: { encrypt: true, // 如果是Azure SQL需要开启 trustServerCertificate: true // 本地开发可以开启,生产环境建议关闭 }, pool: { max: 10, // 连接池最大连接数,可根据需求调整 min: 2, idleTimeoutMillis: 30000 } }); // 提前初始化连接池,避免每次操作都重新连接 async function initPool() { try { await pool.connect(); console.log('数据库连接池初始化成功'); } catch (err) { console.error('连接池初始化失败:', err); process.exit(1); // 连接失败直接退出进程,避免后续操作报错 } } // 程序启动时初始化连接池 initPool();
2. 正确使用预准备语句+控制异步循环
把forEach改成for...of,这样可以确保每个异步操作完成后再执行下一个,避免并发请求挤爆连接池。同时,把预准备语句的创建、prepare、execute、unprepare都放在同一个异步函数里,确保生命周期完整:
async function iterateProducts(products: Array<IProduct>) { if (!pool.connected) { throw new Error('连接池未初始化,请先调用initPool'); } // 创建预准备语句 const insertStmt = new mssql.PreparedStatement(pool); insertStmt.input("name", mssql.NVarChar); // 可以直接用mssql.NVarChar,不用写TYPES insertStmt.input("price", mssql.Float); try { // 先执行prepare,绑定SQL语句 await insertStmt.prepare("INSERT INTO Products (name, price) VALUES (@name, @price)"); // 用for...of替代forEach,确保异步操作顺序执行 for (const product of products) { const result = await insertStmt.execute(product); console.log(`插入产品${product.name}成功,影响行数: ${result.rowsAffected[0]}`); // 这里可以做插入后的操作,比如记录日志、更新缓存等 } } catch (err) { console.error('产品插入操作失败:', err); throw err; // 抛出错误让上层逻辑处理 } finally { // 不管成功失败,都要unprepare释放预准备语句占用的资源 await insertStmt.unprepare(); console.log('预准备语句已释放'); } }
3. 可选:控制并发数(如果产品数量很大)
如果你的产品数组非常大,顺序执行太慢,可以用Promise.all结合分批处理来控制并发数,避免一次性发起太多请求:
// 分批处理,比如每次并发5个请求 async function iterateProductsWithConcurrency(products: Array<IProduct>, concurrency = 5) { if (!pool.connected) { throw new Error('连接池未初始化,请先调用initPool'); } const insertStmt = new mssql.PreparedStatement(pool); insertStmt.input("name", mssql.NVarChar); insertStmt.input("price", mssql.Float); try { await insertStmt.prepare("INSERT INTO Products (name, price) VALUES (@name, @price)"); // 将产品数组分成多个批次 const batches = []; for (let i = 0; i < products.length; i += concurrency) { batches.push(products.slice(i, i + concurrency)); } // 依次处理每个批次,每个批次内并发执行 for (const batch of batches) { await Promise.all(batch.map(async (product) => { const result = await insertStmt.execute(product); console.log(`插入产品${product.name}成功`); })); } } catch (err) { console.error('批量产品插入失败:', err); throw err; } finally { await insertStmt.unprepare(); } }
关键注意点总结
- 永远不要用
forEach处理异步操作:forEach会忽略async函数的返回值,导致并发失控,改用for...of或者带并发控制的Promise.all。 - 预准备语句的生命周期要闭环:prepare后一定要unprepare,放在finally块里确保即使报错也会执行,避免资源泄漏。
- 确保连接池先连接成功:在使用任何数据库操作前,确认连接池已经处于connected状态,避免连接获取失败。
- 根据场景调整连接池配置:如果经常遇到连接不够的情况,可以调整ConnectionPool的
pool参数,增大最大连接数。
这样修改后,你应该就不会再遇到那个连接占用的错误了,预准备语句也能正确复用。
内容的提问来源于stack exchange,提问作者Yiorgos
相关产品推荐
相关产品推荐

