问题描述
我有一个带有 upload
端点的 C# REST API,其唯一目的是处理二进制文件并将其元数据(作为 Attachment
模型)添加到 List<Attachment>
属性不同的实体。
当我以如下顺序方式(伪代码)从我的 Web 应用程序调用端点时,端点按预期执行并处理每个二进制文件并向提供的实体添加一个 Attachment
。
const attachments = [Attachment,Attachment,Attachment];
for(const attachment of attachments) {
await this.api.upload(attachment);
}
但是当我尝试以如下并行方式(伪代码)上传附件时,每个二进制文件都会得到正确处理,但只有一个 Attachment
元数据对象被添加到实体中。
const attachments = [Attachment,Attachment];
const requests = attachments.map((a) => this.api.upload(a));
await Promise.all(requests);
端点基本上执行以下操作(简化):
var attachment = new Attachment()
{
// Metadata is collected from the binary (FormFile)
};
using (var session = Store.OpenAsyncSession())
{
var entity = await session.LoadAsync<Entity>(entityId);
entity.Attachments.Add(attachment);
await session.StoreAsync(entity);
await session.SaveChangesAsync();
};
我怀疑问题是端点被同时调用。两者都请求打开(同时)一个数据库会话并将实体查询到内存中。他们每个人都将 Attachment
添加到实体并在数据库中更新它。您在数据库中看到的已保存附件来自最后完成的请求,例如耗时最长的请求。
我已尝试通过创建 this example 来重现该问题。当您打开链接时,示例立即运行。您可以在 this database server 上看到创建的实体。
打开 Hogwarts
数据库,然后打开联系人 Harry Potter
,您会看到添加了两个附件。当您打开联系人 Hermione Granger
时,您只会看到添加的一个附件(Second.txt
),尽管它也应该同时包含两个附件。
解决此问题的最佳方法是什么?我更喜欢不必将文件批量发送到端点。感谢您的帮助!
PS:您可能需要通过点击 Run
手动运行示例。如果服务器上不存在数据库(因为服务器会自动清空),您可以使用 Hogwarts
名称手动创建它。并且因为它看起来像一个竞争条件,有时两个 Attachment
项都被正确添加。因此,您可能需要多次运行该示例。
解决方法
这是写入数据库的竞争条件的一个相当经典的例子,你是对的。
事件顺序是:
- 请求 1 加载文档
Attachments = []
- 请求 1 加载文档
Attachments = []
- 请求 1
Attachments.Push()
- 要求 2
Attachments.Push()
- 请求 1
SaveChanges()
- 要求 2
SaveChanges()
5
中的更改覆盖了 4
中的更改,因此您正在丢失数据。
有两种方法可以处理这种情况。您可以为此特定场景启用乐观并发,请参阅有关该主题的文档:
基本上,如果文档在幕后更新,您可以执行 session.Advanced.UseOptimisticConcurrency = true;
导致事务失败。
然后您可以重试事务以使其工作(确保创建新会话)。
或者,您可以使用修补 API,这将允许您同时安全地将项目添加到文档中。 以下是相关文档:
请注意,这里有一个注意事项,您不应该关心操作的顺序是什么(因为它们可以以任何顺序发生)。 如果订单背后有业务用例,您可能无法轻松使用补丁 API,需要走完整的交易路线。