在.NET Core 中处理并发冲突方法

任何应用程序都允许并发数据修改,其中多个用户可以同时与相同的数据进行交互,从而导致潜在的竞争条件。例如,考虑用户可以同时点赞社交媒体帖子的场景,这可能会导致点赞计数的更新发生冲突。如果没有适当的冲突解决机制,查询和更新相同数据的竞争线程可能会导致损坏,上次更新会覆盖以前的更新。本文探讨了在 ASP.NET Core 应用程序中此类方案中维护数据完整性的策略。具体来说,我们讨论了两种主要方法:悲观锁定和乐观锁定。

悲观锁定

如果应用程序需要在并发方案中保持数据一致性,则一种方法是使用数据库锁。这称为悲观锁定(或悲观并发)。此方法背后的基本思想是在修改之前锁定数据。当数据被锁定以进行更新时,其他并发进程无法获取它;并发进程必须等到锁再次可用。

让我们重新审视一下我们的社交媒体帖子示例,其中点赞数是用户点赞帖子时更新的计算字段。我们将使用 SQL Server sp_getapplock 存储过程来防止并发冲突。

我们定义 LikePostAsync 方法如下:

// PostService.cs - https://github.com/ChrisClaude/Handle.Concurrency/blob/main/Application/PostService.cs
public async Task<Result> LikePostAsync(Guid postId)
{
  Result result;
  try
  {
      await _repository.BeginTransactionAsync();
      var resourceName = $"{nameof(Post)}_{postId}";
      await _repository.GetLock(resourceName, nameof(LikePostAsync));
      await _repository.LikePostAsync(postId);
      await _repository.CommitAsync();
      result = new(true, "Successfully liked post");
  
      return result;
  }
  catch (ConcurrentConflictException ex)
  {
      await _repository.RollbackAsync();
      result = new(false, ex.Message);
  
      return result;
  }
}

在提供的代码片段中,我们启动数据库事务并在类似操作开始时获取锁。锁将应用于资源。我们使用 Post 类名称和 postId 的组合来为每个帖子创建一个唯一的资源名称_。_这样可以确保只有尝试获取同一帖子的锁的请求才会在处理时遇到延迟。此外,我们将锁用作数据库事务的一部分;因此,当事务通过提交或回滚完成时,锁将被释放。如有必要,可以使用sp_releaseapplock存储过程显式释放锁。

下面是使用 SQL Server sp_getapplock获取资源锁的 GetLock 方法。

// PostRepository.cs - https://github.com/ChrisClaude/Handle.Concurrency/blob/main/Infrastructure/PostRepository.cs
public async Task GetLock(string resourceName, string action)
{
  if (_transaction == null)
  {
   throw new Exception("The transaction is not initialized");
  }
  using var command = _transaction.GetDbTransaction().Connection.CreateCommand();
  command.CommandText = "sp_getapplock";
  command.CommandType = CommandType.StoredProcedure;
  command.Parameters.Add(new SqlParameter("Resource", resourceName));
  command.Parameters.Add(new SqlParameter("LockMode", "Exclusive"));
  // The command will wait for a maximum of 10 seconds to get the lock
  command.Parameters.Add(new SqlParameter("LockTimeout", "10000"));
  var returnParameter = new SqlParameter("Result", SqlDbType.Int)
  {
   Direction = ParameterDirection.ReturnValue
  };
  command.Parameters.Add(returnParameter);
  // The command will timeout after 15 seconds
  command.CommandTimeout = 15;
  command.Transaction = _transaction.GetDbTransaction();
  await command.ExecuteNonQueryAsync();
  var result = (int)returnParameter.Value;
  if (result < 0)
  {
   throw new ConcurrentConflictException($"A concurrent {action} occurred for resource {resourceName}");
  }
}

上面的代码指定了 LockTimeout 的值,这意味着如果无法立即授予锁定请求,并发请求最多必须等待 10 秒。如果在此时间范围内授予锁定,则表示上一个请求已完成其过程。相反,在无法获取锁时,sp_getapplock存储过程将返回负结果值;我们使用此结果引发自定义异常 ConcurrentConflictException,指示发生了并发冲突。

管理锁既有好处也有坏处。虽然它有助于保持数据完整性,但随着用户群的增长,它可能会导致性能下降。此外,值得注意的是,并非所有数据库管理系统都支持悲观并发。此外,Entity Framework Core 不为其提供内置支持。

乐观锁定

乐观锁定是防止并发冲突的另一种方法。与悲观锁定不同,此方法不对数据采用锁定。它允许数据更新操作进行处理,直到需要保存为止;如果数据自查询以来已更改,则保存操作将失败。应用程序通常需要处理故障,并在最新版本的数据上重试更新。

若要在应用程序中配置乐观锁定,需要将属性定义为并发令牌。查询实体时,将检索并跟踪并发令牌以及其他属性。修改实体后,调用 SaveChanges() 将导致将之前查询的并发令牌值与数据库上的存储值进行比较。

SQL Server 具有一项特殊功能,其中包括更新名为 rowversion 的特殊列**。** 顾名思义,此列的值用于对表中的各个行进行版本控制。每当更新行时,其值都会更改。回到我们的社交媒体帖子方案,我们将配置帖子实体,以包含到 SQL Server rowversion 列的字段映射。

// Post.cs
public class Post 
{
  public Guid Id { get; set; }
  public string Content { get; set; }
  public int LikesCount { get; set; }

  [Timestamp]
  public byte[] Version {get; set; }
}

[Timestamp] 装饰器将 Version 属性映射到 SQL Server rowversion 列。实现此目的后,更新和删除操作将生成一个 SQL 命令,该命令在其 WHERE 子句中包含 Version 字段。

var post = await _context.Posts.FirstAsync(p => p.Id == postId);
post.LikesCount += 1;
await _context.SaveChangesAsync();

在提供的代码片段中,我们查询一个 post 实体,将它的所有属性与并发令牌一起加载。然后,我们递增 LikesCount 属性。在上下文中调用 SaveChangesAsync 后,EF Core 会在数据库上执行以下 SQL 命令:

UPDATE [Post] SET [LikesCount] = @p0
WHERE [Id] = @p1 AND [Version] = @p2;

在 WHERE 子句中,我们不仅比较了帖子的 Id,还比较了它_的 Version_ 属性。这可确保仅更新最初查询的行的确切版本。如果在查询时间和更新执行之间修改了该行,则 WHERE 子句将不匹配任何行,从而触发 DbUpdateConcurrencyException。应用程序必须适当地处理此异常。此外,EF Core 在发生并发删除操作时引发 DbUpdateConcurrencyException。

如上所述,rowversion 类型是内置的 SQL Server 功能;配置并发令牌可能因数据库而异。如果您的数据库缺少此功能,或者您希望更精细地控制哪些列更改会触发令牌重新生成,则可以实现应用程序管理的并发令牌。

值得一提的是,乐观锁定仅在更新或删除行时有效;对于数据创建,在添加具有相同键的行时,必须依靠数据库来引发唯一的冲突约束。虽然悲观锁定也可用于处理并发数据创建场景;只要并发创建操作尝试获取同一资源的锁,它们就会被阻止同时执行。

本文回顾了在 ASP.NET Core 应用程序中处理并发冲突的方法。我们演示了如何实现悲观锁定,在修改之前锁定数据以防止对同一数据进行并发操作,以及乐观锁定,如果数据在查询后发生更改,则将数据修改安排为在保存时失败。这些方法之间的选择取决于应用程序的要求,每种方法都提供权衡。

在某些情况下,并发编程的成本可能超过收益。例如,在运行幂等更改(始终具有相同结果的更改)时,或者当用户很少或更新很少时。在这种情况下,可能没有必要实施处理并发冲突的机制。

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值