在 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,因为此数据已标准化。作者可能存在也可能不存在,所以我需要:
- 检查作者是否在数据库中
- 如果是,使用这个ID
- 否则,插入作者并使用新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!"));
这样做的好处是只进行固定数量的查询,而且可能更安全,性能更好。此外,您的数据库未处于一致状态(尽管您可能需要为多个实例重试该操作)。
我正在努力解决一些不那么琐碎的 promise/asynchronous 用例。在我目前正在努力的一个例子中,我有一个从 knex 查询返回的书籍数组(thenable 数组)我希望插入到数据库中:
books.map(function(book) {
// Insert into DB
});
每本书看起来像:
var book = {
title: 'Book title',
author: 'Author name'
};
但是,在插入每本书之前,我需要从单独的 table 中检索作者的 ID,因为此数据已标准化。作者可能存在也可能不存在,所以我需要:
- 检查作者是否在数据库中
- 如果是,使用这个ID
- 否则,插入作者并使用新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!"));
这样做的好处是只进行固定数量的查询,而且可能更安全,性能更好。此外,您的数据库未处于一致状态(尽管您可能需要为多个实例重试该操作)。