事务竞争条件:为什么在 Pool 上的 BEGIN 会导致一切崩溃
I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content (or the specific sections) you want converted to Simplified Chinese? Once I have the text, I’ll keep the source link at the top and preserve all formatting as you requested.
Problem
// ❌ Dangerous: Transaction on pool
async function transferFunds(from, to, amount) {
await pool.query('BEGIN');
await pool.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [
amount,
from,
]);
await pool.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [
amount,
to,
]);
await pool.query('COMMIT');
}
PostgreSQL 连接池是一组客户端连接。每个 pool.query() 可能使用不同的客户端。
- 请求 1:
pool.query('BEGIN')→ 客户端 A - 请求 1:
pool.query('UPDATE…')→ 客户端 B(不同!) - 请求 2:
pool.query('BEGIN')→ 客户端 A(复用!)
你的事务现在跨多个客户端,导致数据不一致。
安全做法
// ✅ Safe: Get dedicated client, use it for entire transaction
async function transferFunds(from, to, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, from],
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, to],
);
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
使用同一个 client 执行 BEGIN、所有查询以及 COMMIT,可以保证事务的完整性。
直接在连接池上会失败
// ❌ pool.query('BEGIN') → Error
// ❌ pool.query('COMMIT') → Error
// ❌ pool.query('ROLLBACK') → Error
// ❌ pool.query('SAVEPOINT') → Error
可行的做法
// ✅ client.query('BEGIN') → OK
// ✅ pool.query('SELECT…') → OK (no transaction)
代码检查规则:no-transaction-on-pool
安装 ESLint 插件:
npm install --save-dev eslint-plugin-pg
进行配置:
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
该规则会捕获所有误用:
src/transfer.ts
3:9 error 🔒 CWE-362 | Transaction command on pool - use pool.connect() for transactions
Fix: const client = await pool.connect(); client.query('BEGIN');
可重用的事务包装器
// ✅ 可重用的事务包装器
async function withTransaction(callback) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
// 用法
await withTransaction(async (client) => {
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, from],
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, to],
);
});
使用场景
| 场景 | 推荐的 API |
|---|---|
| 单次查询 | pool.query() |
| 多个独立查询 | pool.query() |
事务(BEGIN/COMMIT) | pool.connect() → client.query() |
| 长时间运行的会话 | pool.connect() → client.query() |
安装与资源
npm install --save-dev eslint-plugin-pg
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
- npm 包:
eslint-plugin-pg - 规则文档: no-transaction-on-pool
- GitHub: https://github.com/your-repo/eslint-plugin-pg
不要让竞争条件破坏你的数据。 ⭐️ 在 GitHub 上给仓库加星以获取更新。