4.事务和并发
Hibernate是对JDBC的轻量级对象封装,Hibernate本身是不具备Transaction处理功能的,Hibernate的Transaction实际上是底层的JDBC Transaction的封装,或者是JTA Transaction的封装。
Hibernate可以配置为JDBCTransaction或者是JTATransaction,这取决于你在hibernate.properties或者hibernate.cfg.xml中的配置,如果你什么都不配置,默认情况下使用JDBCTransaction。
Hibernate的JDBCTransaction根本就是conn.commit而已,根本毫无神秘可言,只不过在Hibernate中,Session打开的时候,就会自动conn.setAutoCommit(false),不像一般的JDBC,默认都是true,所以你最后不写commit也没有关系,由于Hibernate已经把AutoCommit给关掉了,所以用Hibernate的时候,你在程序中不写Transaction的话,数据库根本就没有反应。
4.1Hibernate的事务隔离机制
简而言之:Hibernate实质是在使用数据库的隔离机制。
详细论述:--------------------------------->
Hibernate 的事务和并发控制很容易掌握。Hibernate 直接使用 JDBC 连接和 JTA 资源,不添加
任何附加锁定行为。Hibernate 不锁定内存中的对象。你的应用程序会按照你的数据库事务的隔离级别规定的那样运作。
数据库的隔离级别:并发性作用。
1、 Read Uncommited(未提交读):没有提交就可以读取到数据(发出了Insert,但没有commit就可以读取到。)很少用
2、 Read Commited(提交读):只有提交后才可以读,常用。
3、 Repeatable Read(可重复读):mysql默认级别, 必需提交才能见到,读取数据时数据被锁住。
4、 Serialiazble(序列化读):最高隔离级别,串型的,你操作完了,我才可以操作,并发性特别不好,
隔离级别 |
是否存在脏读 |
是否存在不可重复读 |
是否存在幻读 |
Read Uncommitted(未提交读) |
Y |
Y |
Y |
Read Commited(提交读) |
N |
Y(可采用悲观锁解决) |
Y |
Repeatable Read(可重复读) |
N |
N |
Y |
Serialiazble(序列化读) |
脏读:没有提交就可以读取到数据称为脏读
不可重复读:再重复读一次,数据与你上的不一样。称不可重复读。
幻读:在查询某一条件的数据,开始查询的后,别人又加入或删除些数据,再读取时与原来的数据不一样了。
4.2 Session和事务范围
幸亏有了 Session,使得 Hibernate 通过标识符查找,和实体查询(不是返回标量值的报表查询)提供了可重复的读取(Repeatable reads)功能,Session 同时也是事务范围内的缓存(cache)。除了对自动乐观并发控制提供版本管理,针对行级悲观锁定,Hibernate 也提供了辅助的(较小的)API,它使用了 SELECT FOR UPDATE的 SQL 语法。
SessionFactory 对象的创建代价很昂贵,它是线程安全的对象,它为所有的应用程序线程所共享。它只创建一次,通常是在应用程序启动的时候,由一个 Configuraion的实例来创建。
Session 对象的创建代价比较小,是非线程安全的,对于单个请求,单个会话、单个的 工作单元而言,它只被使用一次,然后就丢弃。只有在需要的时候,一个 Session对象 才会获取一个 JDBC的 Connection(或一个Datasource)对象,因此假若不使用的时候它不消费任何资源。
- 数据库事务应该尽可能的短,降低数据库中的锁争用。
数据库长事务会阻止你的应用程序扩展到高的并发负载。因此,假若在用户思考期间让数据库事务开着,
直到整个工作单元完成才关闭这个事务,这绝不是一个好的设计。
Hibernate 禁止立即自动事务提交模式,或者期望应用服务器禁止立即自动事务提交模式。数据库事务绝不是可有可无的,任何与数据库之间的通讯都必须在某个事务中进行,不管你是在读还是在写数据。在多用户的 client/server 应用程序中,最常用的模式是 每个请求一个会话,一个新的 Hibernate Session 被打开,并且执行这个操作单元中所有的数据库操作。
- current session
Hibernate 内置了对"当前 session(current session)" 的管理,用于简化此模式。你要做的一切就是在服务器端要处理请求的时候,开启事务,在响应发送给客户之前结束事务。
假若你决定使用编程式的事务分界(见下面引用说明),请参考本章后面讲到的 Hibernate TransactionAPI。
编程式事务 or 声明式事务
- 编程式事务需要你在代码中直接加入处理事务的逻辑,可能需要在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法,如在执行a方法时候需要事务处理,你需要在a方法开始时候开启事务,处理完后。在方法结束时候,关闭事务.
- 声明式的事务的做法是在a方法外围添加注解或者直接在配置文件中定义,a方法需要事务处理,在spring中会通过配置文件在a方法前后拦截,并添加事务.
有时,将 Session 和数据库事务的边界延伸到"展示层被渲染后"会带来便利。你可以找到 Open Session in View 这一模式的提示和示例。(Z10:不推荐使用)
长对话(指的是用户读取,操作时间之间存在较长时间。而非指长事务。)
简而言之:
1.事务要短小。
2长会话建议使用Hibernate自动化版本(即version)
Z10:“在性能和准确性之间需要做一个选择。一般情况下,高并发需要牺牲一定的操作体验,udpate的时候使用version进行比对,不符则回退操作,提示用户。而在低并发注重体验的情况下,可考虑使用悲观锁。保护用户的操作得以成功。”
关注对象标识
简而言之:
不要使用数据库标识来实现对象相等,应该使用业务键值,由唯一的,通常不变的属性组成(通过覆写hashcode和equals方法判断对象是否相同)。
详细论述如下:-------------------------------------->
应用程序可能在两个不同的 Session 中并发访问同一持久化状态,但是,一个持久化类的实例无法在两个 Session*享。因此有两种不同的标识语义:
数据库标识
foo.getId().equals( bar.getId() )
JVM 标识
foo==bar对于那些关联到特定 Session(也就是在单个 Session的范围内)上的对象来说,这两种标识的语义是等价的,与数据库标识对应的 JVM 标识是由 Hibernate 来保证的。不过,当应用程序在两个不同的 session 中并发访问具有同一持久化标识的业务对象实例的时候,这个业务对象的两个实例事实上是不相同的(从 JVM 识别来看)。这种冲突可以通过在同步和提交的时候使用自动版本化和乐观锁定方法来解决。
这种方式把关于并发的头疼问题留给了 Hibernate 和数据库;由于在单个线程内,操作单元中的对象识别不 需要代价昂贵的锁定或其他意义上的同步,因此它同时可以提供最好的可伸缩性。
1 只要在单个线程只持有一个 Session,应用程序就不需要同步任何业务对象。在 Session的范围内,应用程序可以放心的使用 ==进行对象比较。
2 从 JVM 标识的定义上来说,对脱管的对象而言,Hibernate 无法保证他们 的 JVM 标识一致。开发人员必须覆盖持久化类的 equals() 方法和hashCode() 方法,从而实现自定义的对象相等语义。
4.3 数据库事务声明
4.3.1.使用清晰的事务声明,即使只读操作也是如此。
在一个非托管环境中,Hibernate 通常自己负责管理数据库连接池。应用程序开发人员必须手工设置事务声明,换句话说,就是手工启动,提交,或者回
滚数据库事务。你可以使用编程式的事务管理。Hibernate提供了一套称为 Transaction 的封装 API, 用来把你的部署环境中的本地事务管理系统转换
到Hibernate 事务上。这个 API 是可选的,但是我们强烈推荐你使用。
4.3.2.异常处理
如果 Session 抛出异常(包括任何 SQLException),你应该立即回滚数据库事务,调用Session.close()。所有由 Hibernate 抛出的异常都视为不可以恢复的。确保在 finally代码块中调用 close()方法,以关闭掉 Session。我们的观点是,不应该强迫应用程序开发人员 在底层捕获无法恢复的异常。
4.3.3事务超时
超时可以确保不会无限挂起资源、对用户没有交代。在托管(JTA)环境之外,Hibernate 无法完全提供这一功能。但是,Hiberante 至少可以控制数据访问,确保数据库级别的死锁,和返回巨大结果集的查询被限定在一个规定的时间内。
参考代码
Session sess = factory.openSession();
try {
//set transaction timeout to 3 seconds
sess.getTransaction().setTimeout(3);
sess.getTransaction().begin();
// do some work
...
sess.getTransaction().commit()
}
catch (RuntimeException e) {
sess.getTransaction().rollback();
throw e; // or display error message
}
finally {
sess.close();
}
注意 setTimeout()不应该在 CMT bean 中调用,此时事务超时值应该是被声明式定义的。
4.4乐观并发控制
唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的乐观并发控制。版本检查使用版本号、 或者时间戳来检测更新冲突(并且防止更新丢失)。
其他略。
4.5 乐观锁和悲观锁
Hibernate谈到悲观锁、乐观锁,就要谈到数据库的并发问题,数据库的隔离级别越高它的并发性就越差。
4.5.1悲观锁:
悲观锁:具有排他性(我锁住当前数据后,别人无法修改数据)
悲观锁一般由数据库机制来做到的。
悲观锁的实现
通常依赖于数据库机制,在整修过程中将数据锁定,其它任何用户都不能读取或修改(如:必需我修改完之后,别人才可以修改)
悲观锁的适用场景:
悲观锁一般适合短事务比较多(如某一数据取出后加1,立即释放) 长事务占有时间(如果占有1个小时,那么这个1小时别人就不可以使用这些数据),不常用。
悲观锁的使用
如果需要使用悲观锁,肯定在加载数据时就要锁住,通常采用数据库的for update语句。Hibernate使用Load进行悲观锁加载。
Session.load(Class arg0, Serializable arg1, LockMode arg2) throws HibernateException
LockMode:悲观锁模式(一般使用LockMode.UPGRADE)
session = HibernateUtils.getSession();
tx = session.beginTransaction();
Inventory inv = (Inventory)session.load(Inventory.class, 1, LockMode.UPGRADE);
System.out.println(inv.getItemName());
inv.setQuantity(inv.getQuantity()-200); session.update(inv);
tx.commit();
执行输出SQL语句:
Hibernate: select inventory0_.itemNo as itemNo0_0_, inventory0_.itemName as itemName0_0_, inventory0_.quantity as quantity0_0_ from t_inventory inventory0_ where inventory0_.itemNo=? for update //在select语句中加入for update进行使用悲观锁。
脑白金
Hibernate: update t_inventory set itemName=?, quantity=? where itemNo=?
注:只有用户释放锁后,别的用户才可以读取
注:如果使用悲观锁,那么lazy(懒加载无效)
延伸阅读:
Hibernate 总是使用数据库的锁定机制,从不在内存中锁定对象。
类 LockMode定义了 Hibernate 所需的不同的锁定级别。一个锁定可以通过以下的机制来设置:
•当 Hibernate 更新或者插入一行记录的时候,锁定级别自动设置为 LockMode.WRITE。
•当用户显式的使用数据库支持的 SQL 格式 SELECT ... FOR UPDATE 发送 SQL 的时候,锁定级
别设置为 LockMode.UPGRADE。
•当用户显式的使用 Oracle 数据库的 SQL 语句 SELECT ... FOR UPDATE NOWAIT的时候,锁定级
别设置 LockMode.UPGRADE_NOWAIT。
•当 Hibernate 在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候,锁定模式
自动设置为 LockMode.READ。这种模式也可以通过用户显式指定进行设置。
•LockMode.NONE 代表无需锁定。在 Transaction 结束时, 所有的对象都切换到该模式上来。与
session 相关联的对象通过调用 update()或者 saveOrUpdate()脱离该模式。"显式的用户指定"可以通过以下几种方式之一来表示:
•调用 Session.load()的时候指定锁定模式(LockMode)。
•调用 Session.lock()。
•调用 Query.setLockMode()。
如果在 UPGRADE或者 UPGRADE_NOWAIT锁定模式下调用 Session.load(),并且要读取的对象尚未被
session 载入过,那么对象通过 SELECT ... FOR UPDATE这样的 SQL 语句被载入。如果为一个对象
调用 load()方法时,该对象已经在另一个较少限制的锁定模式下被载入了,那么 Hibernate 就
对该对象调用 lock()方法。
如果指定的锁定模式是 READ,UPGRADE或 UPGRADE_NOWAIT,那么 Session.lock()就执行版本号检
查。(在 UPGRADE 或者 UPGRADE_NOWAIT 锁定模式下,执行 SELECT ... FOR UPDATE这样的SQL语
句。)
如果数据库不支持用户设置的锁定模式,Hibernate 将使用适当的替代模式(而不是扔出异
常)。这一点可以确保应用程序的可移植性。
4.5.2乐观锁:
乐观锁:不是锁,是一种冲突检测机制。
乐观锁的并发性较好,因为我改的时候,别人随边修改。
乐观锁的实现方式:常用的是版本的方式(每个数据表中有一个版本字段version,某一个用户更新数据后,版本号+1,另一个用户修改后再+1,当用户更新发现数据库当前版本号与读取数据时版本号不一致(等于小于数据库当前版本号),则更新不了。
乐观锁在存储数据时不用关心。