如果两个或更多的人同时更新一条记录会发生什么?
本文关键字:记录 一条 更新 什么 两个 如果 | 更新日期: 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>
:
MyTable
Id | RowVersion | Description
1 | 2 | test
2日更新
没有更新。