如果两个或更多的人同时更新一条记录会发生什么?

本文关键字:记录 一条 更新 什么 两个 如果 | 更新日期: 2023-09-27 18:13:57

我使用NHibernate的版本属性,每次我的聚合根被更新时自动递增。如果两个或更多的人同时更新同一条记录会发生什么?

我该如何测试呢?

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

如果两个或更多的人同时更新一条记录会发生什么?

什么是原子的,什么不是

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

问题说明如果我们不防范这种并发更新的情况,可能会发生奇怪的事情——看起来不可能出现的偷偷摸摸的bug。假设我们有一个类来模拟水的状态变化:
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冻结了水。难道不应该抛出一个异常,说"你不能煮沸固体"吗?用户B在用户A保存他的更改之前获取了数据,所以对两个用户来说,水最初看起来是液体,所以两个用户都可以保存他们冲突的状态更改。

解决方案

为了防止这种情况,我们可以在类中添加一个Version属性,并在NHibernate中将其映射为<version />映射:

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应用程序的特别注意事项

数据的初始select和随后的update之间的时间越长,出现这种并发性问题的可能性就越大。考虑一个典型的"编辑"。实体的现有数据从数据库中选择,放入HTML表单中,然后发送给浏览器。在将表单发送回服务器之前,用户可能要花几分钟修改表单中的值。很有可能其他人在同一时间编辑了相同的信息,并且他们在我们之前保存了他们的更改。

确保版本在我们实际保存更改的几毫秒内不会更改,在这种情况下可能还不够。为了解决这个问题,您可以将版本号作为隐藏字段与其他表单字段一起发送到浏览器,然后在保存之前从数据库中取出实体时检查以确保版本没有更改。此外,还可以通过提供单独的"视图"来限制初始select和最终update之间的时间间隔。和";edit"视图,而不是仅仅使用"edit"查看所有内容。用户花在"编辑"上的时间越少。这样,他们就不太可能看到恼人的错误消息,说他们的更改无法保存。

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

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

取决于在与SQL Server一起使用事务(如果使用)时如何设置隔离级别。(尽管从技术上讲,"完全同时"录制是不可能的)

有关这方面的一些基本信息,请参阅并发系列:事务隔离级别基础

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

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


< 1日更新/em>

修改id = 1, rowversion= 1, description = 'test', rowversion=2

结果

:

MyTable  
Id | RowVersion | Description  
1  | 2          | test

2日更新

没有更新。