我正在使用NHibernate和version属性,每次更新聚合根时都会自动递增。如果2个或更多人在同一时间更新同一记录会怎样?
另外,我该怎么测试呢?
请注意,这不是我所处的情况,只是想知道。
答案 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
。
数据的初始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
没有更新。