RavenDB:如何同时正确更新文档? (两个同时 API 请求到同一个端点)

RavenDB: How to properly update a document at the same time? (two simultaneous API request to the same endpoint)

我有一个带有 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, 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. When you open the link, the example runs right away. You can see the created entities on this database server.

来重现该问题

打开 Hogwarts 数据库,然后打开联系人 Harry Potter,您会看到添加了两个附件。当您打开联系人 Hermione Granger 时,您只会看到添加的一个附件(Second.txt),尽管它应该也有两个附件。

解决此问题的最佳方法是什么?我更喜欢 而不是 必须将文件作为批处理发送到端点。感谢任何帮助!

PS:您可能需要通过单击 Run 手动 运行 示例。如果服务器上不存在数据库(因为服务器自动清空),您可以使用 Hogwarts 名称手动创建它。而且因为它看起来像一个竞争条件,所以有时 Attachment 项都被正确添加。所以你可能需要 运行 这个例子几次。

这是一个相当经典的写入数据库的竞争条件示例,你是对的。

事件的顺序是:

  1. 请求 1 加载文档 Attachments = []
  2. 请求 1 加载文档 Attachments = []
  3. 请求 1 Attachments.Push()
  4. 请求 2 Attachments.Push()
  5. 请求 1 SaveChanges()
  6. 请求 2 SaveChanges()

5 中的更改会覆盖 4 中的更改,因此您正在丢失数据。

有两种方法可以处理这种情况。您可以为此特定场景启用乐观并发,请参阅有关该主题的文档:

https://ravendb.net/docs/article-page/4.2/csharp/client-api/session/configuration/how-to-enable-optimistic-concurrency#enabling-for-a-specific-session

基本上,如果文档在后台更新,您可以session.Advanced.UseOptimisticConcurrency = true;导致交易失败。

然后您可以重试事务以使其正常工作(确保创建一个新会话)。

或者,您可以使用补丁 API,这将允许您安全地同时向文档添加项目。 这是相关文档:

https://ravendb.net/docs/article-page/4.2/csharp/client-api/operations/patching/single-document#add-item-to-array

注意这里有一个注意事项,你不应该关心操作的顺序是什么(因为它们可以以任何顺序发生)。 如果订单背后有业务用例,您可能无法轻松使用补丁 API,需要使用完整的交易路线。