首页 > 解决方案 > Knex:获取连接超时。游泳池可能已经满了。你错过了一个 .transacting(trx) 电话吗?使用 Knex.Transaction 的最佳实践

问题描述

在使用具有多个表和多个数据库操作的大型应用程序时,很难跟踪正在发生的事务。为了解决这个问题,我们从传递一个trx对象开始。

事实证明这是非常混乱的。

例如:

async getOrderById(id: string, trx?: Knex.Transaction) { ... }

根据调用getOrderById它的函数,它要么传递一个trx对象,要么不传递。trx如果不为空,则将使用上述函数。

乍一看这似乎很简单,但它会导致错误,如果您在一个函数的事务中间并调用另一个不使用事务的函数,knex 将挂起Knex: Timeout acquiring a connection. The pool is probably full.

async getAllPurchasesForUser(userId: string) {
  ..
  const trx = await knex.transaction();
  try {
    ..
    getPurchaseForUserId(userId); // Forgot to make this consume trx, hence Knex timesout acquiring connection.
    ..
}

基于此,我假设这不是最佳实践,但如果 Knex 开发团队的某个人可以发表评论,我会很高兴。

为了改进这一点,我们正在考虑改为使用knex.transactionProvider()在我们执行数据库操作的任何地方都可以在整个应用程序中访问的那个。

网站上的示例似乎不完整:

// Does not start a transaction yet
const trxProvider = knex.transactionProvider();

const books = [
  {title: 'Canterbury Tales'},
  {title: 'Moby Dick'},
  {title: 'Hamlet'}
];

// Starts a transaction
const trx = await trxProvider();
const ids = await trx('catalogues')
  .insert({name: 'Old Books'}, 'id')
books.forEach((book) => book.catalogue_id = ids[0]);
await  trx('books').insert(books);

// Reuses same transaction
const sameTrx = await trxProvider();
const ids2 = await sameTrx('catalogues')
  .insert({name: 'New Books'}, 'id')
books.forEach((book) => book.catalogue_id = ids2[0]);
await sameTrx('books').insert(books);

在实践中,我正在考虑如何使用它:

SingletonDBClass.ts:

const trxProvider = knex.transactionProvider();
export default trxProvider;

订单.ts

import trx from '../SingletonDBClass';
..
async getOrderById(id: string) {
  const trxInst = await trx;
  try {
    const order = await trxInst<Order>('orders').where({id});
    trxInst.commit();
    return order;
  } catch (e) {
    trxInst.rollback();
    throw new Error(`Failed to fetch order, error: ${e}`);
  }
}
..

我是否正确理解这一点?

另一个实际需要事务的示例函数:

async cancelOrder(id: string) {
  const trxInst = await trx;
  try {
    trxInst('orders').update({ status: 'CANCELED' }).where({ id });
    trxInst('active_orders').delete().where({ orderId: id });
    trxInst.commit();
  } catch (e) {
    trxInst.rollback();
    throw new Error(`Failed to cancel order, error: ${e}`);
  }
}

有人可以确认我是否理解正确吗?更重要的是,如果这是做到这一点的好方法。或者有没有我错过的最佳实践?

感谢您的帮助 knex 团队!

标签: node.jspostgresqltypescriptknex.js

解决方案


不,您不能让全局单例类为您的所有内部函数返回事务。否则,您总是试图为所有尝试在应用程序中做不同事情的并发用户使用相同的事务。

此外,当您一旦提交/回滚提供者返回的事务时,它将不再适用于其他查询。交易提供者只能给你单笔交易。

事务提供程序在以下情况下很有用,例如中间件,它为请求处理程序提供事务,但不应该启动它,因为它可能不需要,所以你不想从池中为其分配连接。

做你的事情的好方法是传递事务或一些请求上下文或用户会话,以便每个并发用户可以拥有自己的单独事务。

例如:

async cancelOrder(trxInst, id: string) {
  try {
    trxInst('orders').update({ status: 'CANCELED' }).where({ id });
    trxInst('active_orders').delete().where({ orderId: id });
    trxInst.commit();
  } catch (e) {
    trxInst.rollback();
    throw new Error(`Failed to cancel order, error: ${e}`);
  }
}

根据调用 getOrderById 的函数,它要么传递一个 trx 对象,要么不传递。如果不为空,上述函数将使用 trx。

起初这看起来很简单,但它会导致错误,如果您在一个函数的事务中间并调用另一个不使用事务的函数,knex 将与著名的 Knex: Timeout 获取连接挂起。游泳池可能已经满了。

我们通常这样做的方式是,如果 trx 为 null,则查询会抛出错误,因此您需要显式传递 knex / trx 才能执行该方法,并且在某些方法中实际上需要传递 trx。

无论如何,如果您真的想在默认情况下强制所有内容在会话中通过单个事务,您可以创建 API 模块,为每个用户会话创建一个使用事务初始化的 API 实例:

const dbForSession = new DbService(trxProvider);

const users = await dbForSession.allUsers();

.allUsers()做类似的事情return this.trx('users');


推荐阅读