您的位置:首页 > 其它

精通Hibernate——应用程序中的悲观锁和乐观锁

2015-09-09 17:27 344 查看
当数据库采用read commited隔离级别时,会导致不可重复读和第二类丢失更新的并发问题。可以使用悲观锁或者乐观锁来避免这类问题

悲观锁:在应用程序中显式的为数据资源加锁,悲观锁假定当前事务操纵数据资源时,肯定还会有其他事务同时访问该数据,为了避免当前事务操作收到干扰,先锁定资源,但是他会影响性能。

乐观锁:假定当前操纵数据资源时,不会有其他事务同时访问该数据资源,此时,完全依赖数据库的隔离级别来管理锁的工作。应用程序采用版本控制手段来避免可能出现的并发问题。

悲观锁

悲观锁不常用,这里只是简单的提一下,他的实现方式有二:

1、在应用程序中显式指定采用数据库独占锁来锁定数据资源

2、在数据库表中增加一个表明记录状态的LOCK字段,当值为Y时,表示已经锁定,N表示空闲

说到悲观锁我们顺便学习下LockMode

其实LockMode只是在使用Hibernate 中 的session.load()加载数据时指定的模式,也叫悲观锁(模式),然而,悲观锁是为了弥补read-committed 机制的不足,从而解决non-repeatable (不可重复读)和 phantom-read (幻读)问题 ,而non-repeatable 和 phantom-read 这两个问题也只是事务并发是产生的两种问题

锁定模式

LockMode.NONE:如果缓存中存在对象,直接返回该对象的引用,否则通过select语句到数据库中加载该对象,默认值.

LockMode.READ:不管缓存中是否存在对象,总是通过select语句到数据库中加载该对象,如果映射文件中设置了版本元素,就执行版本检查,比较缓存中的对象是否和数据库中对象版本一致

LockMode.UPGRADE:不管缓存中是否存在对象,总是通过select语句到数据库中加载该对象,如果映射文件中设置了版本元素,就执行版本检查,比较缓存中的对象是否和数据库中对象的版本一致,如果数据库系统支持悲观锁(如Oracle/MySQL),就执行select…for update语句,如果不支持(如Sybase),执行普通select语句,在程序中如何加上悲观锁呢,很简单,直接在session的load方法中添加一个参数即可,例如:

Account a = (Account)session.load(Account.class, 1, LockMode.UPGRADE);


使用悲观锁查找数据或者更新数据时,默认加上for update ,表示对当前事务加锁,其他事务暂时不能使用,例如 select … for update

LockMode.UPGRADE_NOWAIT和LockMode.UPGRADE:具有同样功能,此外,对于Oracle等支持update nowait的数据库,执行select…for update nowait语句,nowait表明如果执行该select语句的事务不能立即获得悲观锁,那么不会等待其它事务释放锁,而是立刻抛出锁定异常

LockMode.WRITE:保存对象时会自动使用这种锁定模式,仅供Hibernate内部使用,应用程序中不应该使用它

LockMode.FORCE:强制更新数据库中对象的版本属性,从而表明当前事务已经更新了这个对象

利用Hibernate版本控制来实现乐观锁

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乐观锁的的使用:

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乐观锁的原理机制。

对于StaleObjectStateException异常有两种处理方式:

方式一:自动撤销事务,通知信息已被其他事务提交,需要重新开始事务

try{
}catch(StaleObjectStateException e){
    tx.rollback();
    log.info("信息已被其他事务提交,需要重新开始事务");
}


方式二:通知信息已被其他事务修改,显示最新信息,由用户决定如何继续事务,用户也可以决定立即撤销事务

Hibernate的乐观锁除了使用version,还可以使用timestamp,两者用法类似



理论上version比timestamp更安全,timestamp只能精确到秒

对游离对象进行版本检查

Session的lock方法显式对一个游离对象进行版本检查

Session session1 = sessionFactory.openSession():
tx1 = session1.beginTransaction();
Account account = (Account)session1.get(Account.class,new long(1));
tx1.commit();
session1.close();

Session session2 = sessionFactory.openSession():
tx2 = session2.beginTransaction();
session2.lock(account,LockMode.READ);
tx2.commit();
session2.close();


lock方法的执行步骤如下:

1、把Account对象与当前Session关联

2、如果设定了LockMode.READ,就比较这个Account对象的版本与accounts表中对应的记录版本是否一致,如果不一致,说明该记录已经被其他事务更改,因此会抛出StaleObjectStateException 异常
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: