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

当数据库采用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 异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值