订单异常不回滚主因是未显式调用rollback()或Promise悬停;应使用mysql2/promise封装execTransaction函数,确保beginTransaction、commit/rollback及connection.release()成对执行,并将所有SQL和业务逻辑置于同一connection事务上下文中。

订单异常时事务不回滚,最常见原因是没显式调用 rollback(),或在 catch 块里漏掉错误抛出导致 Promise 悬停。直接上可落地的配置方式。
用 mysql2/promise 启动事务并捕获异常
别用老版 mysql 包——它默认不返回 Promise,手动封装容易漏 connection.release() 或忘记 rollback()。必须用 mysql2/promise:
const mysql = require('mysql2/promise');
const pool = mysql.createPool({ /* 配置同前 */ });关键点:
-
pool.getConnection()返回 Promise,配合async/await可线性写逻辑 - 事务开启后,所有
connection.query()必须复用同一个connection实例,不能混用pool.query() - 必须在
finally块里调用connection.release(),否则连接池会耗尽
execTransaction 封装函数要带 commit 和 rollback 控制权
业务代码里不能靠人工判断“该不该提交”,得把控制权交给封装层。参考这个最小可用封装:
const execTransaction = async (cb) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const result = await cb(connection);
await connection.commit();
return result;
} catch (err) {
await connection.rollback();
throw err;
} finally {
connection.release();
}
};调用时这样写:
await execTransaction(async (conn) => {
await conn.query('INSERT INTO orders (...) VALUES (?)', [orderData]);
const [rows] = await conn.query('SELECT LAST_INSERT_ID()');
const orderId = rows[0]['LAST_INSERT_ID()'];
await conn.query('INSERT INTO order_items (...) VALUES (?)', [items, orderId]);
// 中间任何一步 throw,都会触发 rollback
});注意:
- 传入的回调函数必须接收
conn参数,所有 SQL 都走它 - 不要在回调里写
await pool.query(...),那会脱离当前事务上下文 - 如果回调里有外部异步操作(如调用微信支付 API),失败时也要
throw,否则事务不会回滚
库存扣减 + 订单生成场景下,回滚失效的典型坑
微信商城类项目最容易在这里翻车:扣库存和写订单分属不同表,但事务只包了订单部分,库存更新走了另一条路。
正确做法是把这两步都塞进同一个 execTransaction:
await execTransaction(async (conn) => {
// 1. 扣库存(带 WHERE 条件防超卖)
const [result] = await conn.query(
'UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?',
[quantity, productId, quantity]
);
if (result.affectedRows === 0) {
throw new Error('库存不足');
}
// 2. 写订单主表
await conn.query('INSERT INTO orders (...) VALUES (?)', [orderData]);
// 3. 写订单明细
await conn.query('INSERT INTO order_items (...) VALUES (?)', [items]);
});常见错误:
- 用
SELECT ... FOR UPDATE却没加事务——MySQL 会自动提交,锁立刻释放 - 库存更新用了
pool.query(),而订单用了conn.query(),两者不在同一事务中 - 错误地认为「先查再 update」能防超卖,实际并发下仍可能双写,必须用原子 update + 条件判断
真正难的是把业务逻辑、第三方调用、数据库操作三者耦合进一个事务作用域。一旦涉及 HTTP 请求(比如调支付网关),就只能拆成补偿事务或本地消息表——纯 MySQL 事务兜不住跨系统失败。


















