实际情况:多个事物同时更新某一条数据,第一个事物读取完之后挂起,第二个事务查询并做了修改,第一个事物提交,这个时候会覆盖掉第二个事物提交的东西
另外,减少整个对象save,save单独字段是更好的方式
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
Hibernate支持乐观锁。当多个事务同时对数据库表中的同一条数据操作时,如果没有加锁机制的话,就会产生脏数据(duty data)。Hibernate有2种机制可以解决这个问题:乐观锁和悲观锁。这里我们只讨论乐观锁。
Hibernate乐观锁,能自动检测多个事务对同一条数据进行的操作,并根据先胜原则,提交第一个事务,其他的事务提交时则抛出org.hibernate.StaleObjectStateException异常。Hibernate乐观锁是怎么做到的呢?
我们先从Hibernate乐观锁的实现说起。要实现Hibenate乐观锁,我们首先要在数据库表里增加一个版本控制字段,字段名随意,比如就叫version,对应hibernate类型只能为 long,integer,short,timestamp,calendar,也就是只能为数字或timestamp类型。然后在hibernate mapping里作如下类似定义:
<version name="version"
column="VERSION"
type="integer"/>
告诉Hibernate version作为版本控制用,交由它管理。
当然在entity class里也需要给version加上定义,定义的方法跟其他字段完全一样。
private Integer version;
…
// setVersion() && getVersion(Integer)
Hibernate乐观锁的的使用:
Session session1 = sessionFactory.openSession();
Session session2 = sessionFactory.openSession();
MyEntity et1 = session1.load(MyEntity.class, id);
MyEntity et2 = session2.load(MyEntity.class, id);
//这里 et1, et2为同一条数据
Transaction tx1 = session1.beginTransaction();
//事务1开始
et1.setName(“Entity1”);
//事务1中对该数据修改
tx1.commit();
session1.close();
//事务1提交
Transaction tx2 = session2.beginTransaction();
//事务2开始
et2.setName(“Entity2”);
//事务2中对该数据修改
tx2.commit();
session2.close();
//事务2提交
在事务2提交时,因为它提交的数据比事务1提交后的数据旧,所以hibernate会抛出一个org.hibernate.StaleObjectStateException异常。
回到前面的问题,Hibernate怎么知道事务2提交的数据比事务1提交后的数据旧呢?
因为MyEntity有个version版本控制字段。
回头看看上面的源代码中的:
MyEntity et1 = session1.load(MyEntity.class, id);
MyEntity et2 = session2.load(MyEntity.class, id);
这里,et1.version==et2.version,比如此时version=1,
当事务1提交后,该数据的版本控制字段version=version+1=2,而事务2提交时version=1<2所以Hibernate认为事务2提交的数据为过时数据,抛出异常。
这就是Hibernate乐观锁的原理机制。
我们已经知道了Hibernate乐观锁是根据version的值来判断数据是否过时,也就是说,在向数据库update某数据时,必须保证该entity 里的version字段被正确地设置为update之前的值,否则hibernate乐观锁机制将无法根据version作出正确的判断。
在我们的WEB应用中,尤其应该注意这个问题。
锁( locking),这个概念在我们学习多线程的时候曾经接触过,其实这里的锁和多线程里面处理并发的锁是一个道理,都是暴力的把资源归为自己所有。这里我们用到锁的目的就是通过一些机制来保证一些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓的“锁”,即给我们选定的目标数据上锁,使其无法被其他程序修改。Hibernate支持两种锁机制:即通常所说的“悲观锁(Pessimistic Locking )”和“乐观锁(Optimistic Locking )”。
悲观锁( Pessimistic Locking)
悲观锁,正如其名,他是对数据库而言的,数据库悲观了,他感觉每一个对他操作的程序都有可能产生并发。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
一个典型的倚赖数据库的悲观锁调用:
下面的代码实现了对查询记录的加锁: query.setLockMode对查询语句中,特定别名所对应的记录进行加锁(我们为TUser类指定了一个别名“user”),这里也就是对返回的所有 user 记录进行加锁。
观察运行期Hibernate生成的 SQL语句:
这里Hibernate通过使用数据库的 for update子句实现了悲观锁机制。
hibernate的加锁模式有:
LockMode.NONE :无锁机制。
LockMode.WRITE :Hibernate在 Insert和 Update记录的时候会自动获取。
LockMode.READ :Hibernate在读取记录的时候会自动获取。
以上这三种锁机制一般由 Hibernate内部使用,如Hibernate为了保证 Update过程中对象不会被外界修改,会在 save 方法实现中自动为目标对象加上WRITE锁。
LockMode.UPGRADE :利用数据库的 for update 子句加锁。
LockMode. UPGRADE_NOWAIT : Oracle的特定实现,利用 oracle的 for update nowait子句实现加锁。
上面这两种锁机制是我们在应用层较为常用的,加锁一般通过以下方法实现:
Criteria.setLockMode
Query.setLockMode
Session.lock
注意,只有在查询开始之前(也就是 Hiberate生成 SQL之前)设定加锁,才会真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含 for update子句的 Select SQL加载进来,所谓数据库加锁也就无从谈起。
在Hibernate使用悲观锁十分容易,但实际应用中悲观锁是很少被使用的,因为它大大限制了并发性,并且利用数据库底层来维护锁,这样大大降低了应用程序的效率。
下面我们来看一下hibernateAPI中提供的两个get方法:
Get(Classclazz,Serializable id,LockMode lockMode)
Get(Classclazz,Serializable id,LockOptions lockOptions )
可以看到get方法第三个参数"lockMode"或"lockOptions",注意在Hibernate3.6以上的版本中"LockMode"已经不建议使用。方法的第三个参数就是用来设置悲观锁的,使用第三个参数之后,我们每次发送的SQL语句都会加上"for update"用于告诉数据库锁定相关数据。LockMode参数选择UPGRADE选项,就会开启悲观锁。
乐观锁(Optimistic Locking)
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现。
乐观锁的工作原理:读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
Hibernate为乐观锁提供了3中实现:
1.基于version
2.基于timestamp
3.为遗留项目添加添加乐观锁
配置基于version的乐观锁:
注:不要忘记在实体类添加属性version
配置基于timestamp的乐观锁:
下面我们就模拟多个session,基于version的来进行一下测试:
运行结果:
Hibernate: insert into studentVersion (ver, name,id) values (?, ?, ?)
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.nameas name0_ from studentVersion student0_ where student0_.name='tom11'
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.nameas name0_ from studentVersion student0_ where student0_.name='tom11'
v1=0--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
v1=1--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
Exception in thread "main" org.hibernate.StaleObjectStateException:Row was updated or deleted by another transaction (or unsaved-value mapping wasincorrect): [Version.Student#4028818316cd6b460116cd6b50830001]
可以看到,第二个“用户”session2修改数据时候,记录的版本号已经被session1更新过了,所以抛出了红色的异常,我们可以在实际应用中处理这个异常,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据展示出来,让使用者有机会比较一下,或者设计程序自动读取新的数据