在 JavaScript promises 中思考(在本例中为 Bluebird)

Thinking in JavaScript promises (Bluebird in this case)

我正在努力解决一些不那么琐碎的 promise/asynchronous 用例。在我目前正在努力的一个例子中,我有一个从 knex 查询返回的书籍数组(thenable 数组)我希望插入到数据库中:

books.map(function(book) {

  // Insert into DB

});

每本书看起来像:

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

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

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

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

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

有什么建议吗?

假设您可以并行处理每本书。那么一切就很简单了(只使用 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(再次!);
  • 第一个请求完成;
  • 第二个请求完成;

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

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

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'))

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

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

  • 不会创建重复的作者(这也应该是数据库本身的一个约束)。
  • 如果服务器没有在中间回复 - 没有插入不一致的数据。
  • 可以输入多个作者。
  • 不要为 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!"));

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