事务竞争条件:为什么在 Pool 上的 BEGIN 会导致一切崩溃

发布: (2026年1月1日 GMT+8 05:38)
3 min read
原文: Dev.to

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/COMMITpool.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];

不要让竞争条件破坏你的数据。 ⭐️ 在 GitHub 上给仓库加星以获取更新。

Back to Blog

相关文章

阅读更多 »

使用事务让 DB 更改可预测

你好,我是Maneshwar。我正在开发 FreeDevTools(在线)https://hexmos.com/freedevtools,旨在打造一个汇聚所有 dev tools、cheat codes 和 TLDRs 的统一平台——...