You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:10:15