当某用户显示实体数据以对其进行编辑,而另一用户在上一用户的更改写入数据库之前更新同一实体的数据时,会发生并发冲突。 如果不启用此类冲突的检测,则最后更新数据库的人员将覆盖其他用户的更改。 在许多应用程序中,此风险是可接受的:如果用户很少或更新很少,或者一些更改被覆盖并不重要,则并发编程可能弊大于利。 在此情况下,不必配置应用程序来处理并发冲突。
悲观并发
如果应用程序确实需要防止并发情况下出现意外数据丢失,一种方法是使用数据库锁定。 这称为悲观并发。 例如,在从数据库读取一行内容之前,请求锁定为只读或更新访问。 如果将一行锁定为更新访问,则其他用户无法将该行锁定为只读或更新访问,因为他们得到的是正在更改的数据的副本。 如果将一行锁定为只读访问,则其他人也可将其锁定为只读访问,但不能进行更新。
管理锁定有缺点。 编程可能很复杂。 它需要大量的数据库管理资源,且随着应用程序用户数量的增加,可能会导致性能问题。 由于这些原因,并不是所有的数据库管理系统都支持悲观并发。 Entity Framework Core 未提供对它的内置支持。
检测并发冲突之跟踪列
数据库表中包含一个可用于确定某行更改时间的跟踪列。 然后可配置 Entity Framework,将该列包含在 SQL Update 或 Delete 命令的 Where 子句中。
跟踪列的数据类型通常是 rowversion。 rowversion 值是一个序列号,该编号随着每次行的更新递增。 在 Update 或 Delete 命令中,Where 子句包含跟踪列的原始值(原始行版本)。 如果正在更新的行已被其他用户更改,则 rowversion 列中的值与原始值不同,这导致 Update 或 Delete 语句由于 Where 子句而找不到要更新的行。 当 Entity Framework 发现 Update 或 Delete 命令没有更新行(即受影响的行数为零)时,便将其解释为并发冲突。
检测并发冲突之匹配原始值
配置 Entity Framework,在 Update 或 Delete 命令的 Where 子句中包含表中每个列的原始值。
与第一个选项一样,如果行自第一次读取后发生更改,Where 子句将不返回要更新的行,Entity Framework 会将其解释为并发冲突。 对于包含许多列的数据库表,此方法可能导致非常多的 Where 子句,并且可能需要维持大量的状态。 如前所述,维持大量的状态会影响应用程序的性能。 因此通常不建议使用此方法。
跟踪列解决并发冲突
添加跟踪属性:
[Timestamp] public byte[] RowVersion { get; set; }
使用 Fluent API的 IsConcurrencyToken 方法:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Student>().Property(p => p.RowVersion).IsConcurrencyToken(); }
更新返回编辑页面方法:
public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } //var student = await _context.Students.FindAsync(id); var student = await _context.Students.AsNoTracking().FirstOrDefaultAsync(m => m.ID == id); if (student == null) { return NotFound(); } return View(student); }
更新编辑方法:
//并发冲突 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(int? id, byte[] rowVersion) { if (id == null) { return NotFound(); } var studentsToUpdate = await _context.Students.FirstOrDefaultAsync(m => m.ID == id); // 在调用 SaveChanges 之前,必须将该原始 RowVersion 属性值置于实体的 OriginalValues 集合中 _context.Entry(studentsToUpdate).Property("RowVersion").OriginalValue = rowVersion; if (await TryUpdateModelAsync<Student>( studentsToUpdate, "", s => s.FirstMidName, s => s.LastName, s => s.Age, s => s.EnrollmentDate)) { try { /* 当 Entity Framework 创建 SQL UPDATE 命令时,该命令将包含一个 WHERE 子句,用于查找具有原始 RowVersion 值的行。 如果没有行受到 UPDATE 命令影响(没有行具有原始 RowVersion 值),则 Entity Framework 会引发 DbUpdateConcurrencyException 异常。 */ await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } catch (DbUpdateConcurrencyException ex) { var exceptionEntry = ex.Entries.Single(); var clientValues = (Student)exceptionEntry.Entity; var databaseEntry = exceptionEntry.GetDatabaseValues(); if (databaseEntry == null) { ModelState.AddModelError(string.Empty, "Unable to save changes. The department was deleted by another user."); } else { //获取数据库中的数据,因为这里处理并发的方式是存储优先 var databaseValues = (Student)databaseEntry.ToObject(); //和数据库的值进行比较,用于给用户进行明确的提示,让用户知道目前数据库中是存储的是什么值 if (databaseValues.FirstMidName != clientValues.FirstMidName) { ModelState.AddModelError("FirstMidName", $"Current value: {databaseValues.FirstMidName}"); } if (databaseValues.LastName != clientValues.LastName) { ModelState.AddModelError("LastName", $"Current value: {databaseValues.LastName:c}"); } if (databaseValues.Age != clientValues.Age) { ModelState.AddModelError("Age", $"Current value: {databaseValues.Age:d}"); } if (databaseValues.EnrollmentDate != clientValues.EnrollmentDate) { ModelState.AddModelError("EnrollmentDate", $"Current value: {databaseValues.EnrollmentDate:d}"); } //提示出现了并发,数据在打开与点击保存过程被其他用户修改过 ModelState.AddModelError(string.Empty, "您尝试编辑的记录在获得原始值后被另一个用户修改。" + "编辑操作已取消,数据库中的当前值已显示。" + "如果仍要编辑此记录,请再次单击“保存”按钮。否则,请单击“返回列表”超链接。 "); //把跟踪属性修改成和数据库一样的,这样第二次更新的时候就可以成功了,当然如果第二次也不想用户更新成功也可以不设置 studentsToUpdate.RowVersion = (byte[])databaseValues.RowVersion; ModelState.Remove("RowVersion"); } } } return View(studentsToUpdate); }
更新编辑视图,添加隐藏字段以保存 RowVersion 属性值,紧跟在 ID属性的隐藏字段后面:
<input type="hidden" asp-for="ID" /> <input type="hidden" asp-for="RowVersion" />
测试并发冲突,同时打开两个页面进行修改:
欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739