如果2个或更多人在同一时间更新记录会怎样?

时间:2021-11-15 20:05:50

I'm using NHibernate with the version property that automatically increments every time my aggregate root is updated. What happens if 2 or more people update the same record at the exact same time?

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

Also, how would I test this?

另外,我该怎么测试呢?

Note that this isn't a situation I've been in, just wondering.

请注意,这不是我一直在进行的情况,只是想知道。

5 个解决方案

#1


5  

What's Atomic, and What's Not

As the others have stated, updates in SQL Server are atomic operations. However, when updating data with NHibernate (or any O/RM), you typically first select the data, make your changes to the object, then update the database with your changes. That sequence of events is not atomic. Even if the select and update were performed within milliseconds of each other, the chance exists for another update to slip in the middle. If two clients fetched the same version of the same data, they could unwittingly overwrite each-other's changes if they assumed that they were the only ones editing that data at that time.

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

Problem Illustration

If we didn't guard against this concurrent-update scenario, weird things could happen - sneaky bugs that shouldn't seem possible. Suppose we had a class that modeled the state changes of water:

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

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;
    }
}

Let's say the following body of water is recorded in the database:

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

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

Two users fetch this record from the database at roughly the same time, modify it, and save the changes back to the database. User A freezes the water:

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

User B tries to boil the water (now ice!)...

用户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();
}

... and is successful!!! What? User A froze the water. Shouldn't an exception have been thrown saying "You cannot boil a Solid!"? User B fetched the data before User A had saved his changes, so to both users, the water appeared to initially be a liquid, so both users were allowed to save their conflicting state changes.

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

Solution

To prevent this scenario, we can add a Version property to the class and map it in NHibernate with a <version /> mapping:

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

public virtual int Version { get; set; }

This is simply a number that NHibernate will increment every time it updates the record, and it will check to make sure no-one else has incremented the version while we weren't watching. Instead of a concurrency-naive sql update like...

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

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

... NHibernate will now use a smarter query like this:

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

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

If the number of rows affected by the query is 0, then NHibernate knows something went wrong - either someone else updated the row so that the version number is now incorrect, or someone deleted the row so that that Id no longer exists. NHibernate will then throw a StaleObjectStateException.

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

Special Note about Web Apps

The more time there is between the initial select of the data and the subsequent update, the greater the chance for this type of concurrency problem. Consider a typical "edit" form in a web app. The existing data for an entity is selected from the database, placed into the HTML form, and sent to the browser. The user may spend several minutes modifying the values in the form before sending it back to the server. There may be a decent chance that someone else was editing the same information at the same time, and they saved their changes before we did.

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

Making sure the version doesn't change during the few milliseconds we're actually saving the changes might not be enough in a scenario like this. To address this problem, you could send the version number to the browser as a hidden field along with the rest of the form fields, then check to make sure the version hasn't changed when you fetch the entity back out of the database before saving. In addition, you can limit the amount of time in-between the initial select and the final update by providing separate "view" and "edit" views instead of just using an "edit" view for everything. The less time the user spends on an "edit" view, the less chance that they'll be presented with an annoying error message saying that their changes could not be saved.

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

#2


4  

Simply put: They can't. The updates are processed in sequence. Each update is - or at least should be - atomic. Thus, the property is incremented twice.

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

#3


4  

Before you can update a row, you have to own the lock for that row. SQL Server locks rows in an atomic fashion. That is, only one of the competing processes can get the lock. All other potential claimants have to wait for the lock to be released.

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

#4


4  

Depends on how the Isolation levels have been setup when transactions(if used) are used with the SQL Server. (Although it is technically impossible for a "exact same time" record editing)

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

Some basic information on this is available at Concurrency Series: Basics of Transaction Isolation Levels

有关此内容的一些基本信息可在并发系列:事务隔离级别基础知识中找到

#5


1  

As Mike Adler said the updates are processed in sequence. but one will fail, I think it will do this by throwing a stale object exception because the version if part of the filter to update the row.

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

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

SQL:
1st update
Update MyTable set description = 'test', rowversion=2 where id = 1 and rowversion = 1

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

Result:

MyTable  
Id | RowVersion | Description  
1  | 2          | test

2nd update
Update MyTable set description = 'second update', rowversion=2 where id = 1 and rowversion = 1

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

nothing updated.

#1


5  

What's Atomic, and What's Not

As the others have stated, updates in SQL Server are atomic operations. However, when updating data with NHibernate (or any O/RM), you typically first select the data, make your changes to the object, then update the database with your changes. That sequence of events is not atomic. Even if the select and update were performed within milliseconds of each other, the chance exists for another update to slip in the middle. If two clients fetched the same version of the same data, they could unwittingly overwrite each-other's changes if they assumed that they were the only ones editing that data at that time.

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

Problem Illustration

If we didn't guard against this concurrent-update scenario, weird things could happen - sneaky bugs that shouldn't seem possible. Suppose we had a class that modeled the state changes of water:

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

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;
    }
}

Let's say the following body of water is recorded in the database:

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

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

Two users fetch this record from the database at roughly the same time, modify it, and save the changes back to the database. User A freezes the water:

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

User B tries to boil the water (now ice!)...

用户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();
}

... and is successful!!! What? User A froze the water. Shouldn't an exception have been thrown saying "You cannot boil a Solid!"? User B fetched the data before User A had saved his changes, so to both users, the water appeared to initially be a liquid, so both users were allowed to save their conflicting state changes.

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

Solution

To prevent this scenario, we can add a Version property to the class and map it in NHibernate with a <version /> mapping:

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

public virtual int Version { get; set; }

This is simply a number that NHibernate will increment every time it updates the record, and it will check to make sure no-one else has incremented the version while we weren't watching. Instead of a concurrency-naive sql update like...

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

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

... NHibernate will now use a smarter query like this:

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

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

If the number of rows affected by the query is 0, then NHibernate knows something went wrong - either someone else updated the row so that the version number is now incorrect, or someone deleted the row so that that Id no longer exists. NHibernate will then throw a StaleObjectStateException.

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

Special Note about Web Apps

The more time there is between the initial select of the data and the subsequent update, the greater the chance for this type of concurrency problem. Consider a typical "edit" form in a web app. The existing data for an entity is selected from the database, placed into the HTML form, and sent to the browser. The user may spend several minutes modifying the values in the form before sending it back to the server. There may be a decent chance that someone else was editing the same information at the same time, and they saved their changes before we did.

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

Making sure the version doesn't change during the few milliseconds we're actually saving the changes might not be enough in a scenario like this. To address this problem, you could send the version number to the browser as a hidden field along with the rest of the form fields, then check to make sure the version hasn't changed when you fetch the entity back out of the database before saving. In addition, you can limit the amount of time in-between the initial select and the final update by providing separate "view" and "edit" views instead of just using an "edit" view for everything. The less time the user spends on an "edit" view, the less chance that they'll be presented with an annoying error message saying that their changes could not be saved.

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

#2


4  

Simply put: They can't. The updates are processed in sequence. Each update is - or at least should be - atomic. Thus, the property is incremented twice.

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

#3


4  

Before you can update a row, you have to own the lock for that row. SQL Server locks rows in an atomic fashion. That is, only one of the competing processes can get the lock. All other potential claimants have to wait for the lock to be released.

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

#4


4  

Depends on how the Isolation levels have been setup when transactions(if used) are used with the SQL Server. (Although it is technically impossible for a "exact same time" record editing)

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

Some basic information on this is available at Concurrency Series: Basics of Transaction Isolation Levels

有关此内容的一些基本信息可在并发系列:事务隔离级别基础知识中找到

#5


1  

As Mike Adler said the updates are processed in sequence. but one will fail, I think it will do this by throwing a stale object exception because the version if part of the filter to update the row.

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

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

SQL:
1st update
Update MyTable set description = 'test', rowversion=2 where id = 1 and rowversion = 1

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

Result:

MyTable  
Id | RowVersion | Description  
1  | 2          | test

2nd update
Update MyTable set description = 'second update', rowversion=2 where id = 1 and rowversion = 1

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

nothing updated.