【发布时间】:2011-11-08 23:59:58
【问题描述】:
我使用 NHibernate 和 version 属性,每次更新我的聚合根时都会自动递增。如果 2 人或更多人同时更新同一条记录会怎样?
另外,我将如何测试这个?
请注意,这不是我遇到的情况,只是想知道。
【问题讨论】:
标签: c# sql-server nhibernate concurrency
我使用 NHibernate 和 version 属性,每次更新我的聚合根时都会自动递增。如果 2 人或更多人同时更新同一条记录会怎样?
另外,我将如何测试这个?
请注意,这不是我遇到的情况,只是想知道。
【问题讨论】:
标签: c# sql-server nhibernate concurrency
正如其他人所说,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 冻结了水。不应该抛出一个异常说“你不能煮一个固体!”吗?用户 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。
数据的初始select 和随后的update 之间的时间越长,出现此类并发问题的可能性就越大。考虑 Web 应用程序中的典型“编辑”表单。从数据库中选择实体的现有数据,放入 HTML 表单,然后发送到浏览器。用户可能会花几分钟时间修改表单中的值,然后再将其发送回服务器。很有可能其他人正在同时编辑相同的信息,并且他们在我们之前保存了他们的更改。
确保版本在我们实际保存更改的几毫秒内不会更改,在这种情况下可能还不够。为了解决这个问题,您可以将版本号作为隐藏字段与其他表单字段一起发送到浏览器,然后在保存之前从数据库中取回实体时检查以确保版本没有更改.此外,您可以限制初始select 和最终update 之间的时间量,方法是提供单独的“视图”和“编辑”视图,而不仅仅是对所有内容使用“编辑”视图。用户在“编辑”视图上花费的时间越少,他们看到令人讨厌的错误消息的机会就越小,说明他们的更改无法保存。
【讨论】:
简单地说:他们不能。更新按顺序处理。每个更新都是 - 或者至少应该是 - 原子的。因此,该属性增加了两次。
【讨论】:
在更新行之前,您必须拥有该行的锁。 SQL Server 以原子方式锁定行。也就是说,只有一个竞争进程可以获得锁。所有其他潜在的声明者都必须等待锁被释放。
【讨论】:
取决于在 SQL Server 中使用事务(如果使用)时隔离级别的设置方式。 (虽然“精确同时”的记录编辑在技术上是不可能的)
有关这方面的一些基本信息,请访问Concurrency Series: Basics of Transaction Isolation Levels
【讨论】:
正如 Mike Adler 所说,更新是按顺序处理的。但是一个会失败,我认为它会通过抛出一个陈旧的对象异常来做到这一点,因为如果版本是过滤器的一部分来更新行。
MyTable
Id | RowVersion | Description
1 | 1 | this description
SQL:
第一次更新
更新 MyTable set description = 'test', rowversion=2 where id = 1 and rowversion = 1
结果:
MyTable
Id | RowVersion | Description
1 | 2 | test
第二次更新
Update MyTable set description = 'second update', rowversion=2 where id = 1 and rowversion = 1
没有更新。
【讨论】: