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

如何避免Node.js MySQL执行大量查询时出现死锁问题?

解决大量URL处理时MySQL死锁问题:正确串行/可控并发执行任务

你遇到的死锁问题本质是并行的数据库写入操作触发了InnoDB的锁竞争,而之前用async.queue没生效的核心原因是:你的队列任务没有真正等待load函数里的异步操作(HTTP请求+数据库插入)完成就调用了callback,导致队列实际上还是在并行执行所有任务,根本没起到串行/控并发的作用。

一、核心问题:async.queue用法错误

看你原来的队列代码:

let q = async.queue(function(task, callback) {
  console.log("Task:", task.uri);
  callback(); // 这里直接调用了callback,根本没等load完成!
})

你只是打印了任务URI就立刻结束了任务,load函数里的HTTP请求和数据库插入还是在后台并行跑,完全没被队列控制。

二、正确的解决方案:让队列等待异步任务完成

我们需要把load改造成能通知任务完成的异步函数,然后在队列任务里等待它执行完毕再调用callback。

步骤1:封装load为Promise函数

首先把回调风格的request和数据库操作改成Promise,这样更容易控制异步流程:

const request = require('request-promise'); // 改用request-promise简化Promise封装,也可以自己手动封装原生request
const async = require('async');
const pool = require('./database');

const urls = [
  'https://www.quora.com/What-is-the-best-way-to-have-delayed-job-queue-with-node-js',
  'https://de.wikipedia.org/wiki/Reinhardt-Zimmermann-L%C3%B6sung',
  'https://towardsdatascience.com/the-5-clustering-algorithms-data-scientists-need-to-know-a36d136ef68'
];

// 封装load为返回Promise的异步函数
const load = async function(url) {
  try {
    const html = await request({ url: url });
    console.log(`开始处理URL: ${url}`);
    
    // 1. 替换成你的HTML解析逻辑
    // 2. 构建插入数据(示例)
    let data = [{ title: '解析后的标题', content: '解析后的内容' }];
    let sql = "INSERT IGNORE INTO tbl_test (title, content) VALUES ?";
    // 注意:批量插入需要二维数组格式
    let values = data.map(item => [item.title, item.content]);
    
    // 等待数据库插入完成
    await new Promise((resolve, reject) => {
      pool.query(sql, [values], function(error) {
        if(error) reject(error);
        else resolve();
      });
    });
    
    console.log(`URL ${url} 处理完成`);
  } catch (error) {
    console.error(`处理URL ${url} 出错:`, error);
    // 可选:抛出错误让队列捕获,或者忽略错误继续执行下一个任务
    // throw error;
  }
};

步骤2:正确配置async.queue

现在配置队列,让每个任务真正等待load完成后再结束:

// 配置队列:concurrency设为1就是完全串行,设为2-3就是可控并发
let q = async.queue(async function(task, callback) {
  try {
    await load(task.uri);
    callback(); // 任务完成,通知队列执行下一个
  } catch (err) {
    callback(err); // 出错时通知队列,可选择是否终止后续任务
  }
}, 1); // 这里设为1表示严格串行执行,根据需求调整并发数

q.drain = function() {
  console.log('所有任务处理完成');
  pool.end();
};

q.error = function(err, task) {
  console.error(`处理任务 ${task.uri} 时出错:`, err);
};

// 向队列添加所有URL任务
urls.forEach((url) => {
  q.push({ uri: url }, function(err) {
    if(err) {
      console.log(`任务 ${url} 处理失败`);
    } else {
      console.log(`任务 ${url} 处理成功`);
    }
  });
});

步骤3:数据库连接池的小优化

你的database.js基本没问题,可额外加一点细节优化:

require('dotenv').config();
const mysql = require('mysql');

let pool = mysql.createPool({
  connectionLimit: 10,
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  // 新增:自动重试连接
  waitForConnections: true,
  queueLimit: 0
});

pool.getConnection((err, connection) => {
  if(err) {
    switch(err.code) {
      case 'PROTOCOL_CONNECTION_LOST':
        console.log('数据库连接丢失,将自动重试');
        break;
      case 'ER_CON_COUNT_ERROR':
        console.log('数据库连接数过多');
        break;
      case 'ECONNREFUSED':
        console.log('数据库连接被拒绝');
        break;
      case 'POOL_CLOSED':
        console.log('连接池已关闭');
        break;
      default:
        console.error('数据库连接错误:', err);
    }
  }
  if(connection) connection.release();
});

module.exports = pool;

三、额外优化建议

  • 控制批量插入大小:如果每个URL解析后的数据量很大,拆分多个小批量插入,减少锁持有时间。
  • 死锁排查:如果偶尔仍出现死锁,可执行SHOW ENGINE INNODB STATUS;查看死锁详情,确认是否有其他业务逻辑导致锁竞争。
  • 错误重试机制:对数据库操作添加重试逻辑,应对偶发的锁等待超时问题。

这样改完后,队列会严格按照你设置的并发数执行任务,每个URL的HTTP请求和数据库插入都完成后才会处理下一个,从根本上避免并行写入导致的锁竞争,死锁问题就能解决了。

内容的提问来源于stack exchange,提问作者Rep

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:46:56