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

gcs*_*cso 5 c# sql-server nhibernate concurrency

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

另外,我该怎么测试呢?

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

Dan*_*ing 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

假设数据库中记录了以下水体:

new BodyOfWater
{
    Id = 1,
    State = StateOfMatter.Liquid
};
Run Code Online (Sandbox Code Playgroud)

两个用户几乎同时从数据库中获取此记录,对其进行修改,然后将更改保存回数据库.用户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();
}
Run Code Online (Sandbox Code Playgroud)

用户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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

public virtual int Version { get; set; }
Run Code Online (Sandbox Code Playgroud)

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

update BodyOfWater set State = 'Gas' where Id = 1;
Run Code Online (Sandbox Code Playgroud)

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

update BodyOfWater set State = 'Gas', Version = 2 where Id = 1 and Version = 1;
Run Code Online (Sandbox Code Playgroud)

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

关于Web Apps的特别说明

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

在这样的场景中,确保版本在几毫秒内没有改变我们实际上保存更改可能是不够的.要解决此问题,您可以将版本号作为隐藏字段与其余表单字段一起发送到浏览器,然后检查以确保在保存之前从数据库中取回实体时版本未更改.此外,您可以通过提供单独的"视图"和"编辑"视图来限制初始select和最终之间的时间量,update而不是仅使用"编辑"视图.用户花在"编辑"视图上的时间越少,他们就会越少有机会收到一条恼人的错误消息,说明他们的更改无法保存.