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

问题描述

我有一个带有 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. 请求 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,需要走完整的交易路线。