如果2个或更多人在同一时间更新记录会发生什么?

时间:2011-11-08 23:59:58

标签: c# sql-server nhibernate concurrency

我正在使用NHibernate和version属性,每次更新聚合根时都会自动递增。如果2个或更多人在同一时间更新同一记录会怎样?

另外,我该怎么测试呢?

请注意,这不是我所处的情况,只是想知道。

5 个答案:

答案 0 :(得分:5)

什么是原子,什么不是

正如其他人所说,SQL Server中的更新是原子操作。但是,在使用NHibernate(或任何O / RM)更新数据时,通常首先select数据,对对象进行更改,然后根据您的更改update数据库。那一系列事件是原子。即使选择和更新在彼此之间的毫秒内执行,也存在另一次更新在中间滑动的可能性。如果两个客户端获取相同数据的相同版本,如果他们认为他们是当时唯一编辑该数据的人,他们可能会无意中覆盖彼此的其他更改。

问题插图

如果我们没有防范这种并发更新的情况,可能会发生奇怪的事情 - 看起来不可能的偷偷摸摸的错误。假设我们有一个模拟水状态变化的课程:

public class BodyOfWater
{
    public virtual int Id { get; set; }
    public virtual StateOfMatter State { get; set; }

    public virtual void Freeze()
    {
        if (State != StateOfMatter.Liquid)
            throw new InvalidOperationException("You cannot freeze a " + State + "!");
        State = StateOfMatter.Solid;
    }

    public virtual void Boil()
    {
        if (State != StateOfMatter.Liquid)
            throw new InvalidOperationException("You cannot boil a " + State + "!");
        State = StateOfMatter.Gas;
    }
}

让我们说下面的水体被记录在数据库中:

new BodyOfWater
{
    Id = 1,
    State = StateOfMatter.Liquid
};

两个用户几乎同时从数据库中获取此记录,修改它并将更改保存回数据库。用户A冻结水:

using (var transaction = sessionA.BeginTransaction())
{
    var water = sessionA.Get<BodyOfWater>(1);
    water.Freeze();
    sessionA.Update(water);

    // Same point in time as the line indicated below...

    transaction.Commit();
}

用户B试图将水煮沸(现在冰!)......

using (var transaction = sessionB.BeginTransaction())
{
    var water = sessionB.Get<BodyOfWater>(1);

    // ... Same point in time as the line indicated above.

    water.Boil();
    sessionB.Update(water);
    transaction.Commit();
}

......并且成功了!!!什么?用户A冻结了水。不应该抛出异常说#34;你不能煮一个固体!&#34;?用户B在用户A保存更改之前获取了数据,因此对于两个用户来说,水似乎最初是流动的,因此允许两个用户保存他们的冲突状态更改。

解决方案

为防止出现这种情况,我们可以在类中添加Version属性,并使用<version />映射将其映射到NHibernate中:

public virtual int Version { get; set; }

这只是一个NHibernate每次更新记录时都会增加的数字,它会检查以确保在我们没有观看时没有其他人增加版本。而不是像...那样的并发天真的sql更新。

update BodyOfWater set State = 'Gas' where Id = 1;

... NHibernate现在将使用更智能的查询:

update BodyOfWater set State = 'Gas', Version = 2 where Id = 1 and Version = 1;

如果受查询影响的行数为0,则NHibernate知道出现了问题 - 其他人更新了行以使版本号现在不正确,或者有人删除了行以使该ID不再存在。然后NHibernate将抛出StaleObjectStateException

关于Web Apps的特别说明

数据的初始select与后续update之间的时间越长,此类并发问题的可能性就越大。考虑一个典型的&#34;编辑&#34;在网络应用程序中的表单。从数据库中选择实体的现有数据,放入HTML表单并发送到浏览器。在将表单中的值发送回服务器之前,用户可能需要花费几分钟时间修改表单中的值。可能有其他人同时编辑相同信息的机会,他们在我们之前保存了他们的更改。

在这样的情况下,确保版本在几毫秒内没有变化我们实际保存更改可能还不够。要解决此问题,您可以将版本号作为隐藏字段与其余表单字段一起发送到浏览器,然后检查以确保在从数据库中取回实体时版本没有更改在保存之前。此外,您可以通过提供单独的&#34;视图&#34;来限制初始select和最终update之间的时间量。和&#34;编辑&#34;视图而不仅仅是使用&#34;编辑&#34;查看一切。用户花在&#34;编辑&#34;上的时间越少看来,他们遇到一条令人讨厌的错误消息的可能性越小,表明他们的更改无法保存。

答案 1 :(得分:4)

简单地说:他们不能。更新按顺序处理。每次更新都是 - 或者至少应该是 - 原子的。因此,该属性增加了两倍。

答案 2 :(得分:4)

在更新行之前,您必须拥有该行的锁定。 SQL Server以原子方式锁定行。也就是说,只有一个竞争过程可以获得锁定。所有其他潜在的索赔人都必须等待释放锁定。

答案 3 :(得分:4)

取决于在SQL Server使用事务(如果使用)时如何设置隔离级别。 (虽然技术上不可能进行“完全相同的时间”记录编辑)

有关此问题的一些基本信息,请访问Concurrency Series: Basics of Transaction Isolation Levels

答案 4 :(得分:1)

正如Mike Adler所说,更新是按顺序处理的。但是一个会失败,我认为它会通过抛出陈旧的对象异常来做到这一点,因为版本是过滤器的一部分来更新行。

MyTable  
Id | RowVersion | Description  
1  | 1          | this description

<强> SQL:
第一次更新
更新MyTable set description ='test',rowversion = 2,其中id = 1,rowversion = 1

结果

MyTable  
Id | RowVersion | Description  
1  | 2          | test

第二次更新
更新MyTable set description ='second update',rowversion = 2,其中id = 1,rowversion = 1

没有更新。