事务的概念
事物的个基本特性
数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作。事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。
1. 原子性(atomic),事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行
2. 一致性(consistent),事务在完成时,必须使所有的数据都保持一致状态。
3. 隔离性(insulation),由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。
4. 持久性(Duration),事务完成之后,它对于系统的影响是永久性的。
数据库事务并发可能带来的问题
如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题。由于并发操作带来的数据不一致性包括:(丢失更新)丢失数据修改、(脏读)读”脏”数据、不可重复读、(幻读)产生幽灵数据:
lost update(丢失更新)
事务T1读取了数据,并执行了一些操作,然后更新数据。事务T2也做相同的事,则T1和T2更新数据时可能会覆盖对方的更新,从而引起错误。
dirty read(脏读)
事务T1更新了数据还未提交,这时事务T2来读取相同的数据,则T2读到的数据其实是错误的数据,即脏数据。基于脏数据所作的操作是不可能正确的
unrepeatable read(不可重复读)
一个事务的两次读取中,读取相同的资源得到不同的值。当事务T2在事务T1的两次读取之间更新数据,则会发生此种错误(重点在修改)
phantom read(幻读)
事务T1对一定范围内执行操作,T2对相同的范围内执行不兼容的操作,这时会发生幻读。
如:T1删除符合条件C1的所有数据,T2又插入了一些符合条件C1的数据,则在T1中再次查找符合条件C1的数据还是可以查到,这对T1来说好像是幻觉一样,这时的读取操作称为幻读。(重点在新增或删除)
数据库的隔离级别
为了解决上面的4种问题,就出现了4种隔离级别,不同的数据库默认使用不同的隔离级别
1.read uncommit(未提交读)
当事务A更新某条数据时,不容许其他事务来更新该数据,但可以读取。
2.read commit(已提交读)
当事务A更新某条数据时,不容许其他事务进行任何操作包括读取,但事务A读取时,其他事务可以进行读取、更新
3.read repeatable(可重复读)
当事务A更新数据时,不容许其他事务进行任何操作,但当事务A进行读取时,其他事务只能读取,不能更新。
4.serializable(可序列化)
最严格的隔离级别,工作方式类似于可重复读。但它不仅会锁定受影响的数据,还会锁定这个范围。这就阻止了新数据插入查询所涉及的范围,事务必须依次进行。
Hibernate乐观锁策略,认为很少出现同时读取、更新的情况,在数据库隔离级别一般设为read commit,会导致出现lost update的问题
hibernate的事务
对jdbc的封装
hibernate是jdbc轻量级的封装,本身不具备事务管理的能力,在事物管理层面,一般是委托于底层的jdbc和jta来完成调度的。默认事物处理机制是基于jdbc transaction的,当然,我们也能通过配置jta来实现:
Xml代码
<hibernate-configuration>
<session-factory>
........
<property name="hibernate.transaction.factory_class">
org.hibernate.engine.transaction.internal.jta.JTATransactionFactory //JTA
<!--org.hibernate.engine.transaction.internal.jdbc.JDBCTransactionFactory--> //JDBC
</property>
........
</sesssion-factoty>
</hibernate-configuration>
将事物委托给jdbc处理,无疑是最简单的方式,hibernate支持它做默认方式,其事物的封装,也非常简单,比如当我们在处理这段代码时:
Java代码
try{
session = sessionFactory.openSession();
Transaction tx=session.beginTransaction();
......
tx.commit(); }catch(Exception e){
e.printStackTrace();
tx.rollback();
}
而从jdbc事物层面而言,实际上做的是:
Java代码
try{
Connection dbconn=getConnection();
dbconn.setAutoCommit(false);
......
dbconn.commit();
}catch(Exception e){
e.printStackTrace();
dbcon.rollback();
}
hibernate对Jta事务的支持
需要配置对jta的支持,属性配置文件中
hibernate.transaction.factory_class =org.hibernate.engine.transaction.internal.jta.JTATransactionFactory
hibernate.current_session_context_class=jta
hibernate.transaction.manager_lookup_class=classname.of.TransactionManagerLookup
或xml中
<property name="hibernate.transaction.factory_class">
org.hibernate.engine.transaction.internal.jta.JTATransactionFactory
</property>
<property name="hibernate.current_session_context_class">
jta
</property>
<property name="hibernate.transaction.manager_lookup_class">
classname.of.TransactionManagerLookup
</property>
try { UserTransaction tx = (UserTransaction)new InitialContext() .lookup("java:comp/UserTransaction"); tx.begin(); // Do some work on Session bound to transaction factory.getCurrentSession().load(...); factory.getCurrentSession().persist(...); tx.commit(); } catch (RuntimeException e) { tx.rollback(); throw e; // or display error message }
hibernate的悲观锁和乐观锁
Hibernate悲观锁: 在数据有加载的时候就给其进行加锁,直到该锁被释放掉,其他用户才可以进行修改
优点:数据的一致性保持得很好
缺点:不适合多个用户并发访问。当一个锁住的资源不被释放掉的时候,这个资源永远不会被其他用户进行修改,容易造成无限期的等待。
实现:悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保 证外部系统不会修改数据)。
Hibernate乐观锁:就是在对数据进行修改的时候,对数据采取版本或者时间戳等方式来比较,数据是否一致性来实现加锁。
优点比较好。
实现:版本或时间戳等,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过 为数据库表增加一个“version”字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应 记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
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子句实现加锁。
悲观锁的使用:
String hqlStr = " from TUser as user where user.name=’Erica’ " ;
Query query = session.createQuery(hqlStr);
query.setLockMode( " user " ,LockMode.UPGRADE); // 加锁
List userList = query.list(); // 执行查询
注意,只有在查询开始之前(也就是Hiberate 生成SQL 之前)设定加锁,才会 真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含for update 子句的 Select SQL加载进来,所谓数据库加锁也就无从谈起
这里Hibernate通过使用数据库的for update子句实现了悲观锁机制。
上面这种锁机制是我们在应用层较为常用的,加锁一般通过以下方法实现:
Criteria.setLockMode
Query.setLockMode
Session.lock
乐观锁的使用:
java代码:
省略getter和setter方法
package hibernate.senssic.relevance;
public class Person {
private int id;
private String name;
private int version;
}
xml映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="hibernate.senssic.relevance.Person" optimistic-lock="version">
<id name="id">
<generator class="identity">
</generator>
</id>
<!-- 必须位于id节点之后 -->
<version name="version"></version>
<property name="name"></property>
</class>
</hibernate-mapping>
每次对表进行更新的时候,我们可以发现,数据库中的version都在递增。通过比较version就可以解决脏读数据了
乐观锁的注解配置:
@Version
private int version;
悲观锁与乐观锁的比较:
悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受;相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在
系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)
optimistic-lock属性有如下可选取值:
Ø none
无乐观锁
Ø version
通过版本机制实现乐观锁
Ø dirty
通过检查发生变动过的属性实现乐观锁
Ø all
通过检查所有属性实现乐观锁
其中通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也是Hibernate中,目前唯一在数据对象脱离Session发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择version方式作为Hibernate乐观锁实现机制。