【问题标题】:Thinking in JavaScript promises (Bluebird in this case)在 JavaScript 承诺中思考(在本例中为 Bluebird)
【发布时间】:2015-07-23 11:18:14
【问题描述】:

我试图了解一些不太重要的承诺/异步用例。在我目前正在处理的一个示例中,我有一个从 knex 查询(thenable 数组)返回的书籍数组,我希望将其插入数据库:

books.map(function(book) {

  // Insert into DB

});

每本书的外观如下:

var book = {
    title: 'Book title',
    author: 'Author name'
};

但是,在插入每本书之前,我需要从单独的表中检索作者的 ID,因为该数据已标准化。作者可能存在也可能不存在,所以我需要:

  • 检查作者是否存在于数据库中
  • 如果是,请使用此 ID
  • 否则,插入作者并使用新ID

不过,上面的操作也都是异步的。

我可以在原始映射(获取和/或插入 ID)中使用承诺作为插入操作的先决条件。但这里的问题是,由于一切都是异步运行的,代码很可能会插入重复的作者,因为初始的 check-if-author-exists 与 insert-a-new-author 块是分离的。

我可以想到几种方法来实现上述目标,但它们都涉及拆分承诺链,而且通常看起来有点混乱。这似乎是必须经常出现的问题。我确定我在这里遗漏了一些基本的东西!

有什么建议吗?

【问题讨论】:

  • 你不想要一个 "Upsert" 类型的方法。即总是会返回一个作者ID,如果它不存在那么它会创建它,如果它确实存在那么它只会返回现有的作者ID?这样一来,您就可以减少在检查插入之间插入某些东西的机会。您可能还想在这里使用某种锁定机制
  • 你要插入多少本书,你的数组有多大?最好先返回一组所有作者 ID,然后在客户端合并该数据。此外,很多这种逻辑可能在数据库中执行得更好,而不是在 JavaScript 中。
  • 对于这样做的正确方法是值得的,这将是一个 upsert 并让数据库为您处理这个问题。问题是 upsert 是非常新的(5 天前在 Pg 中实施)所以 knex 还没有赶上。
  • 另外,如果可能的话,您真的想发送单个查询,而不是批处理 n 插入。在客户端实现这种逻辑和并发管理对我来说似乎很愚蠢。使用事务可能会更简单。

标签: javascript node.js asynchronous promise bluebird


【解决方案1】:

这是我将如何实现它。我认为一些重要的要求是:

  • 从未创建过重复的作者(这也应该是数据库本身的一个约束)。
  • 如果服务器在中间没有回复 - 没有插入不一致的数据。
  • 可以输入多个作者。
  • 不要对数据库进行 n 查询 n 的事情 - 避免经典的“n+1”问题。

我会使用事务来确保更新是原子的——也就是说,如果操作正在运行并且客户端在中间死去——没有书就不会创建作者。临时失败不会导致内存泄漏也很重要(就像在作者地图中保持失败承诺的答案一样)。

knex.transaction(Promise.coroutine(function*(t) {
    //get books inside the transaction
    var authors = yield books.map(x => x.author);
    // name should be indexed, this is a single query
    var inDb = yield t.select("authors").whereIn("name", authors);
    var notIn = authors.filter(author => !inDb.includes("author"));
    // now, perform a single multi row insert on the transaction
    // I'm assuming PostgreSQL here (return IDs), this is a bit different for SQLite
    var ids = yield t("authors").insert(notIn.map(name => {authorName: name });
    // update books _inside the transaction_ now with the IDs array
})).then(() => console.log("All done!"));

这样做的优点是只进行固定数量的查询,并且可能更安全且性能更好。此外,您的数据库处于不一致的状态(尽管您可能需要为多个实例重试该操作)。

【讨论】:

  • 我想我的很多观点是,您应该首先考虑“数据库”,然后考虑“承诺”。
【解决方案2】:

假设您可以并行处理每本书。那么一切都很简单(仅使用 ES6 API):

Promise
  .all(books.map(book => {
    return getAuthor(book.author)
          .catch(createAuthor.bind(null, book.author));
          .then(author => Object.assign(book, { author: author.id }))
          .then(saveBook);
  }))
  .then(() => console.log('All done'))

问题在于获取作者和创建新作者之间存在竞争条件。考虑以下事件顺序:

  • 我们尝试为书籍 B 获取作者 A;
  • 获取作者 A 失败;
  • 我们请求创建作者A,但尚未创建;
  • 我们尝试为图书 C 获取作者 A;
  • 获取作者 A 失败;
  • 我们要求创建作者 A(再次!);
  • 第一个请求完成;
  • 第二个请求完成;

现在我们在作者表中有两个 A 实例。这是不好的! 为了解决这个问题,我们可以使用传统的方法:锁定。我们需要保留每个作者锁的表。当我们发送创建请求时,我们锁定了相应的锁。请求完成后,我们将其解锁。所有其他涉及同一作者的操作都需要先获取锁,然后再进行任何操作。

这似乎很难,但在我们的例子中可以简化很多,因为我们可以使用我们的请求承诺而不是锁:

const authorPromises = {};

function getAuthor(authorName) {

  if (authorPromises[authorName]) {
    return authorPromises[authorName];
  }

  const promise = getAuthorFromDatabase(authorName)
    .catch(createAuthor.bind(null, authorName))
    .then(author => {
      delete authorPromises[authorName];
      return author;
    });

  authorPromises[author] = promise;

  return promise;
}

Promise
  .all(books.map(book => {
    return getAuthor(book.author)
          .then(author => Object.assign(book, { author: author.id }))
          .then(saveBook);
  }))
  .then(() => console.log('All done'))

就是这样!现在,如果对作者的请求正在进行中,则将返回相同的承诺。

【讨论】:

  • 很高兴在这里见到你!我可以用蓝鸟糖美化你的代码吗?
  • 故意使用仅限 ES6 的 API。随意添加蓝鸟风味的例子进行比较!
  • 仅作记录,这个答案虽然不错,但并不能真正解决 OP 的问题。防止多个作者应该在数据库级别而不是在代码级别完成 - 如果多个节点实例运行,或者服务器曾经关闭......更不用说如果createAuthor 由于网络中断而抛出会发生什么。
  • 这是真的!更多的脚本/数据引导解决方案。
  • 本杰明也有兴趣看到蓝鸟漂亮的版本!
猜你喜欢
  • 1970-01-01
  • 2017-08-06
  • 2016-07-27
  • 2014-05-06
  • 2017-04-09
  • 1970-01-01
  • 2014-07-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多