什么是原子的,什么不是
正如其他人所说,SQL Server 中的更新是原子操作。然而,当使用 NHibernate(或任何 O/RM)更新数据时,您通常首先select
数据,对对象进行更改,然后update
数据库包含您的更改。该事件序列不是原子的。即使选择和更新是在几毫秒内执行的,另一个更新也有可能在中间发生。如果两个客户端获取相同数据的相同版本,并且假设他们是当时唯一编辑该数据的客户端,则他们可能会无意中覆盖彼此的更改。
问题说明
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;
}
}
假设数据库中记录了以下水体:
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获取数据before用户 A 已保存他的更改,因此对于两个用户来说,水最初似乎都是液体,因此两个用户都可以保存其冲突的状态更改。
Solution
为了防止这种情况,我们可以添加一个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
,出现此类并发问题的机会就越大。考虑网络应用程序中典型的“编辑”表单。实体的现有数据从数据库中选择,放入 HTML 表单中,然后发送到浏览器。用户可能会花几分钟修改表单中的值,然后再将其发送回服务器。很有可能其他人正在同时编辑相同的信息,并且他们在我们之前保存了更改。
在这种情况下,确保版本在我们实际保存更改的几毫秒内不会更改可能还不够。要解决此问题,您可以将版本号作为隐藏字段与其余表单字段一起发送到浏览器,然后在保存之前从数据库中取回实体时检查以确保版本没有更改。此外,您可以限制初始之间的时间量select
和决赛update
通过提供单独的“查看”和“编辑”视图,而不是仅对所有内容使用“编辑”视图。用户花在“编辑”视图上的时间越少,他们看到无法保存更改的恼人错误消息的可能性就越小。