如何避免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
相关产品推荐
相关产品推荐

