23
2014
05

Hibernate4性能之并发和锁机制

 数据库事务的定义 

数据库事务(Database Transaction),是指作为单个逻辑工作单元执行的一系列操作。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。 
● 原子性(atomic),事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行 
● 一致性(consistent),事务在完成时,必须使所有的数据都保持一致状态。 
● 隔离性(insulation),由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。 
● 持久性(Duration),事务完成之后,它对于系统的影响是永久性的。 

数据库事务并发可能带来的问题 
如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题。由于并发操作带来的数据不一致性包括:丢失数据修改、读”脏”数据(脏读)、不可重复读、产生幽灵数据: 

假设数据库中有如下一条记录: 

● 第一类丢失更新(lost update):在完全未隔离事务的情况下,两个事物更新同一条数据资源,某一事物异常终止,回滚造成第一个完成的更新也同时丢失。 
 
在T1时刻开启了事务1,T2时刻开启了事务2,在T3时刻事务1从数据库中取出了id="402881e535194b8f0135194b91310001"的数据,T4时刻事务2取出了同一条数据,T5时刻事务1将age字段值更新为30,T6时刻事务2更新age为35并提交了数据,但是T7事务1回滚了事务age最后的值依然为20,事务2的更新丢失了,这种情况就叫做"第一类丢失更新(lost update)"。 
● 脏读(dirty read):如果第二个事务查询到第一个事务还未提交的更新数据,形成脏读。因为第一个事务你还不知道是否提交,所以数据不一定是正确的。 
 
在T1时刻开启了事务1,T2时刻开启了事务2,在T3时刻事务1从数据库中取出了id="402881e535194b8f0135194b91310001"的数据,在T5时刻事务1将age的值更新为30,但是事务还未提交,T6时刻事务2读取同一条记录,获得age的值为30,但是事务1还未提交,若在T7时刻事务1回滚了事务2的数据就是错误的数据(脏数据),这种情况叫做" 脏读(dirty read)"。 
● 虚读(phantom read):一个事务执行两次查询,第二次结果集包含第一次中没有或者某些行已被删除,造成两次结果不一致,只是另一个事务在这两次查询中间插入或者删除了数据造成的。 
 
在T1时刻开启了事务1,T2时刻开启了事务2,T3时刻事务1从数据库中查询所有记录,记录总共有一条,T4时刻事务2向数据库中插入一条记录,T6时刻事务2提交事务。T7事务1再次查询数据数据时,记录变成两条了。这种情况是"虚读(phantom read)"。 
● 不可重复读(unrepeated read):一个事务两次读取同一行数据,结果得到不同状态结果,如中间正好另一个事务更新了该数据,两次结果相异,不可信任。 
 
在T1时刻开启了事务1,T2时刻开启了事务2,在T3时刻事务1从数据库中取出了id="402881e535194b8f0135194b91310001"的数据,此时age=20,T4时刻事务2查询同一条数据,T5事务2更新数据age=30,T6时刻事务2提交事务,T7事务1查询同一条数据,发现数据与第一次不一致。这种情况就是"不可重复读(unrepeated read)"。 
● 第二类丢失更新(second lost updates):是不可重复读的特殊情况,如果两个事务都读取同一行,然后两个都进行写操作,并提交,第一个事务所做的改变就会丢失。 
 
在T1时刻开启了事务1,T2时刻开启了事务2,T3时刻事务1更新数据age=25,T5时刻事务2更新数据age=30,T6时刻提交事务,T7时刻事务2提交事务,把事务1的更新覆盖了。这种情况就是"第二类丢失更新(second lost updates)"。 

Hibernate事务隔离级别:(不同数据库对应默认的级别不一样) 
为了解决数据库事务并发运行时的各种问题数据库系统提供四种事务隔离级别,在Hibernate的配置文件中可以显示的配置数据库事务隔离级别。每一个隔离级别用一个整数表示: 
● Serializable 串行化(8)二进制值0001 
● Repeatable Read 可重复读(4)二进制值0010 MySql默认隔离级别 
● Read Commited 可读已提交(2)二进制值0100 Oracle默认级别 
● Read Uncommited 可读未提交(1)二进制值1000 
在hibernate.cfg.xml中使用hibernate.connection.isolation参数配置数据库事务隔离级别。 

每一个隔离级别可以解决的问题: 

隔离级别第一类丢失更新脏读幻读不可重复读第二类丢失更新
串行化不可能不可能不可能不可能不可能
可重复读不可能不可能可能不可能不可能
可读已提交不可能不可能可能可能可能
可读未提交不可能可能可能可能可能



Hibernate对数据的锁机制: 
Hibernate可以利用Query或者Criteria的setLockMode()方法来设定要锁定的表或列(Row)及其锁定模式: 
LockMode.NONE:无锁机制;在事务结束时,所有的对象都切换到该模式上来。与session相关联的对象通过调用update()或者saveOrUpdate()脱离该模式 
LockMode.WRITE:当更新或者插入一行记录的时候,锁定级别自动设置为LockMode.WRITE 
LockMode.READ:当Hibernate在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候,锁定模式自动设置为LockMode.READ。这种模式也可以通过用户显式指定进行设置。 
LockMode.UPGRADE:利用数据库的for update子句加锁 
LockMode.UPGRADE_NOWAIT:利用oracle的特定实现for update nowait子句实现 

使用悲观锁解决事务并发问题: 
悲观锁,正如其名,他总是悲观的认为要操作的数据会有并发访问。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 
一个典型的依赖数据库的悲观锁调用:select * from account where name=”Erica” for update这条sql语句锁定了account表中所有符合检索条件(name=”Erica”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。悲观锁,也是基于数据库的锁机制实现。 
在Hibernate使用悲观锁十分容易,但实际应用中悲观锁是很少被使用的,因为它大大限制了并发性: 

T1,T2时刻取款事务和转账事务分别开启,T3事务查询ACCOUNTS表的数据并用悲观锁锁定,T4转账事务也要查询同一条数据,数据库发现该记录已经被前一个事务使用悲观锁锁定了,然后让转账事务等待直到取款事务提交。T6时刻取款事务提交,T7时刻转账事务获取数据。 

悲观锁用法参考下面代码实例: 

Transaction tx=session.beginTransaction();  
//取得持久化User对象,并使用LockMode.UPGRADE模式锁定对象  
User user=(User)session.get(User.class,1,LockMode.UPGRADE);  
user.setName(“newName”); //更改对象属性,注意并不需要使用session.save(user)  
tx.commit();  
  
String hqlStr="from TUser user where user.name='Erica'";  
Query query=session.createQuery(hqlStr);    
query.setLockMode("user",LockModel.UPGRADE);

 这样的话,Hibernate会使用select …… for update语句载入User类,并且锁住了这个对象在数据库中的列,直到事务完成(commit()以后)。 


使用乐观锁解决事务并发问题 
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现。 
  乐观锁的工作原理:读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 
Hibernate为乐观锁提供了3种实现: 
●基于version 
●基于timestamp 
●为遗留项目添加添加乐观锁 

总结 
数据库事务应该尽可能的短 
这样能降低数据库中的锁争用。数据库长事务会阻止你的应用程序扩展到高的并发负载。因此,假若在用户思考期间让数据库事务开着,直到整个工作单元完成才关闭这个事务,这绝不是一个好的设计。 
这就引出一个问题:一个操作单元,也就是一个事务单元的范围应该是多大? 
一个操作一个?一个请求一个?一个应用一个? 
反模式:session-per-operation 
在单个线程中,不要因为一次简单的数据库调用,就打开和关闭一次Session!数据库事务也是如此。也就是说应该禁止自动事务提交(auto-commit)。 
session-per-request 
最常用的模式是每个请求一个会话。在这种模式下,来自客户端的请求被发送到服务器端,即 Hibernate 持久化层运行的地方,一个新的 Hibernate Session 被打开,并且执行这个操作单元中所有的数据库操作。一旦操作完成(同时对客户端的响应也准备就绪),session 被同步,然后关闭。会话和请求之间的关系是一对一的关系。 
Hibernate内置了对“当前session(current session)”的管理,用于简化此模式。你要做的一切就是在服务器端要处理请求的时候,开启事务,在响应发送给客户之前结束事务,通常使用Servelt Filter来完成。 
针对这种模式,Spring提供了对Hibernate事务的管理,提供了“一请求一事务”的Filter来利用Http请求与响应来控制session和事务的生命周期。 

<filter>  
      <filter-name>HibernateOpenSessionInViewFilter</filter-name>  
      <filter-class>  
            org.springframework.orm.hibernate3.support.OpenSessionInViewFilter  
      </filter-class>  
</filter>

摘自:http://zhou137520.iteye.com/blog/1638574


« 上一篇下一篇 »

评论列表:

1.博主  2014-5-23 17:05:28 回复该留言
文章写的不错,学习了。这里有一个问题想请教下lz:乐观锁的版本记录version的实现方案具体是怎么操作的呢?在建表的时候就为乐观锁额外的添加一个version字段吗?
.博主  2014-5-23 17:06:57 回复该留言
一般是通过为数据库表增加一个 “version” 字段来实现(我只知道Oracle是这样,其他的没试过)
个人感觉火车站卖票系统就是乐观锁,查票时查到了脏数据(就是另外一些事务还未提交的数据),看到显示屏上有少量剩余票,但点进去去买又买不到。乐观锁原理其实就是给每一条数据加上时间概念的标识,表明我这条数据是什么时候的,可以理解为是相对时间,就是你所说的建表的时候添加version,然后我去取这条数据,比如version这个时候是0015了,那么我操作完这条数据,要update回去时,又去查一遍这个version值,发现如果还是0015,那么我就放心了,我就可以把version变成0016然后提交,因为在我操作这个过程中没人动他,如果比0015大,那说明有人动过他了,我就选择不操作这条数据了。

还有一种方式是直接用时间字段表示,也就是时间戳,就是我提交这条数据时,之前拿到数据时的那个时间值一定要等于我要提交之前这个时刻点的时间值,如果小于当前数据的这个时间戳,那么肯定是有事务在我之前提交了。

其实乐观锁是解决高并发性能的办法。悲观锁更安全,但是并发性能太差,特别是有长事务的时候,并发事务会排很长的队。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。