从事务隔离级别谈到Hibernate乐观锁,悲观锁

先提一下基础知识,数据库事务的4个基本特性:

1.Atomic(原子性)

事务中包含的操作被看成一个逻辑单元,这个单元要么全部完成,要么全部没做。

2.Consistency(一致性)

隔离执行事务时(在没有其他事务并发的情况下)保持数据库的一致性。举例说明:A转账给B,那么此事务执行前和执行后A账户和B账户的总和是不变的。

3.Isolation(隔离性)

尽管多个事务可能并发执行,但是系统保证,对于任一事务Ti和Tj,在Ti看来,Tj或者在Ti开始之前已经完成执行,或者在Ti完成之后开始执行的。这样,每个事务都感觉不到其他事务在并发的执行。

4.Durability(持久性)

一个事务一旦执行成功,它对数据库的改变是永久的,即使系统可能出现故障。确保持久性是数据库系统中称为恢复管理部件的软件部件负责的。例如基于日志的恢复,远程备份系统等等。

事务的并发执行需要严格的管控,否则会出现各种问题:

1.Lost Update

在完全未隔离事务的情况下,两个事务更新同一条数据,其中一个事务异常终止,然后回滚,而在这个事务回滚之前另外一个事务提交成功,那么另外一个事务提交的数据会被回滚操作所覆盖。

2.Dirty Read

一个事务读到了另外一个事务未提交的新数据。

3.Phantom Read 

一个事务执行了两次查询操作(例如统计具有某特性的数据行数),但是两次查询操作中间,有其他事务插入了新数据或者删除了数据,造成两次结果不一致。

4.Unreapted Read

一个事务对同一条数据执行了两次读操作,但是读取的结果却不一样。出现原因:两次读操作之间有另外一个事务更新了这条数据。注意与Phantom Read区分,这里如果锁住要读的数据就不会出问题,但是对于Phantom Read仅仅锁住一行数据是没有用的!

5.Second lost Update

这是4的特殊情况,如果两个事务都读取同一行数据,都进行写操作并提交成功,那么第一个提交的事务所做的更新就会被第二个事务所覆盖。


要解决以上可能出现的并行问题,数据库一般都提供了事务隔离级别,不同的隔离级别解决问题的程度不一样。

1. Serializable 串行化
2. Repeatable Read 可重复读
3. Read Commited 可读已提交
4. Read Uncommited 可读未提交

从事务隔离级别谈到Hibernate乐观锁,悲观锁_第1张图片

这里推荐一篇文章http://www.cnblogs.com/zhujingyuan/archive/2009/11/12/1602193.html

此文中把隔离级别讲得很清楚了。对于SqlServer默认的事务隔离级别是Read Commited。已经可以满足大部分的应用场景了。


要实现上述的隔离级别就必须要使用锁,从程序员的角度来看锁分为两种:悲观锁和乐观锁

下面看Hibernate中的悲观锁和乐观锁。

悲观锁的使用场景是基于以下假设:在修改一条数据的时候,另外一个线程也在修改同一条数据的可能性很大。

乐观锁的使用场景则相反,在修改一条数据的时候另外一个线程修改同一条数据的可能性非常小。

悲观锁依赖数据库本身的锁机制,当然也只有依赖数据库本身的锁机制,才能真正将数据锁住。否则即使当前系统无法修改数据,其他系统还是可以修改数据的!你加了锁,别人没加锁!

很多文章都提到这样一个例子来说明悲观锁:

select * from some_table where id = 1 for update
在执行这条SQL后,并在当前事务结束之前,id为1的所有记录的所有字段都将被锁定。我觉得这个例子正好暴露了悲观锁不能解决Phantom Read的问题,如果此事务操作过程中,有某一条数据的id由2变为1,那么将出现Phantom Read。

Hibernate中如下使用悲观锁:

public class PessimisticLocking
{
	public static void main(String[] args) throws Exception
	{
		Session session = HibernateSessionFactory.getSession();
		Transaction tx = session.beginTransaction();
		tx.begin();
		String hql = "from message as m where id = 1";
		Query query = session.createQuery(hql);
		query.setLockMode("m",LockMode.UPGRADE);
		List message = query.list();
		...
		System.out.println("Now,the data whose id = 1 could not be modify,after press enter you can modify");
		System.in.read();//read a char from the console , let the application in a staying state!
		tx.commit();
		session.close();
	}
}

在运行这段代码的时候,如果不按回车那么tx.commit就不会执行,这个事务就没提交,这个事务没提交数据库中所有id=1的记录都不能被任何工具修改!使用setLockMode方法获得悲观锁后,Hibernate会生成如下SQL:

select message0_.id as id0_,message0_name as name0_ from message mymessage0_ where mymessage0_.id=1 for update
可以看出来,Hibernate是通过在SQL语句中加入for update来获得悲观锁的。

到这里,我想了下为什么不直接使用数据库的事务隔离级别来解决问题呢,而非要在代码中加入悲观锁?

想了下,理由很简单:如果直接设置数据库的事务隔离级别,就不能动态调整事务隔离级别了,在一个项目中不同的地方可能使用不同的隔离级别,如果都使用串行化的隔离级别自然能解决所有并发问题,但是这会带来性能上的下降!终归还是性能问题!而在适当的时候给予相配的隔离级别不但解决了问题,性能也要快很多!

总结起来就是:可以动态更改事务隔离级别。

再来看看乐观锁(Optimistic Locking)

由于悲观锁有很大的性能损失。而乐观锁是基于数据版本(version)实现的,因此对于同一个应用程序,它可以很好地实现锁机制,在不浪费太多性能的前提下。

Hibernate中可以如下实现:

在相应的表中加一列叫做version,类型为int

在实体Bean的xml配置文件中加入一列version,并且添加optimistic-lock属性


	
	
...


然后就可以使用乐观锁了,Java代码如下:

public class OptimisticLocking
{
	public static void main(String[] args) throws Exception
	{
		Session session = HibernateSessionFactory.getSession();
		Transaction tx = session.beginTransaction();
		tx.begin();
		String hql = "from message as m where id = 1";
		Query query = session.createQuery(hql);
		query.setLockMode("m",LockMode.UPGRADE);
		Message message = (Message)query.uniqueResult();
		if(message != null)
		{
			message.setName("newName");
			session.saveOrUpdate(message);
		}
		System.in.read();//read a char from the console , let the application in a staying state!
		tx.commit();
		session.close();
	}
}

同时运行两次上述程序,都不按回车键,这时对其中一个按回车提交事务,这样version就加1了,而另外一个程序的version就和当前记录的version字段值是一样的,因此再提交另外一个事务的时候就会抛出异常:

org.hibernate.StaleObjectStateException

表示提交请求被拒绝。

我觉得总的思路就是:

现实的需求导致数据库ACID的特性出现,不是所有需求都要求满足ACID,针对不同的需求定义了四种隔离级别。而要想实现隔离就要加锁,加锁可以在数据库层实现,也可以在应用程序层实现。Hibernate的悲观锁和乐观锁就是针对不同的隔离级别的需求,让程序员在应用程序层有一个动态调整事务隔离级别的可能性,这样带来的好处是性能的优化。


你可能感兴趣的:(数据库)