为什么需要并发控制?
在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
典型的冲突有:
l 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
l 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。
为了解决这些并发带来的问题。 我们需要引入并发控制机制,Nhibernate中可以使用乐观锁和悲观锁来控制数据冲突。
乐观锁
它如果检测到实体被改变,我们就不能更新它,让我们看一个简单的例子,乐观锁(dirty):
配置:
<class name="…" table="Person" lazy="true" optimistic-lock="dirty" dynamic-update="true">
测试代码:
using (ITransaction tx = session.BeginTransaction())
{
var person = session.Get<Person>(1);
person.Name = "other";
tx.Commit();
}
测试结果:
UPDATE Person SET Name = @p0
WHERE Id = @p1 AND Name = @p2;@p0 = 'other', @p1 = 1, @p2 = 'wang'
如果把optimistic-lock 参数变成All,那么测试结果是:
UPDATE Person SET Name = @p0
WHERE Id = @p1 AND Name = @p2 AND Country = @p3 AND ZipCode = @p4;@p0 = 'other2', @p1 = 1, @p2 = 'other', @p3 = 'China', @p4 = '310000'
这样就能防止冲突了? 是的,请看下面一个抛异常的例子:
[Test]
public void PersonConcurrencyTest()
{
using (ISession session = NHibernateHelper.GetSession())
{
using (ITransaction tx = session.BeginTransaction())
{
var person = session.Get<Person>(1);
person.Name = "test01";
// 模拟另外一个用户修改数据
using (ISession session2 = NHibernateHelper.GetSession())
{
using (ITransaction tx2 = session2.BeginTransaction())
{
var person2 = session2.Get<Person>(1);
person2.Name = "test04";
tx2.Commit();//提交成功
}
}
tx.Commit(); //提交失败,数据冲突
}
}
}
错误信息:
NHibernate: UPDATE Person SET Name = @p0 WHERE Id = @p1 AND Name = @p2 AND Country = @p3 AND ZipCode = @p4;@p0 = 'test01', @p1 = 1, @p2 = 'test02', @p3 = 'China', @p4 = '310000'
Test 'NHibernateSample.Data.Test.NHibernateSampleFixture.PersonConcurrencyTest' failed: NHibernate.StaleObjectStateException : Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [NHibernateSample.Domain.Entities.Person#1]
at NHibernate.Persister.Entity.AbstractEntityPersister.Check(Int32 rows, Object id, Int32 tableNumber, IExpectation expectation, IDbCommand statement)
……….
第二次提交之所以失败是因为那行数据被另外一个事务(用户)修改了,这样我们就得到了StaleObjectException 错误,当前事务提交失败,seesion失效
考虑这样一个场景:用户小王从数据库中取得这条数据后展现在界面上,用户编辑了10分钟还喝了一杯茶,然后保存数据;期间数据被小张修改,小王是否可以检测到数据被修改(数据冲突异常)?
答案是不能
这里有一个更好的策略,使用显式的版本列,即在数据库中增加一列 Version
配置文件作相应修改:
<class name="…" table="Person" lazy="true" optimistic-lock="version" dynamic-update="true">
增加版本控制列:
<version name="Version" column="Version" type="Int32" ></version>
当执行普通更新操作时,执行的sql如下:
UPDATE Person SET Version = @p0, Name = @p1
WHERE Id = @p2 AND Version = @p3;@p0 = 1, @p1 = 'other2', @p2 = 1,@p3=0
我想你也猜到了,如果version 字段值与原来的不一样,就会抛StaleObjectException异常
Version字段是怎么被更新的?
这是Nhibernate的机制,当我们完成配置工作的时候(Person.hbm.xml),一旦person表有数据被更新,version字段会字段更新。
在某些情况下,比如数据库中的job,绕过Nhibernate机制,直接更新这个表的数据,那个version字段的值会被自动更新吗?
不会。
这可怎么办呢,做一下稍微的改动就可以避免。上面控制版本的字段version 是int类型,做如下修改:
<version name="Version" column="Version" type="timestamp"/>
该成timeStamp 类型,这个字段任何时候都会被更新只要有数据被更新。
悲观锁
Nhibernate中的悲观锁使用LockMode来传递。
比如:
using (ITransaction tx = session.BeginTransaction())
{
var person = session.Get<Person>(1,LockMode.Upgrade);
person.Name = "other-Pessimistic2";
tx.Commit();
}
SELECT person0_.Id as Id0_0_, person0_.Name as Name0_0_, person0_.Country
as Country0_0_, person0_.ZipCode as ZipCode0_0_
FROM Person person0_ with (updlock, rowlock) WHERE
person0_.Id=@p0;@p0 = 1
关于sql Lock 的更多信息可以参考微软技术文档(Table Hints):
http://msdn.microsoft.com/en-us/library/ms187373.aspx
各适用于什么样的场景?
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 乐观锁不能解决脏读的问题
例子中用到的数据库结构:
CREATE TABLE [dbo].[Person](
[Id] [int] NOT NULL,
[Name] [nvarchar](100) NULL,
[Country] [nvarchar](50) NULL,
[ZipCode] [nvarchar](20) NULL,
[Version] [int] NULL
)
浙公网安备 33010602011771号