简体   繁体   English

思考JavaScript承诺(在这种情况下蓝鸟)

[英]Thinking in JavaScript promises (Bluebird in this case)

I'm trying to get my head around some not quite so trivial promise/asynchronous use-cases. 我试图了解一些不那么琐碎的承诺/异步用例。 In an example I'm wrestling with at the moment, I have an array of books returned from a knex query (thenable array) I wish to insert into a database: 在一个我正在努力解决的例子中,我有一个从knex查询(可编组数组)返回的书籍数组我希望插入到数据库中:

books.map(function(book) {

  // Insert into DB

});

Each book item looks like: 每个书籍项目如下:

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

However, before I insert each book, I need to retrieve the author's ID from a separate table since this data is normalised. 但是,在我插入每本书之前,我需要从一个单独的表中检索作者的ID,因为这些数据是标准化的。 The author may or may not exist, so I need to: 作者可能存在也可能不存在,因此我需要:

  • Check if the author is present in the DB 检查作者是否存在于DB中
  • If it is, use this ID 如果是,请使用此ID
  • Otherwise, insert the author and use the new ID 否则,插入作者并使用新ID

However, the above operations are also all asynchronous. 但是,上述操作也都是异步的。

I can just use a promise within the original map (fetch and/or insert ID) as a prerequisite of the insert operation. 我可以在原始映射中使用promise(获取和/或插入ID)作为插入操作的先决条件。 But the problem here is that, because everything's ran asynchronously, the code may well insert duplicate authors because the initial check-if-author-exists is decoupled from the insert-a-new-author block. 但问题在于,因为所有内容都是异步运行的,所以代码可能会插入重复的作者,因为初始check-if-author-exists与insert-a-new-author块分离。

I can think of a few ways to achieve the above but they all involve splitting up the promise chain and generally seem a bit messy. 我可以想到实现上述目标的几种方法,但它们都涉及拆分承诺链,而且通常看起来有点混乱。 This seems like the kind of problem that must arise quite commonly. 这似乎是一种必须出现的问题。 I'm sure I'm missing something fundamental here! 我确定我在这里缺少一些基本的东西!

Any tips? 有小费吗?

Let's assume that you can process each book in parallel. 我们假设您可以并行处理每本书。 Then everything is quite simple (using only ES6 API): 然后一切都很简单(仅使用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'))

The problem is that there is a race condition between getting author and creating new author. 问题是获取作者和创建新作者之间存在竞争条件。 Consider the following order of events: 请考虑以下事件顺序:

  • we try to get author A for book B; 我们试图为作者B获得作者A;
  • getting author A fails; 让作者A失败;
  • we request creating author A, but it is not created yet; 我们要求创建作者A,但它尚未创建;
  • we try to get author A for book C; 我们试图为作者C获得作者A;
  • getting author A fails; 让作者A失败;
  • we request creating author A (again!); 我们要求创建作者A(再次!);
  • first request completes; 第一次请求完成;
  • second request completes; 第二个请求完成;

Now we have two instances of A in author table. 现在我们在作者表中有两个A实例。 This is bad! 这是不好的! To solve this problem we can use traditional approach: locking. 要解决这个问题,我们可以使用传统方法:锁定。 We need keep a table of per author locks. 我们需要保留每个作者锁的表。 When we send creation request we lock the appropriate lock. 当我们发送创建请求时,我们锁定适当的锁。 After request completes we unlock it. 请求完成后,我们解锁它。 All other operations involving the same author need to acquire the lock first before doing anything. 涉及同一作者的所有其他操作需要在执行任何操作之前先获取锁。

This seems hard, but can be simplified a lot in our case, since we can use our request promises instead of locks: 这似乎很难,但在我们的案例中可以简化很多,因为我们可以使用我们的请求promises而不是lock:

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

That's it! 而已! Now if a request for author is inflight the same promise will be returned. 现在,如果对作者的请求是飞行,则将返回相同的承诺。

Here is how I would implement it. 以下是我将如何实现它。 I think some important requirements are: 我认为一些重要的要求是:

  • No duplicate authors are ever created (this should be a constraint in the database itself too). 没有创建重复的作者(这也应该是数据库本身的约束)。
  • If the server does not reply in the middle - no inconsistent data is inserted. 如果服务器没有在中间回复 - 则不会插入不一致的数据。
  • Possibility to enter multiple authors. 可以进入多位作者。
  • Don't make n queries to the database for n things - avoiding the classic "n+1" problem. 不要让n查询到数据库n东西-避免了经典的“N + 1”的问题。

I'd use a transaction, to make sure that updates are atomic - that is if the operation is run and the client dies in the middle - no authors are created without books. 我使用一个事务,以确保更新是原子的 - 即如果操作运行并且客户端在中间死亡 - 没有作者是在没有书籍的情况下创建的。 It's also important that a temportary failure does not cause a memory leak (like in the answer with the authors map that keeps failed promises). 同样重要的是,临时故障不会导致内存泄漏(例如在作者映射的答案中保持失败的承诺)。

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!"));

This has the advantage of only making a fixed number of queries and is likely to be safer and perform better. 这样做的好处是只能进行固定数量的查询,并且可能更安全,性能更好。 Moreover, your database is not in a consistent state (although you may have to retry the operation for multiple instances). 此外,您的数据库不处于一致状态(尽管您可能必须重试多个实例的操作)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM