在不更新行版本的情况下检查实体的并发性

Posted

技术标签:

【中文标题】在不更新行版本的情况下检查实体的并发性【英文标题】:Checking Concurrency on an Entity without updating the Row Version 【发布时间】:2019-11-24 16:30:02 【问题描述】:

我有一个父实体,我需要做一个并发检查(如下注释)

[Timestamp]
public byte[] RowVersion  get; set; 

我有一堆客户端进程,它们从这个父实体访问只读值,主要是更新它的子实体

约束

    客户端不应干扰彼此的工作(例如,更新子记录不应在父实体上引发并发异常)。

    我有一个服务器进程,它 更新这个父实体,在这种情况下如果 父实体 已更改,客户端进程需要抛出。

注意:客户端的并发检查是牺牲的,服务器的工作流程是关键任务。

问题

我需要(从客户端进程)检查父实体是否已更改而不更新父实体的行版本

EF中对父实体进行并发检查很容易:

// Update the row version's original value
_db.Entry(dbManifest)
      .Property(b => b.RowVersion)
      .OriginalValue = dbManifest.RowVersion; // the row version the client originally read

// Mark the row version as modified
_db.Entry(dbManifest)
       .Property(x => x.RowVersion)
       .IsModified = true;

IsModified = true交易破坏者,因为它会强制更改行版本。或者,在上下文中说,来自客户端进程的此检查将导致 父实体 中的行版本更改,这会不必要地干扰其他 客户端进程的 工作流。 p>

解决方法:我可能会将来自客户端进程的 SaveChanges 包装在 Transaction 中,然后随后读取父实体的行版本,反过来,如果行版本发生变化,则回滚。

总结

Entity Framework 是否有一种开箱即用的方式,我可以在其中SaveChanges(在客户端进程中为子实体)还检查父实体的行版本是否已更改(不更新父实体的行版本)。

【问题讨论】:

是否可以使用 sqlserver rowversion 功能? docs.microsoft.com/en-us/sql/t-sql/data-types/… @ilkerkaran 是的,我正在使用它,但它更多的是如何检查父表上的并发更改而不更改该表上的rowversion,因此SaveChanges 失败实体框架。 这个问题和你的类似吗? social.msdn.microsoft.com/Forums/en-US/…. 修改父子的方法有很多,还是只有几个?换句话说,您的事务变通办法会导致重复代码吗? 我认为您的交易解决方法还不错。特别是。封装后,优点是它都在一个地方,所以很清楚会发生什么,并且不太可能有任何副作用。任何其他解决方案,例如使用 EF 的命令树拦截器,都将由分离的代码部分组成,从而很容易在频谱的任一侧破坏某些东西。 F.e.如果没有从数据库中提取父 rowid,这一切都会失败,当代码没有显示为什么/在哪里需要它们时,这很容易忘记。此外,也许更重要的是,它将您与这个版本的 EF 联系在一起。 【参考方案1】:

有一个非常简单的解决方案,“out-of-2-boxes”,但它需要两个修改,我不确定您是否可以或愿意进行:

在包含ParentRowVersion 列的子表上创建一个可更新视图 将子实体映射到此视图

让我展示一下它是如何工作的。这一切都很简单。

数据库模型:

CREATE TABLE [dbo].[Parent]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Parent] ADD CONSTRAINT [PK_Parent] PRIMARY KEY CLUSTERED  ([ID]) ON [PRIMARY]

CREATE TABLE [dbo].[Child]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL,
[ParentID] [int] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Child] ADD CONSTRAINT [PK_Child] PRIMARY KEY CLUSTERED  ([ID]) ON [PRIMARY]
GO
CREATE VIEW [dbo].[ChildView]
WITH SCHEMABINDING
AS
SELECT Child.ID
, Child.Name
, Child.ParentID
, Child.RowVersion
, p.RowVersion AS ParentRowVersion
FROM dbo.Child
INNER JOIN dbo.Parent p ON p.ID = Child.ParentID

视图是可更新的,因为它符合conditions for Sql Server views to be updatable。

数据

SET IDENTITY_INSERT [dbo].[Parent] ON
INSERT INTO [dbo].[Parent] ([ID], [Name]) VALUES (1, N'Parent1')
SET IDENTITY_INSERT [dbo].[Parent] OFF

SET IDENTITY_INSERT [dbo].[Child] ON
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (1, N'Child1.1', 1)
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (2, N'Child1.2', 1)
SET IDENTITY_INSERT [dbo].[Child] OFF

类模型

public class Parent

    public Parent()
    
        Children = new HashSet<Child>();
    
    public int ID  get; set; 
    public string Name  get; set; 
    public byte[] RowVersion  get; set; 
    public ICollection<Child> Children  get; set; 


public class Child

    public int ID  get; set; 
    public string Name  get; set; 
    public byte[] RowVersion  get; set; 

    public int ParentID  get; set; 
    public Parent Parent  get; set; 
    public byte[] ParentRowVersion  get; set; 

上下文

public class TestContext : DbContext

    public TestContext(string connectionString) : base(connectionString) 

    public DbSet<Parent> Parents  get; set; 
    public DbSet<Child> Children  get; set; 

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    
        modelBuilder.Entity<Parent>().Property(e => e.RowVersion).IsRowVersion();
        modelBuilder.Entity<Child>().ToTable("ChildView");
        modelBuilder.Entity<Child>().Property(e => e.ParentRowVersion).IsRowVersion();
    

把它放在一起

这段代码更新了Child,而假的并发用户更新了Parent

using (var db = new TestContext(connString))

    var child = db.Children.Find(1);

    // Fake concurrent update of parent.
    db.Database.ExecuteSqlCommand("UPDATE dbo.Parent SET Name = Name + 'x' WHERE ID = 1");
    
    child.Name = child.Name + "y";
    db.SaveChanges();

现在SaveChanges 抛出所需的DbUpdateConcurrencyException。当父更新被注释掉时,子更新成功。

我认为这种方法的优势在于它完全独立于数据访问库。你所需要的只是一个支持乐观并发的 ORM。未来转向 EF-core 不会有问题。

【讨论】:

这实际上是一个非常巧妙的解决方案和创造性的解决方案,也是一种用视图达到预期结果的新颖方法。今天在工作中玩弄它,谢谢! "out-of-2-boxes" - 哈哈,我喜欢这样!如果 EF 允许我们用代码(表达式类型列?)创建这样的“视图”会很好,但它没有,所以这似乎是“两个世界”的最佳组合:) @Ivan tnx!是的,EF 可以做更多的图形/聚合意识,尽管我认为 EF-core 是朝那个方向迈出的“一小步”。我认为您能够将这种表达式类型的专栏想法实现为拉取请求,从而在该领域实现“巨大飞跃”。只需要一点时间... 这本着我所要求的精神最好地回答了这个问题 很好的答案!只是好奇如何在使用 EF Code First 进行模型定义时创建视图。【参考方案2】:

好吧,您需要做的是在写入子实体时检查父实体的并发令牌(时间戳)。唯一的挑战是父时间戳不在子实体中。

您没有明确说明,但我假设您使用的是 EF Core。

查看https://docs.microsoft.com/en-us/ef/core/saving/concurrency,如果 UPDATE 或 DELETE 影响零行,EF Core 似乎会抛出并发异常。为了实现并发测试,EF 添加了一个 WHERE 子句来测试并发标记,然后测试 UPDATE 或 DELETE 是否影响了正确的行数。

您可以尝试在 UPDATE 或 DELETE 中添加一个额外的 WHERE 子句,以测试父级的 RowVersion 的值。我认为您可以使用 System.Diagnostics.DiagnosticListener 类来拦截 EF Core 2 来执行此操作。https://weblogs.asp.net/ricardoperes/interception-in-entity-framework-core 上有一篇文章,Can I configure an interceptor yet in EntityFramework Core? 有一篇讨论。显然 EF Core 3(我认为它会在 9 月/10 月推出)将包含一个类似于 EF pre-Core 中的拦截机制,请参阅https://github.com/aspnet/EntityFrameworkCore/issues/15066

希望这对您有用。

【讨论】:

嗯,首先这是 不是 EF Core,而是 EF6 - 我专门询问了 OP,他们添加了正确的标签,在回答时应该已经存在。其次,关于唯一的挑战——对我来说这是一个相当大的挑战。虽然总体上这个想法似乎是正确的,但我希望看到它以一种通用的方式实现,尤其是使用非常违反直觉的 EF6 元数据模型。 是的,这绝对是实体框架而不是核心,今天我会进一步研究这种方法,看看它是否适用。 我在上面的回答中引用的最后一个链接重申了问题,并命名了与解决核心前问题相关的类。【参考方案3】:

从一个项目到另一个项目,我在广泛的平台(不仅是 .Net)上遇到了这个问题。 从架构的角度来看,我可以提出几个并非 EntityFramework 特有的决策。 (至于我#2更好)

OPTION 1 实现乐观锁定方法。一般来说,想法听起来像:“让我们更新客户端然后检查父级的状态”。您已经提到了“使用事务”的想法,但是乐观锁定可以减少保留 Parent 实体所需的时间。比如:

var expectedVersion = _db.Parent...First().RowVersion;
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required))

    //modify Client entity there
    ...
    //now make second check of Parent version
    if( expectedVersion != _db.Parent...First().RowVersion )
        throw new Exception(...);
    _db.SaveChanges();

注意!根据 SQL 服务器设置(隔离级别),您可能需要应用到父实体 select-for-update,请查看如何操作。 How to implement Select For Update in EF Core

选项 2 至于我更好的方法而不是 EF 来使用显式 SQL,例如:

UPDATE 
    SET Client.BusinessValue = :someValue -- changes of client
    FROM Client, Parent
         WHERE Client.Id = :clientToChanges -- restrict updates by criteria
         AND Client.ParentId = Parent.Id -- join with Parent entity
         AND Parent.RowVersion = :expectedParent

在 .Net 代码中进行此查询后,您需要检查是否有 1 行受到影响(0 表示 Parent.Rowversion 已更改)

if(_db.ExecuteSqlCommand(sql) != 1 )
    throw new Exception();

还可以尝试借助附加的 DB 表来分析“全局锁”设计模式。您可以在 http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

阅读到有关此方法的信息

【讨论】:

以上是关于在不更新行版本的情况下检查实体的并发性的主要内容,如果未能解决你的问题,请参考以下文章

并发扣款一致性,幂等性问题,这个话题还没聊完!!!

java 并发之volatile

事务并发之隔离级别

在 BigTable 数据存储中,关于并发性,我如何“锁定”实体?

云原生技术分享 | MySQL锁与事务的并发性

java并发特性:原子性可见性有序性